API reference · 2026-10-01

QuiC Platform API

Message QuiC users who opted in to your app. No message is ever delivered without the user's opt-in. Business chats are NOT end-to-end encrypted. Free during the preview; rate limits and quotas apply.

Base URL https://api.quic.chat/platform/v1. Authenticate with Authorization: Bearer qk_… (keys & scopes). Errors use one envelope. Webhook payloads are in the events reference.

Download openapi.json (OpenAPI 3.1.0) to import into Postman, Insomnia or a code generator.

Messages#

post/messages— Send a message

Queues a text or media message to a user who opted in. Sandbox keys reach accepted testers only; internal_live apps reach their own organization (and may address by work email with a live key); public_live apps reach any organization that allows external businesses. Outside the 24h window after the user's last message an app may send at most 3 messages per user per 24h.

Scope: messages:send

Parameters

  • Idempotency-Key (header, string, required) — Required. Any unique string (<=255 printable ASCII). Kept 24h: same key + same body replays the first response; same key + different body is 422.

Request body

POST /messages request body
FieldTypeNotes
torequiredobject | object
to (option 1).appUserIdrequiredstringPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz · pattern ^u_[0-9A-Za-z]{22}$
to (option 2).emailrequiredstring (email)internal_live apps with a live key only (D4) · max length 254
typerequired"text" | "image" | "video" | "audio" | "file"
textstringRequired for text; caption for media · max length 4096
mediaIdstringFrom POST /media; required for media types · min length 1 · max length 64
clientReferencestringEchoed in message.status events · min length 1 · max length 128

Response 202 — Accepted

POST /messages response 202
FieldTypeNotes
idrequiredstring
statusrequired"accepted"
appUserIdrequiredstringPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz · pattern ^u_[0-9A-Za-z]{22}$
typerequired"text" | "image" | "video" | "audio" | "file"
createdAtrequiredstring (date-time)
clientReferencerequiredstring | null

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

get/messages/{id}— Get a message

Scope: messages:read

Parameters

  • id (path, string, required) — Message id

Response 200 — The message

GET /messages/{id} response 200
FieldTypeNotes
idrequiredstring
directionrequired"outbound" | "inbound"
appUserIdrequiredstring | nullPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz
typerequiredstring
textrequiredstring | null
mediarequiredobject[]
media[].idrequiredstring
media[].mimeTyperequiredstring | null
media[].fileNamerequiredstring
media[].sizerequiredinteger
statusrequired"sent" | "delivered" | "read" | "received"
clientReferencerequiredstring | null
createdAtrequiredstring (date-time)
deliveredAtrequiredstring (date-time) | null
readAtrequiredstring (date-time) | null

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

Media#

post/media— Create an upload

Returns a presigned PUT URL. Upload the bytes with the same Content-Type within 15 minutes, then reference media.id in POST /messages. Images, video and audio up to 16 MB; documents up to 100 MB. 100 uploads per hour.

Scope: media:write

Request body

POST /media request body
FieldTypeNotes
fileNamerequiredstringmin length 1 · max length 255
mimeTyperequired"image/jpeg" | "image/png" | "image/webp" | "image/gif" | "video/mp4" | "video/3gpp" | "video/quicktime" | "audio/mpeg" | "audio/mp4" | "audio/aac" | "audio/ogg" | "audio/amr" | "application/pdf" | "text/plain" | "text/csv" | "application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/vnd.ms-powerpoint" | "application/vnd.openxmlformats-officedocument.presentationml.presentation"
sizerequiredintegerBytes. Images/video/audio <= 16 MB, documents <= 100 MB · ≤ 104857600

Response 201 — Upload created

POST /media response 201
FieldTypeNotes
mediarequiredobject
media.idrequiredstring
media.sourcerequired"app" | "user"
media.kindrequired"image" | "video" | "audio" | "document" | "other"
media.mimeTyperequiredstring
media.fileNamerequiredstring
media.sizerequiredinteger
media.statusrequired"pending_upload" | "ready"
media.urlrequiredstring | nullPresigned download URL, valid 1 hour
media.urlExpiresAtrequiredstring (date-time) | null
media.createdAtrequiredstring (date-time)
uploadrequiredobject
upload.urlrequiredstringPUT the bytes here within 15 minutes
upload.methodrequired"PUT"
upload.headersrequiredobject
upload.expiresAtrequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

get/media/{id}— Get media

Your uploads, or media a user sent you (message.received media ids). Returns a 1-hour download URL.

Scope: messages:read

Parameters

  • id (path, string, required) — Media id

Response 200 — The media

GET /media/{id} response 200
FieldTypeNotes
idrequiredstring
sourcerequired"app" | "user"
kindrequired"image" | "video" | "audio" | "document" | "other"
mimeTyperequiredstring
fileNamerequiredstring
sizerequiredinteger
statusrequired"pending_upload" | "ready"
urlrequiredstring | nullPresigned download URL, valid 1 hour
urlExpiresAtrequiredstring (date-time) | null
createdAtrequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

get/users/{appUserId}— Get a user's consent + window

Scope: consents:read

Parameters

  • appUserId (path, string, required) — Per-app user id

Response 200 — The user

GET /users/{appUserId} response 200
FieldTypeNotes
appUserIdrequiredstringPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz · pattern ^u_[0-9A-Za-z]{22}$
consentrequiredobject
consent.appUserIdrequiredstringPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz · pattern ^u_[0-9A-Za-z]{22}$
consent.statusrequired"opted_in" | "opted_out" | "blocked"
consent.sourcerequired"user_initiated" | "profile_allow" | "opt_in_link" | "sandbox_tester"
consent.refrequiredstring | null
consent.optedInAtrequiredstring (date-time) | null
consent.optedOutAtrequiredstring (date-time) | null
consent.updatedAtrequiredstring (date-time)
windowrequiredobject
window.openrequiredbooleantrue within 24h of the user's last message
window.expiresAtrequiredstring (date-time) | null

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

get/consents— List consents

Scope: consents:read

Parameters

  • status (query, "opted_in" | "opted_out" | "blocked")
  • limit (query, integer)
  • cursor (query, string)

Response 200 — A page of consents

GET /consents response 200
FieldTypeNotes
datarequiredobject[]
data[].appUserIdrequiredstringPer-app user id, e.g. u_4f9XkQ2bT7mN1pR8sV0wYz · pattern ^u_[0-9A-Za-z]{22}$
data[].statusrequired"opted_in" | "opted_out" | "blocked"
data[].sourcerequired"user_initiated" | "profile_allow" | "opt_in_link" | "sandbox_tester"
data[].refrequiredstring | null
data[].optedInAtrequiredstring (date-time) | null
data[].optedOutAtrequiredstring (date-time) | null
data[].updatedAtrequiredstring (date-time)
nextCursorrequiredstring | null

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

post/optin-links— Create an opt-in link

A signed link to your business profile. Opening it never opts the user in; they must tap Allow in QuiC. The ref comes back in consent.updated.

Scope: optin_links:create

Request body

POST /optin-links request body
FieldTypeNotes
refstringYour own reference, echoed in consent.updated · pattern ^[A-Za-z0-9_.:-]{1,64}$
expiresInDaysinteger≥ 1 · ≤ 90

Response 201 — Link created

POST /optin-links response 201
FieldTypeNotes
urlrequiredstring
tokenrequiredstring
refrequiredstring | null
expiresAtrequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

Business profile#

get/business-profile— Get your business profile

Response 200 — Profile

GET /business-profile response 200
FieldTypeNotes
appIdrequiredstring
handlerequiredstring
displayNamerequiredstring
avatarUrlrequiredstring | null
aboutrequiredstring | null
websiteUrlrequiredstring | null
domainrequiredstring | null
categoryrequiredstring | null
verificationStatusrequired"unverified" | "pending" | "verified" | "revoked"
verifiedAtrequiredstring (date-time) | null
updatedAtrequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

patch/business-profile— Update your business profile

Scope: profile:write

Request body

PATCH /business-profile request body
FieldTypeNotes
displayNamestringmin length 2 · max length 60
aboutstring | null
websiteUrlstring (uri) | null
categorystring | null
avatarMediaIdstring | nullA ready image from POST /media, <= 5 MB

Response 200 — Profile

PATCH /business-profile response 200
FieldTypeNotes
appIdrequiredstring
handlerequiredstring
displayNamerequiredstring
avatarUrlrequiredstring | null
aboutrequiredstring | null
websiteUrlrequiredstring | null
domainrequiredstring | null
categoryrequiredstring | null
verificationStatusrequired"unverified" | "pending" | "verified" | "revoked"
verifiedAtrequiredstring (date-time) | null
updatedAtrequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.

Diagnostics#

get/ping— Check a key

Response 200 — Key is valid

GET /ping response 200
FieldTypeNotes
okrequiredtrue
appIdrequiredstring
appStatusrequiredstring
environmentrequired"sandbox" | "live"
scopesrequired("messages:send" | "messages:read" | "media:write" | "consents:read" | "optin_links:create" | "profile:write" | "webhooks:manage" | "templates:manage")[]
apiVersionrequiredstring
requestIdrequiredstring
timerequiredstring (date-time)

Errors: 400 invalid_request / invalid_json / idempotency_key_required, 401 invalid_api_key, 403 insufficient_scope / app_suspended / key_environment_mismatch / consent_required / recipient_not_tester / recipient_outside_org / recipient_org_blocks_businesses, 404 not_found, 409 idempotency_in_progress / media_not_uploaded, 422 idempotency_key_reused / media_invalid, 429 rate_limited / daily_quota_exceeded / business_initiated_cap / media_rate_limited. See error codes.