Create a project and SDK key
Available
Sign in, name a project, copy its af_sdk_ key once, and rotate it from Projects & SDK. The full secret is never stored in plaintext.
Magic-link sign-in and personal workspace creation are available now.
Name a project from the signed-in empty state. The dashboard shows the full af_sdk_ key once at creation. Copy it immediately; reload and later list responses only include the prefix. The secret is stored as a hash.
Rotate the key from Projects & SDK. Rotation asks for confirmation, revokes the previous active key, and shows the replacement once. Builds that still embed the old key will stop sending.
Treat the key as a project-scoped public credential, not a dashboard password. Native iOS and Android SDKs are not available. The hashed af_sdk_ key authenticates the Preview HTTP ingest route documented in HTTP API. Rotation is an operator dashboard action; ingest does not rotate keys.
iOS SDK quickstart
Preview
The iOS client is not published yet. This is the intended SwiftUI contract, not an install path.
The planned iOS SDK is a SwiftUI drop-in for iOS 16+: a feedback form, a moderated public board, and a My requests view that can show shipped versions. My requests and shipped updates exist as Preview HTTP; this iOS SDK package remains unpublished.
Do not add a package name or Swift Package Manager URL to your project from this page. Those coordinates will be published with the SDK, not guessed here.
Preview HTTP ingest and board exist for native and server clients that already hold a project SDK key. This iOS SDK is not the install path and is not published.
When the SDK ships, a retry must not create a duplicate item, and anonymous reporters must not read another reporter's private requests.
Native and server clients can POST Preview ingest, GET their own requests, GET and ack shipped updates, and GET a moderated public board (and vote) with a hashed project SDK key. Signed-in members can draft and publish a release from /releases. Optional X-AppFeedback-Reporter-JWT documents customer-backend ES256 identity; it is not a substitute for the opaque af_reporter_ token.
Signed-in members can record private feedback at /inbox/new using their dashboard session. Manual items have no reporter identity and do not appear in My requests.
POST /api/v1/feedback is a Preview ingest route. Callers supply their own HTTPS origin and concatenate it with /api/v1/feedback. Do not treat any product domain as a required production base URL; this API is not a production endpoint.
Send Content-Type: application/json, Authorization: Bearer af_sdk_…, X-AppFeedback-Reporter: af_reporter_…, and an Idempotency-Key of 8–200 printable ASCII characters excluding space (0x21–0x7E).
Generate the reporter token locally: 32 cryptographically random bytes, base64url, prefixed with af_reporter_. Persist it on device. The server stores only a SHA-256 hash and cannot recover the token.
Optionally send X-AppFeedback-Reporter-JWT: the customer backend signs ES256 compact JWTs (not AppFeedback and not af_sdk_). The opaque af_reporter_ header is still required on ingest, mine, updates, ack, and vote; the JWT is not a substitute for the opaque af_reporter_ token. A bare external user id header or body field is not authorization.
Register public JWKs (kty, crv, x, y — never d) on Projects & SDK with kid and iss. AppFeedback verifies iss, aud=appfeedback, project_id matching the SDK project, sub, iat/exp within fifteen minutes, and ES256 signature against the registered key. On success, sub is stored only as HMAC-SHA256 of (project id, external user id) with server pepper; raw sub is not stored. paying in the JWT is not returned on Preview routes and is not a public JSON field.
Customer signing recipe (Node.js sketch for documentation only; not shipped as a package): generate an ES256 key pair with generateKeyPairSync("ec", { namedCurve: "P-256" }), export the public JWK, sign a compact JWT whose header is { alg: "ES256", kid, typ: "JWT" } and whose payload includes iss, aud: "appfeedback", sub, project_id, iat, exp (≤ 900 seconds), and optional paying, using sign("SHA256", …, { dsaEncoding: "ieee-p1363" }). Register { kid, iss, jwk } on Projects & SDK. Rotate by uploading a new kid, switching the backend, then revoking the old key.
JSON fields are kind (bug or request), title (trimmed 1–160), body (trimmed 1–10,000), platform (ios or android), and optional appVersion (trimmed 1–80). Unknown fields are rejected. The payload ceiling is 16 KiB.
A first insert and an identical normalized replay both return 201 { id, createdAt }. The response never includes projectId, hashes, or raw tokens. A reused idempotency key with a different normalized body returns 409. Any SDK-key failure returns 401. Invalid input, including a malformed Content-Length, returns 400. 413 is only for an oversized body. Over the Preview per-project or per-IP budget returns 429 { error: RATE_LIMITED } with Retry-After (integer seconds) and no X-RateLimit-* headers. A Free workspace at its lifetime item cap returns 402 { error: PLAN_LIMIT } on a new ingest; a matching idempotency replay is still 201. Dashboard reads and export are unaffected.
After a network drop, timeout, or 5xx, retry the same SDK key, reporter token, idempotency key, and JSON body. After 429, wait Retry-After seconds and retry the same credentials and body. These Preview limits are PostgreSQL fixed windows in the app. They are not a WAF, not distributed-quota production controls, and not a license to flood.
GET /api/v1/feedback/mine lists the calling reporter’s own submissions for the project authorized by the SDK key. Send Authorization: Bearer af_sdk_… and X-AppFeedback-Reporter: af_reporter_…. Query accepts only limit (integer 1–100, default 50) and cursor (the last returned item UUID), without repeated keys. The same Preview per-project and per-IP limits apply; over budget is 429 { error: RATE_LIMITED } with Retry-After.
The JSON is { items: [{ id, kind, title, body, platform, appVersion, status, shippedInVersion, createdAt, updatedAt }], nextCursor }. Dates are ISO-8601. status is the current semantic group only. shippedInVersion is a string or JSON null. Merged source rows belonging to the reporter are included with their original fields; merge targets are not followed and their ids and content are not returned. Missing, malformed, or revoked SDK keys and missing or malformed reporter tokens return 401. An unknown well-formed reporter token returns an empty page and does not create a reporter. Invalid query or an inaccessible cursor returns 400. Unexpected failures return 500 { error: INTERNAL_ERROR } with Cache-Control: no-store and do not include SQL or token details. Responses use Cache-Control: no-store.
GET /api/v1/feedback/updates lists unread shipped updates for the calling reporter. Send the same Authorization: Bearer af_sdk_… and X-AppFeedback-Reporter: af_reporter_… headers. Query accepts only limit (integer 1–100, default 50) and cursor (the last returned update UUID), without repeated keys. Missing or malformed reporter tokens return 401. An unknown well-formed reporter token returns an empty page and does not create a reporter.
The JSON is { items: [{ id, feedbackId, kind, title, platform, shippedInVersion, releaseTitle, publishedAt }], nextCursor }. Dates are ISO-8601. There is no item body, notes, or projectId. POST /api/v1/feedback/updates/ack with Content-Type: application/json and { ids: ["uuid"] } acknowledges those rows. Success is 200 { ids } in request order for ids that belong to this reporter, including already-acked rows. Foreign ids are omitted. Empty ids is 400. The payload ceiling is 16 KiB. Responses use Cache-Control: no-store. Preview per-project and per-IP rate limits apply to mine, not to GET /api/v1/feedback/updates or POST /api/v1/feedback/updates/ack.
GET /api/v1/feedback/board lists published items for one platform. Callers supply their own HTTPS origin and concatenate it with /api/v1/feedback/board. Send Authorization: Bearer af_sdk_…. Query requires platform (ios or android) and accepts optional limit (integer 1–100, default 50) and cursor (the last returned item UUID), without repeated keys. Staging is not listed. Hidden, inbox, rejected, and merged sources are omitted the same way as missing ids.
GET /api/v1/feedback/board/:itemId returns one published item. Query requires platform. Staging is not listed. Hidden, staged, tombstone, other-project, and other-platform ids return 404 { error: NOT_FOUND }.
The JSON is { items: [{ id, kind, title, body, platform, votes, voted, status, statusLabel, shippedInVersion, createdAt, updatedAt }], nextCursor }. Dates are ISO-8601. status is the semantic group. platform is the original submission platform. shippedInVersion is a string or JSON null. X-AppFeedback-Reporter is optional on GET: omit it for voted: false without inserting a reporter. A present but malformed reporter returns 401. Responses use Cache-Control: no-store. GET /api/v1/feedback/board does not include assignee.
PUT / DELETE /api/v1/feedback/board/:itemId/vote records or removes one vote per reporter token per item. Query requires platform. No JSON body. Send Authorization: Bearer af_sdk_… and X-AppFeedback-Reporter: af_reporter_…. Missing or malformed reporter returns 401. PUT is an idempotent insert; DELETE is an idempotent remove. The JSON is { id, votes, voted }. Reporter paying status is a server-side, per-project attribute meaning the reporter currently pays for the customer's app. It is not AppFeedback Free/Indie billing. Clients cannot set it.
These routes are for native and server HTTP clients. There is no browser CORS and no session cookie. They are not production. Preview board and vote routes use the hashed project SDK key. The same Preview per-project and per-IP limits apply to board and vote; over budget is 429 { error: RATE_LIMITED } with Retry-After. Invalid or unregistered reporter JWTs return 401 with no anonymous fallback when the header is present.
Signed-in workspace members can triage ingested items in the dashboard inbox: change project status, append internal notes, store private per-platform staging intent, and reversibly merge duplicates. Signed-in members can filter that inbox list by kind, platform, app version, and project status. Filters are dashboard URL query params on /inbox, not a native SDK operation. Signed-in members can draft and publish a release from /releases. Publishing stamps attached completed items and their current merge sources, creates unread reporter updates, and does not change status or visibility. Staging is still not a public board. Signed-in /board is a Preview progress board: public items for the first project sit in /workflows columns; operators move them with Move to or restrained drag via the private item POST. Signed-in /inbox rows and /board cards can show and change a workspace-member assignee. A separate Staged — not public section is not a column. Staging is not a public board and creates no public route. There is still no GET /api/v1/feedback collection. Native iOS and Android SDKs are unpublished. Public cards and the inspector can show a paying-supporter count (a subset of votes). Staging still shows no paid chrome. GET /api/v1/feedback/board does not include paidVotes.
Signed-in members can download appfeedback.workspace.v1 JSON from /settings/data using the dashboard session, not the SDK key. Native SDKs must not call that dashboard export. The workspace owner manages Stripe Checkout and Customer Portal from /billing. Free is one project and 1,000 lifetime items; Indie is $15/month or $150/year. After cancel, existing items stay readable and export stays available. The workspace owner can invite members from /team by email and remove members. A leftover assignee still shows as left workspace. The workspace owner can delete the workspace from /settings/data by typing their email. Deletion removes projects, keys, and feedback immediately, signs the owner out, and cannot be undone. Sign in afterwards creates a new empty personal workspace.
Workspace JSON export
Available
Signed-in members download appfeedback.workspace.v1 JSON from /settings/data. The file omits SDK secrets and reporter token hashes. The workspace owner can delete from the same page by typing their email.
Items include source (sdk or manual); manual items have a null reporterId.
Open /settings/data and choose Export workspace. The download is one JSON document of this workspace’s product records.
The file includes projects, statuses, SDK key prefixes, reporter ids, items (including merged), votes, activity, releases, and reporter updates. It does not include SDK secrets, reporter token hashes, idempotency keys, rate-limit rows, or other workspaces.
Export is available on Free and after a downgrade. This is a dashboard session action, not a native SDK operation.
The workspace owner can invite members from /team by email and can remove members. Members can leave. Leftover assignees remain visible. The workspace owner can delete from /settings/data by typing their email. Deletion removes projects, keys, and feedback immediately and signs the owner out. It cannot be undone. Sign in afterwards creates a new empty personal workspace.