Skip to content

API reference

Markdown

You 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 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.

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/identify without a token answers 401 identity_verification_required when Require verified users is on (the default for new apps), or when that userId was ever verified.
  • POST /v1/sdk/messages answers 403 identity_verification_required when 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.

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.

{
"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" }

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, settings (deep merge)
GET /v1/apps/:appId/setup { connected, firstMessageAt, setupPrompt, appKey, identitySecret, previousIdentitySecret, previousIdentitySecretExpiresAt }
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.

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 }
}

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.

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.

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.

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.