Developers/API Reference
REST API · v1

API Reference

The Swake REST API lets you submit feedback, identify users, run surveys, and handle notifications from any platform or language — plain HTTPS with JSON. Build a fully custom feedback experience while your team triages everything from the Swake portal.

Introduction

Every endpoint lives under https://api.swake.io/v1, follows REST conventions, and returns JSON. It is language- and platform-agnostic — call it from a mobile app, a web front-end, a backend service, or a script. There are three surfaces: the SDK API (authenticated with a project API key), the Public API (no auth, for public portals/boards), and outgoing Webhooks.

Authentication & API Keys

SDK endpoints use your project API key in the X-API-Key header. Keys start with ep_live_ and belong to a single project.

REST + JSON

Predictable resource URLs and JSON everywhere. Errors always include a machine-readable code and a human-readable message.

HMAC-Signed Webhooks

Every outgoing delivery is signed with a per-endpoint HMAC-SHA256 secret so you can verify authenticity on your server.

Rate Limiting & Pagination

SDK endpoints are rate-limited at 100 req/min per key. List endpoints use cursor-based pagination with a configurable page size.


Authentication

Authenticate SDK requests by sending your project API key in the X-API-Key header. Create and rotate keys in the Swake portal under Project → Installation → API.

bash
curl https://api.swake.io/v1/sdk/ping \
  -H "X-API-Key: ep_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Never expose your key in a browser. The ep_live_ key grants write access to your project. In front-end apps, route requests through a same-site backend proxy that injects the key server-side — see the pattern below.

typescript
// Recommended: proxy through your own backend so the
// secret key never ships to the browser.
Swake.init({
  apiKey: 'proxy-placeholder',        // your backend holds the real key
  baseUrl: 'https://yourapp.com/api/swake',
  credentials: 'include',             // send your session cookie
});
SurfaceAuthNotes
/v1/sdk/*X-API-KeyProject API key. Rate-limited per key.
/v1/public/*NoneOpen, rate-limited by IP. Some actions need a verified identity.
/v1/portal/*Session cookieTeam/admin surface — out of scope for this reference.

Rate limiting

SDK endpoints are limited to 100 requests per minute per API key. When you exceed the limit the API returns 429 with the error code RATE_LIMITED, a Retry-After header, and an error.retryAfter value (seconds).

Respect the Retry-After header and back off exponentially. Ad serving has its own separate 100 req/min budget.


Errors

Errors use conventional HTTP status codes and always return the same JSON shape. Validation errors add a details array.

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "title is required",
    "details": [{ "path": "title", "message": "Required" }]
  }
}
StatusCodeMeaning
400VALIDATION_ERRORA field is missing or malformed
401MISSING_API_KEYNo X-API-Key header was sent
401INVALID_API_KEYKey not found or revoked
403FORBIDDENIdentity does not own the resource
404NOT_FOUNDResource does not exist in this project
429RATE_LIMITEDToo many requests — see Retry-After
503SERVICE_UNAVAILABLETransient upstream failure — retry


POST/v1/sdk/identify

Identify a user

Create or update an end user by their externalUserId (your own user ID). Call this after your app authenticates the user so their feedback, surveys, and notifications are tied to them. Omitted fields are preserved.

Body fieldTypeRequiredDescription
externalUserIdstringYesYour internal user ID
emailstringNoUser email address
namestringNoDisplay name
imagestringNoAvatar URL, shown on the public portal
customMetadataobjectNoArbitrary key/value traits (fully replaced)
bash
curl -X POST https://api.swake.io/v1/sdk/identify \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserId": "user_123",
    "email": "jane@example.com",
    "name": "Jane Smith",
    "image": "https://cdn.example.com/avatars/jane.jpg",
    "customMetadata": { "plan": "pro" }
  }'
json
{
  "data": {
    "id": "01J8...",
    "externalId": "user_123",
    "email": "jane@example.com",
    "name": "Jane Smith",
    "customMetadata": { "plan": "pro" },
    "firstSeenAt": "2026-03-25T10:00:00Z",
    "lastSeenAt": "2026-03-25T10:00:00Z"
  }
}

POST/v1/sdk/submissions

Create a submission

Submit a bug report, feature request, question, or general feedback. Passing externalUserId associates the submission with a user (and auto-casts their vote). Returns 201. Include a customFields map to set the project's custom field values — they're validated against the project definitions (required fields enforced), same as the public feedback form.

Body fieldTypeRequiredDescription
typestringYesbug · feature · question · other
titlestringYes1–500 characters
descriptionstringNoUp to 10,000 characters
externalUserIdstringNoAttributes the submission to a user
deviceInfoobjectNoDevice context; deviceInfo.os recommended
customFieldsobjectNofieldId → value map of project custom fields (see below)
bash
curl -X POST https://api.swake.io/v1/sdk/submissions \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bug",
    "title": "Checkout button unresponsive on mobile",
    "description": "Tapping Pay does nothing on iOS 17.",
    "externalUserId": "user_123",
    "deviceInfo": { "os": "iOS 17.2", "appVersion": "2.4.1" },
    "customFields": { "01JCF...": "Pro" }
  }'
json
{
  "data": {
    "id": "01J8...",
    "type": "bug",
    "title": "Checkout button unresponsive on mobile",
    "status": "open",
    "priority": "medium",
    "createdAt": "2026-03-25T10:00:00Z"
  }
}

GET/v1/sdk/custom-fields

List custom fields

Return the project's custom field definitions so you can render typed inputs — text, number, date, or dropdown — before submitting. The project is resolved from your API key, so no parameters are needed. Send the collected values back under customFields on POST /v1/sdk/submissions (and PATCH /v1/sdk/submissions/:id).

bash
curl https://api.swake.io/v1/sdk/custom-fields \
  -H "X-API-Key: ep_live_..."
json
{
  "customFields": [
    { "id": "01JCF...", "name": "Account tier", "fieldType": "dropdown", "options": ["Free", "Pro"], "required": true, "position": 0 },
    { "id": "01JCG...", "name": "Requested by", "fieldType": "text", "options": null, "required": false, "position": 1 }
  ]
}

GET/v1/sdk/submissions

List a user's submissions

Return the feedback history for one user, most recent first. Filter by status and page with limit / cursor. Each item carries isPublished — true only when the feedback is live on a public board (a visible item on an active board, not unpublished) — so a "My Feedback" UI can show an Unpublish action only where it applies.

Query paramRequiredDescription
externalUserIdYesUser whose submissions to return
statusNoFilter by status, e.g. open
limitNoPage size (default 20)
cursorNoPagination cursor from a previous response
bash
curl "https://api.swake.io/v1/sdk/submissions?externalUserId=user_123&status=open&limit=10" \
  -H "X-API-Key: ep_live_..."
json
{
  "data": [
    { "id": "01J8...", "type": "bug", "title": "...", "status": "open", "createdAt": "...", "isPublished": true }
  ],
  "pagination": { "hasMore": true, "cursor": "01J9..." }
}

GET/v1/sdk/submissions/:id

Get a submission

Fetch a single submission with its full comment thread and attachments. Each attachment includes a short-lived viewUrl (1-hour expiry). The response also carries isPublished and customFieldValues (a fieldId → value map) for pre-filling an edit form.

bash
curl https://api.swake.io/v1/sdk/submissions/01J8... \
  -H "X-API-Key: ep_live_..."
json
{
  "data": {
    "id": "01J8...",
    "type": "bug",
    "title": "Checkout button unresponsive on mobile",
    "status": "in_progress",
    "isPublished": true,
    "customFieldValues": { "01JCF...": "Pro" },
    "comments": [
      { "id": "01J8...", "authorType": "team", "body": "Thanks — looking into it.", "createdAt": "..." }
    ],
    "attachments": [
      { "id": "01J8...", "filename": "screenshot.png", "viewUrl": "https://...", "expiresAt": "..." }
    ]
  }
}

POST/v1/sdk/submissions/:id/comments

Add a comment

Let a user reply on their own submission. The externalUserId must match the submission owner, otherwise the API returns 403 FORBIDDEN.

bash
curl -X POST https://api.swake.io/v1/sdk/submissions/01J8.../comments \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "externalUserId": "user_123", "body": "Still happening on 2.4.2." }'
json
{
  "data": {
    "id": "01J8...",
    "submissionId": "01J8...",
    "authorType": "user",
    "authorId": "01J8...",
    "body": "Still happening on 2.4.2.",
    "createdAt": "2026-03-25T10:05:00Z"
  }
}

POST/v1/uploads/presign

Upload an attachment

Attachments use a three-step flow: request a presigned URL, upload the raw bytes directly to storage, then confirm. Limits: up to 5 files per submission, ≤ 10 MB each, of type image/png · jpeg · webp · gif, video/mp4 · quicktime, or application/pdf.

bash
# 1. Ask for a presigned upload URL
curl -X POST https://api.swake.io/v1/uploads/presign \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "submissionId": "01J8...",
    "filename": "screenshot.png",
    "contentType": "image/png",
    "sizeBytes": 82345
  }'
json
{
  "uploadUrl": "https://<r2-signed-url>",
  "r2Key": "attachments/01J8...",
  "attachmentId": "01J8...",
  "expiresAt": "2026-03-25T10:15:00Z"
}
bash
# 2. PUT the raw bytes to uploadUrl (Content-Type must match), then confirm
curl -X POST https://api.swake.io/v1/uploads/confirm \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "attachmentId": "01J8..." }'

To read an attachment later, call GET /v1/attachments/:id/url (accepts an API key or a portal session) — it returns a fresh, time-limited url.


GET/v1/sdk/notifications

Notifications

List a user's notifications (team replies and status changes). Companion endpoints: GET /v1/sdk/notifications/unread-count, PATCH /v1/sdk/notifications/:id/read, and POST /v1/sdk/notifications/read-all.

bash
curl "https://api.swake.io/v1/sdk/notifications?externalUserId=user_123&unreadOnly=true" \
  -H "X-API-Key: ep_live_..."
json
{
  "data": [
    {
      "id": "01J8...",
      "submissionId": "01J8...",
      "type": "team_reply",
      "payload": { "submissionTitle": "...", "commentPreview": "...", "teamMemberName": "Alex" },
      "readAt": null,
      "createdAt": "2026-03-25T10:05:00Z"
    }
  ],
  "pagination": { "hasMore": false, "cursor": null }
}

GET/v1/sdk/surveys/eligible

Eligible surveys

Return the active surveys, polls, and NPS prompts the identified user is eligible to see. Targeting (days active, segments, traits) is evaluated server-side from the user's record — pass only external_user_id.

bash
curl "https://api.swake.io/v1/sdk/surveys/eligible?external_user_id=user_123" \
  -H "X-API-Key: ep_live_..."
json
{
  "eligible": [
    {
      "id": "01J8...",
      "type": "nps",
      "title": "How likely are you to recommend us?",
      "presentation": "bottom_sheet",
      "webPosition": "bottom-right",
      "questions": [
        { "id": "q1", "type": "rating", "text": "0–10", "required": true }
      ]
    }
  ]
}

POST/v1/sdk/surveys/:surveyId/responses

Submit a survey response

Record a response. Idempotent per user per survey — a repeat call returns the existing response with alreadySubmitted: true. Returns 201.

Body fieldTypeRequiredDescription
external_user_idstringNoIdentifies the responder
answersarrayYesAt least one { question_id, value }
device_infoobjectNoplatform · app_version · os_version
bash
curl -X POST https://api.swake.io/v1/sdk/surveys/01J8.../responses \
  -H "X-API-Key: ep_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "user_123",
    "answers": [ { "question_id": "q1", "value": 9 } ]
  }'
json
{
  "response": {
    "id": "01J8...",
    "surveyId": "01J8...",
    "externalUserId": "user_123",
    "startedAt": "2026-03-25T10:06:00Z",
    "completedAt": "2026-03-25T10:06:03Z",
    "alreadySubmitted": false
  }
}

Public API

The /v1/public/* endpoints power public feedback portals, voting boards, roadmaps, and changelogs. They require no API key (rate-limited by IP); actions that write on behalf of a person need a verified identity — an externalUserId or an email verified via magic link.

MethodPathDescription
GET/v1/public/portal/:slugBootstrap a portal (branding, boards, flags)
GET/v1/public/boards/:boardIdBoard items with vote & comment counts
POST/v1/public/boards/:boardId/items/:itemId/voteCast a vote (identity required)
POST/v1/public/boards/:boardId/items/:itemId/commentsAdd a public comment
GET/v1/public/roadmaps/:slugRoadmap columns and items
POST/v1/public/projects/:projectId/submissionsSubmit feedback (identity required)
bash
# No API key — public endpoints are open (rate-limited by IP)
curl https://api.swake.io/v1/public/portal/acme

Webhooks

Swake POSTs a signed JSON event to any HTTPS URL you register whenever feedback lifecycle events occur — feedback.created, feedback.status_changed, vote.cast, survey.response_completed, and more. Each delivery carries four headers:

bash
X-Swake-Signature:   sha256=a1b2c3d4e5f6...
X-Swake-Timestamp:   1711353600
X-Swake-Event:       feedback.created
X-Swake-Delivery-Id: evt_01J8...

Verify authenticity by recomputing HMAC-SHA256(secret, "{timestamp}.{rawBody}") and comparing it to X-Swake-Signature. Always use the raw request body and a timing-safe comparison, and reject deliveries older than 5 minutes.

javascript
const crypto = require('crypto');

function verify(rawBody, headers, secret) {
  const ts = headers['x-swake-timestamp'];
  const sig = headers['x-swake-signature'].replace('sha256=', '');

  // Reject deliveries older than 5 minutes (replay defence)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('stale');

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error('bad signature');
  }
}

Registering endpoints, the full event catalog, payload shapes, retry schedule, and secret rotation are covered in the dedicated Webhooks guide.