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 host
Authentication

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

Example key

pk_live_xxxx••••••••xxxx

Issue a real key in the dashboard.

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"
GET/api/v1/config

Bootstrap 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-key
stringrequired
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"
POST/api/v1/users

Upsert 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_id
stringrequired
Opaque, stable identifier from your system. Used to key all conversation membership.
name
stringoptional
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.
email
stringoptional
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_tokens
string[]optional
Push tokens for fan-out to FCM/APNs.
notification_prefs
objectoptional
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"
  }'
GET/api/v1/conversations

List a user's conversations

Returns up to 100 conversations the user participates in, sorted by last activity descending.

Query parameters

user_id
stringrequired
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"
POST/api/v1/conversations

Create (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.

Gotcha: if you call this for the same 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_ref
stringoptional
Your system's id (e.g. order id). Required for stable idempotency on order/support threads.
participants
string[]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"]
  }'
GET/api/v1/conversations/:id/messages

Paginated 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

before
string (ISO-8601)optional
Return messages with created_at strictly less than this. Use the oldest message's created_at to paginate backwards.
limit
numberoptional
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"
POST/api/v1/conversations/:id/messages

Send 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_id
stringrequired
The user_id sending the message.
body
stringoptional
Required when message_type is 'text'. Plain text — the SDK handles linkification.
message_type
"text" | "image"optional
Defaults to 'text'.
media_url
stringoptional
Required for non-text messages. Use the URL returned by POST /upload.
reply_to
string (message id)optional
Threads this message under an earlier one in the same conversation.
receiver_id
stringoptional
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"
  }'
PATCH/api/v1/conversations/:id/messages/:msgId

Edit 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_id
stringrequired
Must match the original sender_id or the request 403s.
body
stringrequired
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?" }'
DELETE/api/v1/conversations/:id/messages/:msgId

Soft-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_id
stringrequired
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" }'
POST/api/v1/conversations/:id/typing

Typing 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_id
stringrequired
Who's typing.
sender_name
stringoptional
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" }'
POST/api/v1/conversations/:id/upload

Upload 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

file
binaryrequired
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"
Realtime

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.

Channel name

conv:<conversation_id>

Prefix is returned as channel_prefix by GET /v1/config.

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.

StatusBody
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?

GETPOSTPATCHDELETE— every method this API uses.