API reference
The TinyChat REST surface.
Every endpoint you need to send messages, manage conversations, and upload attachments from any server or client. Calls are authenticated with a single header. All requests share the same base URL as your dashboard.
basehttps://api.tinychat.devor your dashboard hostOne header, every call.
Send your tenant's publishable key in the x-tinychat-api-key header on every request. Keys start with pk_live_ or pk_test_. Real keys are issued in your dashboard — never check them into source control.
CORS
- Origin: any (
*) - Methods:
GET, POST, PATCH, DELETE, OPTIONS - Headers:
content-type, x-tinychat-api-key
Example request
curl https://api.tinychat.dev/v1/config \
-H "x-tinychat-api-key: pk_live_REPLACE_ME"/api/v1/configBootstrap SDK config
Fetch the public realtime credentials and the tenant's metadata. The SDK calls this once on launch and caches the result for the session.
Headers
x-tinychat-api-keystringrequired | Your tenant key (pk_live_… or pk_test_…). |
Response
Returns the tenant identity and the Supabase Realtime endpoint used for live subscriptions. The channel_prefix is what the SDK joins to <id> to form a channel name.
{
"tenant": {
"id": "11111111-2222-3333-4444-555555555555",
"name": "Acme Logistics"
},
"realtime": {
"supabase_url": "https://xxxx.supabase.co",
"supabase_anon_key": "eyJhbGciOiJ…",
"channel_prefix": "conv:"
}
}Example
curl https://api.tinychat.dev/v1/config \
-H "x-tinychat-api-key: pk_live_REPLACE_ME"/api/v1/usersUpsert an end-user
Identify the user the SDK is acting on behalf of. Call once per session, or whenever identity changes. Idempotent on (tenant_id, user_id).
Body
user_idstringrequired | Opaque, stable identifier from your system. Used to key all conversation membership. |
namestringoptional | Display name. Empty strings are ignored — re-upserting an anonymous user will not blank out a previously stored name. Pass a real value or omit it. |
emailstringoptional | Email address. Same empty-string rule as name. |
role"customer" | "driver" | "admin" | "support"optional | Defaults to 'customer'. Affects how the inbox routes and labels the user. |
fcm_tokensstring[]optional | Push tokens for fan-out to FCM/APNs. |
notification_prefsobjectoptional | Opaque blob; the SDK reads/writes it. |
Response
{
"user": {
"id": "9d2a…",
"tenant_id": "1111…",
"user_id": "u_42",
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "customer",
"fcm_tokens": [],
"notification_prefs": {},
"last_seen_at": "2026-01-15T10:21:09.114Z"
}
}Example
curl -X POST https://api.tinychat.dev/v1/users \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{
"user_id": "u_42",
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "customer"
}'/api/v1/conversationsList a user's conversations
Returns up to 100 conversations the user participates in, sorted by last activity descending.
Query parameters
user_idstringrequired | The user whose conversations you want. Same opaque id you used with POST /v1/users. |
kind"order" | "support" | "direct"optional | Filter to a single kind. Omit to get every kind. |
Response
{
"conversations": [
{
"id": "c_abc",
"tenant_id": "1111…",
"kind": "support",
"external_ref": null,
"participants": ["u_42", "agent_7"],
"last_message": "Thanks, all sorted",
"last_at": "2026-01-15T10:21:09.114Z"
}
]
}Example
curl "https://api.tinychat.dev/v1/conversations?user_id=u_42&kind=support" \
-H "x-tinychat-api-key: pk_live_REPLACE_ME"/api/v1/conversationsCreate (or fetch) a conversation
Get-or-create. Idempotent on (tenant_id, kind, external_ref) — re-calling with the same external_ref returns the existing row instead of creating a duplicate or overwriting participants.
external_ref twice with different participants, the second call returns the original row — it does not update participants. Manage membership via your own backend if you need to add agents.Body
kind"order" | "support" | "direct"required | Which inbox bucket the conversation lives in. |
external_refstringoptional | Your system's id (e.g. order id). Required for stable idempotency on order/support threads. |
participantsstring[]required | Non-empty array of user_ids — at minimum the customer and the agent/driver. |
Response
{
"conversation": {
"id": "c_abc",
"tenant_id": "1111…",
"kind": "order",
"external_ref": "order_9001",
"participants": ["u_42", "driver_3"],
"last_at": null
}
}Example
curl -X POST https://api.tinychat.dev/v1/conversations \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{
"kind": "order",
"external_ref": "order_9001",
"participants": ["u_42", "driver_3"]
}'/api/v1/conversations/:id/messagesPaginated message history
Returns messages oldest-first in the array. Pass a cursor from the oldest message you already have to load the next page.
Query parameters
beforestring (ISO-8601)optional | Return messages with created_at strictly less than this. Use the oldest message's created_at to paginate backwards. |
limitnumberoptional | Default 50, max 200. |
Response
{
"messages": [
{
"id": "m_1",
"conversation_id": "c_abc",
"sender_id": "u_42",
"body": "Hi, where's my order?",
"message_type": "text",
"media_url": null,
"reply_to": null,
"created_at": "2026-01-15T10:18:01.412Z",
"edited_at": null,
"deleted_at": null
}
]
}Example
curl "https://api.tinychat.dev/v1/conversations/c_abc/messages?limit=50" \
-H "x-tinychat-api-key: pk_live_REPLACE_ME"/api/v1/conversations/:id/messagesSend a message
Inserts the message, broadcasts it on the Supabase Realtime channel conv:<id>, and fires the tenant's outbound webhook fire-and-forget.
Body
sender_idstringrequired | The user_id sending the message. |
bodystringoptional | Required when message_type is 'text'. Plain text — the SDK handles linkification. |
message_type"text" | "image"optional | Defaults to 'text'. |
media_urlstringoptional | Required for non-text messages. Use the URL returned by POST /upload. |
reply_tostring (message id)optional | Threads this message under an earlier one in the same conversation. |
receiver_idstringoptional | Optional explicit recipient — useful for direct conversations. |
Response
{
"message": {
"id": "m_42",
"conversation_id": "c_abc",
"sender_id": "u_42",
"body": "Hi, where is my order?",
"message_type": "text",
"created_at": "2026-01-15T10:21:09.114Z"
}
}Example
curl -X POST https://api.tinychat.dev/v1/conversations/c_abc/messages \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{
"sender_id": "u_42",
"body": "Hi, where is my order?",
"message_type": "text"
}'/api/v1/conversations/:id/messages/:msgIdEdit a message
Updates body and sets edited_at. Only the original sender (matched on sender_id) may edit. The edit is broadcast on the conversation channel so other clients update in place.
Body
sender_idstringrequired | Must match the original sender_id or the request 403s. |
bodystringrequired | New text. Cannot be empty. |
Example
curl -X PATCH https://api.tinychat.dev/v1/conversations/c_abc/messages/m_42 \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{ "sender_id": "u_42", "body": "Actually, where IS my order?" }'/api/v1/conversations/:id/messages/:msgIdSoft-delete a message
Sets deleted_at; the message is hidden from history but is retained for moderation. Only the original sender may delete.
Body
sender_idstringrequired | Must match the original sender_id or the request 403s. |
Example
curl -X DELETE https://api.tinychat.dev/v1/conversations/c_abc/messages/m_42 \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{ "sender_id": "u_42" }'/api/v1/conversations/:id/typingTyping indicator
Fire-and-forget. Broadcasts a typing event on the conversation channel; the receiver clears the indicator ~3 s after the last event. Throttle to at most one event every 2–3 seconds on the client.
Body
sender_idstringrequired | Who's typing. |
sender_namestringoptional | Display name shown in the indicator ("Ada is typing…"). Optional. |
Example
curl -X POST https://api.tinychat.dev/v1/conversations/c_abc/typing \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-H "content-type: application/json" \
-d '{ "sender_id": "u_42", "sender_name": "Ada" }'/api/v1/conversations/:id/uploadUpload an image attachment
Multipart upload to Supabase Storage. Returns a public URL that you then attach to a message via media_url + message_type: 'image'.
Form data
filebinaryrequired | Max 10 MB. Allowed MIME types: image/jpeg, image/png, image/gif, image/webp, image/heic. |
Response
{
"url": "https://xxxx.supabase.co/storage/v1/object/public/chat/…/img.jpg",
"path": "1111…/c_abc/abcd-ef.jpg"
}Example
curl -X POST https://api.tinychat.dev/v1/conversations/c_abc/upload \
-H "x-tinychat-api-key: pk_live_REPLACE_ME" \
-F "file=@./receipt.png"Live message delivery.
Each conversation has its own Supabase Realtime channel named conv:<conversation_id>. When a new message is sent, edited, or deleted, the server broadcasts a payload on that channel; typing indicators ride the same channel under a separate event name.
Events
message— new, edited, or soft-deleted message.typing— sender id + name, fires per keystroke window.
Subscribe (browser / Node)
import { createClient } from "@supabase/supabase-js";
// 1. Bootstrap: pull realtime creds from /v1/config
const cfg = await fetch("https://api.tinychat.dev/v1/config", {
headers: { "x-tinychat-api-key": apiKey },
}).then((r) => r.json());
const supabase = createClient(
cfg.realtime.supabase_url,
cfg.realtime.supabase_anon_key,
);
// 2. Subscribe to a conversation's channel
const channel = supabase
.channel(`${cfg.realtime.channel_prefix}${conversationId}`)
.on("broadcast", { event: "message" }, ({ payload }) => {
console.log("new/edited/deleted message:", payload.message);
})
.on("broadcast", { event: "typing" }, ({ payload }) => {
console.log("typing:", payload.senderId);
})
.subscribe();
// 3. Tear down when the screen unmounts
// channel.unsubscribe();Errors
Predictable failure modes.
Errors are always JSON with a single error string. Treat the status code as authoritative; the message body is for humans.
| Status | Body |
|---|---|
400 | { "error": "invalid json" }Body wasn't parseable JSON. |
400 | { "error": "<field> is required" }Missing a required field — see each endpoint's request schema. |
401 | { "error": "missing x-tinychat-api-key header" }No auth header sent. |
401 | { "error": "invalid api key" }Key not found, malformed, or wrong format. |
403 | { "error": "tenant is overage|suspended" }Tenant is past its plan limit or has been suspended. |
404 | { "error": "conversation not found" }Wrong id, or the conversation belongs to a different tenant. |
413 | { "error": "file too large (>10 MB)" }Upload exceeded the 10 MB ceiling. |
415 | { "error": "unsupported mime: …" }Upload wasn't one of the allowed image types. |
500 | { "error": "<db message>" }Supabase or storage failure — usually transient. |
Next
Prefer drop-in widgets to wiring fetch calls?