API reference
MarkdownYou rarely call the API directly; the SDKs and the dashboard do. This reference is for building your own client, debugging, or pointing an agent at it.
JSON uses camelCase. Timestamps are ISO 8601 strings in UTC.
| Environment | Base URL |
|---|---|
| Local dev | http://localhost:4000 (iOS simulator) or http://10.0.2.2:4000 (Android emulator) |
| Hosted | https://api.conciergesdk.com |
IDs and keys
Section titled “IDs and keys”IDs are prefixed and URL safe: org_, app_, cus_ (end customer), dev_ (device), cnv_, msg_, mem_, kdoc_. They’re generated on the server, except a message’s clientId, which the SDK generates (a UUID) for idempotency.
app_...is the public app key. It’s safe to embed in apps.ids_...is the identity secret. Server only. It signs user JWTs (HS256).dtk_...is a device token, returned when a device registers.
SDK API
Section titled “SDK API”Base path /v1/sdk. Every call sends X-App-Key: app_.... After registering, every call also sends Authorization: Bearer <deviceToken>.
| Method | Path | Body or query | Returns |
|---|---|---|---|
| GET | /v1/sdk/config |
App key only | { appearance, ai: { enabled, showAsAi }, identity: { requireVerified } }, cached for 300s |
| POST | /v1/sdk/devices |
{ platform, osVersion, appVersion, sdkVersion, model, locale } |
{ deviceId, deviceToken, customerId } |
| POST | /v1/sdk/identify |
{ userId, name?, email?, token?, metadata? } |
{ customerId, verified }. Without a valid token the claim is scoped to this device |
| POST | /v1/sdk/logout |
{ customerId }, a new anonymous customer |
|
| GET | /v1/sdk/conversation |
{ conversation, messages, hasMore }, the latest 50 |
|
| GET | /v1/sdk/messages |
?before=<seq>&limit=50 or ?after=<seq> |
{ messages, hasMore } |
| POST | /v1/sdk/messages |
{ clientId, body, attachments? } |
Message. 201 when created, 200 on a retry with the same clientId |
| POST | /v1/sdk/read |
{ seq } |
{ ok: true } |
| POST | /v1/sdk/typing |
{ ok: true } |
|
| GET | /v1/sdk/unread |
{ count } |
|
| POST | /v1/sdk/push-token |
{ token, environment } |
{ ok: true } |
| POST | /v1/sdk/uploads |
multipart file (JPEG, PNG or HEIC, up to 10MB) |
{ url, width, height, mime } |
| POST | /v1/sdk/context |
{ metadata?, breadcrumbs?: [{ name, at }] } |
{ ok: true } |
Notes:
POST /v1/sdk/identifywithout a token answers401 identity_verification_requiredwhen Require verified users is on (the default for new apps), or when thatuserIdwas ever verified.POST /v1/sdk/messagesanswers403 identity_verification_requiredwhen Require verified users is on and the customer isn’t verified (signed-out devices are allowed while Signed-out chat is on).- The conversation is created on first fetch, so it’s never null. The inbox only lists conversations with at least one message.
- Upload types are sniffed from the bytes, not trusted from the multipart header.
- Customer metadata is string to string, up to 50 keys. The last 50 breadcrumbs are kept.
SDK WebSocket
Section titled “SDK WebSocket”GET /v1/sdk/ws?appKey=app_...&token=<deviceToken> (or send the same credentials as headers). Open it only while the chat is visible. Reconnect with backoff and catch up with GET /v1/sdk/messages?after=<lastSeq>.
Server to client:
{ "type": "message.created", "message": { } }{ "type": "typing", "authorType": "team" }{ "type": "read", "seq": 12 }{ "type": "conversation.updated", "conversation": { } }Client to server: { "type": "ping" } every 25s (answered with pong) and { "type": "typing" }.
Close codes: a bad key or token is rejected with HTTP 401 on the upgrade. 4401 after the upgrade means auth was lost (for example, the app was deleted), so register again instead of reconnecting with the same token. 1001 (server restart) and 4000 (heartbeat timeout) mean reconnect with backoff.
Message
Section titled “Message”{ "id": "msg_x", "conversationId": "cnv_x", "seq": 12, "clientId": "uuid", "authorType": "team", "author": { "name": "Kirill", "avatarUrl": null }, "body": "hi", "attachments": [], "card": null, "createdAt": "2026-10-23T10:00:00Z"}authorType is customer, team, ai or system. attachments are [{ url, width, height, mime }].
Cards:
{ "type": "review", "title": "Enjoying the app?", "buttonLabel": "Rate us", "url": "itms-apps://..." }{ "type": "offer", "title": "Yearly is 40% off this week", "buttonLabel": "View offer", "paywallId": "yearly_40" }Dashboard API
Section titled “Dashboard API”Base path /v1. Authenticated with the dashboard session cookie. Calls act in your active organization, or in the one named by an X-Org-Id header.
| Method | Path | Notes |
|---|---|---|
| GET | /v1/me |
{ user, orgs, activeOrgId } |
| GET, POST | /v1/apps |
Create with { name, iconUrl?, appStoreUrl? } |
| GET, PATCH, DELETE | /v1/apps/:appId |
PATCH accepts name, iconUrl, appStoreId, playPackage (review cards link there; null unlinks) and settings (deep merge) |
| GET | /v1/apps/:appId/setup |
{ connected, firstMessageAt, setupPrompt, appKey, cardHandlers, storeListing }. cardHandlers is { paywall, offer } from the latest device on SDK 0.1.1 or later, or null |
| POST | /v1/apps/:appId/identity-secret/rotate |
{ identitySecret, previousIdentitySecret, previousIdentitySecretExpiresAt }. The old secret keeps verifying for 24 hours |
| GET | /v1/apps/:appId/conversations |
?status=open|snoozed|closed|all&q=&cursor=&limit= returns { items, nextCursor } |
| GET | /v1/conversations/:id |
{ conversation, customer, devices, messages, hasMore } |
| GET | /v1/conversations/:id/messages |
?before=&after=&limit= |
| POST | /v1/conversations/:id/messages |
{ clientId, body, card? }. A team reply pauses the AI on that conversation |
| PATCH | /v1/conversations/:id |
{ status?, assigneeId?, priority?, aiState? } |
| POST | /v1/conversations/:id/read |
Marks the team as read up to the latest message |
| GET | /v1/apps/:appId/customers |
?q=&cursor=&limit= |
| GET | /v1/customers/:id |
Customer, devices and conversation summary |
| GET, POST | /v1/apps/:appId/knowledge |
List, or create { title, content } |
| PATCH, DELETE | /v1/knowledge/:id |
|
| GET | /v1/apps/:appId/stats |
{ open, closed, medianFirstResponseSec, aiResolved } |
Conversation status is open, snoozed or closed. priority is normal, high or urgent. aiState is active, paused or off.
App settings
Section titled “App settings”PATCH /v1/apps/:appId with a partial settings object. It’s deep merged.
{ "appearance": { "accentLight": "#6B4CF2", "accentDark": "#9E85FF", "title": "Support", "welcome": "Hi! How can we help?", "showTeamNames": true, "showTeamPhotos": false }, "ai": { "enabled": false, "model": "claude-haiku-5-5", "showAsAi": true, "handoff": "on_request", "systemPrompt": "" }, "identity": { "requireVerified": true, "allowAnonymous": true }, "push": { "apnsConfigured": false, "fcmConfigured": false }}Privacy: export and delete
Section titled “Privacy: export and delete”For GDPR and CCPA requests from your end users (you are the controller, Concierge is your processor under the DPA). Owner or admin only. Besides the dashboard session, these accept a staff API key (Authorization: Bearer ck_...), so you can automate requests from your backend.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/customers/:id/export |
Everything stored for one customer as JSON: profile, devices, conversations, messages, attachment URLs, cards, offers, purchases, consents, refund requests and more |
| DELETE | /v1/customers/:id |
Hard deletes all of it in one transaction, including uploaded photos, and returns counts per table |
| GET | /v1/apps/:appId/users/:userId/export |
The same, by your own user id (every customer of the app with that verified or claimed id) |
| DELETE | /v1/apps/:appId/users/:userId |
Deletes every customer of the app matching your user id |
For your own dashboard account: GET /v1/account/export, DELETE /v1/account with { "confirm": "<your email>" } (refused with 409 sole_owner while you are the only owner of an organization), and DELETE /v1/org with { "confirm": "<org name>" } (owner only; cancels the subscription, then deletes every app and all data). These take the session cookie only. The same actions are on the Team page and on each customer’s panel in the inbox.
Import conversation history
Section titled “Import conversation history”POST /v1/apps/:appId/conversations/import brings past conversations from your previous support tool in as history. Owner or admin, Pro plan. Takes the dashboard session or a staff API key (Authorization: Bearer ck_...). The body is { "conversations": [...] } in the shape the switch guide shows, at most 100 conversations and 2,000 messages per request (1 MB).
The response lists each conversation as imported, skipped (already_imported or too_many_messages) or failed, with the Concierge conversationId and customerId. Imported conversations never trigger AI replies, push or emails. 402 subscription_required before you subscribe, 429 rate_limited past 120 requests an hour or 50,000 conversations a day per workspace.
Setup prefill
Section titled “Setup prefill”POST /v1/apps/:appId/setup/prefill saves what the setup prompt’s coding agent found in your codebase, for agents without the MCP server. Takes the dashboard session or a staff API key (Authorization: Bearer ck_...). Body: { storeListing?: { appStoreId?, playPackage? }, aiInstructions?, offers?: [...], knowledge?: [{ title, content }] }. Nothing reaches customers until you act: store ids and the AI system prompt only fill empty values, offers are saved with the draft status and articles as drafts, and names that already exist are skipped, so it is safe to run again. The response lists what was created, kept or skipped, plus notes.
Dashboard WebSocket
Section titled “Dashboard WebSocket”GET /v1/ws with the session cookie. Org scoped events: message.created (with appId), conversation.updated, typing, customer.updated and app.connected (the first message from a new app). Send { "type": "typing", "conversationId": "cnv_..." } to show the team typing on the customer’s device.
Errors
Section titled “Errors”Every error looks like this, with a matching HTTP status:
{ "error": { "code": "invalid_request", "message": "body is required", "details": { } } }| Status | Code | When |
|---|---|---|
| 400 | invalid_request |
Validation failed. details has the field issues. |
| 401 | Missing or bad app key, device token or session. | |
| 403 | identity_verification_required |
Verified identity is required and the customer isn’t verified. |
| 404 | Not found, or it belongs to another organization. | |
| 413, 415 | Upload too big, or not JPEG, PNG or HEIC. | |
| 429 | rate_limited |
Slow down. Honor Retry-After. |