Skip to main content

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." }'
kindWhen to use it
textFree-form text, with the window open
mediaA file, with the window open — upload it first to /media
templateWindow closed: an approved template reopens the conversation
noteInternal 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-ID recovers nothing. On every reconnection, read the conversations that matter again.
  • The browser's EventSource does not send the Authorization header. 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.