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.
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.
// 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
});
| Surface | Auth | Notes |
|---|---|---|
/v1/sdk/* | X-API-Key | Project API key. Rate-limited per key. |
/v1/public/* | None | Open, rate-limited by IP. Some actions need a verified identity. |
/v1/portal/* | Session cookie | Team/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.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "title is required",
"details": [{ "path": "title", "message": "Required" }]
}
}
| Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | A field is missing or malformed |
401 | MISSING_API_KEY | No X-API-Key header was sent |
401 | INVALID_API_KEY | Key not found or revoked |
403 | FORBIDDEN | Identity does not own the resource |
404 | NOT_FOUND | Resource does not exist in this project |
429 | RATE_LIMITED | Too many requests — see Retry-After |
503 | SERVICE_UNAVAILABLE | Transient upstream failure — retry |
Pagination
List endpoints are cursor-paginated. Pass a limit and, to fetch the next page, the cursor returned in pagination. When hasMore is false you have reached the end.
curl "https://api.swake.io/v1/sdk/submissions?externalUserId=user_123&limit=20&cursor=01J8..." \
-H "X-API-Key: ep_live_..."
{
"data": [ /* ...items... */ ],
"pagination": { "hasMore": true, "cursor": "01J9..." }
}
/v1/sdk/identifyIdentify 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 field | Type | Required | Description |
|---|---|---|---|
externalUserId | string | Yes | Your internal user ID |
email | string | No | User email address |
name | string | No | Display name |
image | string | No | Avatar URL, shown on the public portal |
customMetadata | object | No | Arbitrary key/value traits (fully replaced) |
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" }
}'
{
"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"
}
}
/v1/sdk/submissionsCreate 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 field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | bug · feature · question · other |
title | string | Yes | 1–500 characters |
description | string | No | Up to 10,000 characters |
externalUserId | string | No | Attributes the submission to a user |
deviceInfo | object | No | Device context; deviceInfo.os recommended |
customFields | object | No | fieldId → value map of project custom fields (see below) |
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" }
}'
{
"data": {
"id": "01J8...",
"type": "bug",
"title": "Checkout button unresponsive on mobile",
"status": "open",
"priority": "medium",
"createdAt": "2026-03-25T10:00:00Z"
}
}
/v1/sdk/custom-fieldsList 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).
curl https://api.swake.io/v1/sdk/custom-fields \
-H "X-API-Key: ep_live_..."
{
"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 }
]
}
/v1/sdk/submissionsList 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 param | Required | Description |
|---|---|---|
externalUserId | Yes | User whose submissions to return |
status | No | Filter by status, e.g. open |
limit | No | Page size (default 20) |
cursor | No | Pagination cursor from a previous response |
curl "https://api.swake.io/v1/sdk/submissions?externalUserId=user_123&status=open&limit=10" \
-H "X-API-Key: ep_live_..."
{
"data": [
{ "id": "01J8...", "type": "bug", "title": "...", "status": "open", "createdAt": "...", "isPublished": true }
],
"pagination": { "hasMore": true, "cursor": "01J9..." }
}
/v1/sdk/submissions/:idGet 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.
curl https://api.swake.io/v1/sdk/submissions/01J8... \
-H "X-API-Key: ep_live_..."
{
"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": "..." }
]
}
}
/v1/sdk/submissions/:id/commentsAdd a comment
Let a user reply on their own submission. The externalUserId must match the submission owner, otherwise the API returns 403 FORBIDDEN.
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." }'
{
"data": {
"id": "01J8...",
"submissionId": "01J8...",
"authorType": "user",
"authorId": "01J8...",
"body": "Still happening on 2.4.2.",
"createdAt": "2026-03-25T10:05:00Z"
}
}
/v1/uploads/presignUpload 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.
# 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
}'
{
"uploadUrl": "https://<r2-signed-url>",
"r2Key": "attachments/01J8...",
"attachmentId": "01J8...",
"expiresAt": "2026-03-25T10:15:00Z"
}
# 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.
/v1/sdk/notificationsNotifications
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.
curl "https://api.swake.io/v1/sdk/notifications?externalUserId=user_123&unreadOnly=true" \
-H "X-API-Key: ep_live_..."
{
"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 }
}
/v1/sdk/surveys/eligibleEligible 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.
curl "https://api.swake.io/v1/sdk/surveys/eligible?external_user_id=user_123" \
-H "X-API-Key: ep_live_..."
{
"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 }
]
}
]
}
/v1/sdk/surveys/:surveyId/responsesSubmit a survey response
Record a response. Idempotent per user per survey — a repeat call returns the existing response with alreadySubmitted: true. Returns 201.
| Body field | Type | Required | Description |
|---|---|---|---|
external_user_id | string | No | Identifies the responder |
answers | array | Yes | At least one { question_id, value } |
device_info | object | No | platform · app_version · os_version |
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 } ]
}'
{
"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.
| Method | Path | Description |
|---|---|---|
| GET | /v1/public/portal/:slug | Bootstrap a portal (branding, boards, flags) |
| GET | /v1/public/boards/:boardId | Board items with vote & comment counts |
| POST | /v1/public/boards/:boardId/items/:itemId/vote | Cast a vote (identity required) |
| POST | /v1/public/boards/:boardId/items/:itemId/comments | Add a public comment |
| GET | /v1/public/roadmaps/:slug | Roadmap columns and items |
| POST | /v1/public/projects/:projectId/submissions | Submit feedback (identity required) |
# 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:
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.
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.