The inbox through the API
The inbox is where the team answers from the dashboard. The API gives access to the same conversations, to connect a helpdesk, a CRM or a bot: what your integration replies shows up for the team, and what the team replies shows up for your integration.
Reading conversations
curl 'https://api.joinotify.com/inbox/conversations?folder=open&unread=true' \
-H 'Authorization: Bearer sk_live_xxx'
The list is paged by cursor: send the answer's nextCursor as cursor for the next
page, until it comes back null. A conversation's messages come from
GET /inbox/conversations/{id}/messages, newest first, with the cursor in before.
Each conversation says whether the 24-hour window is open (windowOpen) and until when
(windowExpiresAt). That is what decides what you can reply.
Replying
curl -X POST https://api.joinotify.com/inbox/conversations/cm1c0nv000001/messages \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-d '{ "kind": "text", "text": "Hi Ana! Your order is out for delivery." }'
kind | When to use it |
|---|---|
text | Free-form text, with the window open |
media | A file, with the window open — upload it first to /media |
template | Window closed: an approved template reopens the conversation |
note | Internal note for the team; never goes to WhatsApp |
With the window closed, text and media are refused before leaving, with
422 window_closed — unlike POST /messages, which accepts and fails later. Replying
reopens a closed or snoozed conversation and pauses that conversation's automations for a
while, as when a team member takes over.
To send a file, post its bytes to POST /inbox/conversations/{id}/media (the body is the
file, not multipart, with the name in X-File-Name) and reply with
{ "kind": "media", "assetId": "..." }.
Organizing
PATCH /inbox/conversations/{id} closes, reopens, sets pending, snoozes until a date,
assigns to a team member (the ids come from GET /inbox/agents) or pauses automations.
Each change is recorded in the conversation, and closing and assigning fire the
conversation.closed and conversation.assigned events.
An API key is not a team member: the mine folder is always empty, and in the events
assigned_by and closed_by come back null when the key was the one acting.
Real time
GET /inbox/stream is a Server-Sent Events stream. It says what changed — new
message, message status, conversation updated, someone typing — without the content:
event: message.created
id: 12
data: {"type":"message.created","conversationId":"cm1c0nv000001","phoneNumberId":"106540352242922","messageId":"cm1m3ss4g3001","direction":"in"}
When an event arrives, fetch the conversation or the messages. Three things to watch:
- There is no replay. An event missed during an outage does not come back, and
Last-Event-IDrecovers nothing. On every reconnection, read the conversations that matter again. - The browser's
EventSourcedoes not send theAuthorizationheader. Use a client that does — the stream is meant for servers. - Each API key opens at most 5 streams at a time.
If you only need to react to incoming messages, the messages
webhook is still the simplest path.
Key scope
An API key limited to some numbers only sees those numbers' conversations — a
conversation on another number answers 404, as if it did not exist. GET /inbox/agents,
which lists the account's team with their emails, answers 403 for it.