API reference · v1.0.0

Chirp API

The Chirp API reads and changes the conversations in your Chirp workspace and sends mail from your inboxes.

Base URL
https://app.teamchirp.io/v1
Spec
openapi.json · OpenAPI 3.1.0

Quick start

  1. In Chirp, open Settings → API keys and create a key. Chirp shows the secret once; copy it.

  2. Keep it in an environment variable and list your inboxes:

    First request
    export CHIRP_API_KEY=chirp_…
    
    curl https://app.teamchirp.io/v1/inboxes \
      -H "Authorization: Bearer $CHIRP_API_KEY"
  3. Send a reply with a key that has the send scope. The Idempotency-Key makes a retry safe: a retry with the same key returns the first result and does not send again.

    Send a reply
    IDEMPOTENCY_KEY=$(uuidgen)   # make it once; reuse it on a retry
    
    curl -X POST https://app.teamchirp.io/v1/conversations/1042/replies \
      -H "Authorization: Bearer $CHIRP_API_KEY" \
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
      -H "Content-Type: application/json" \
      -d '{"body":"Thanks, Dana. The refund is on its way."}'

Authentication

Create a key in Chirp under Settings → API keys. Chirp shows the secret once; it starts with chirp_. Send it in the Authorization header of every request:

Authorization: Bearer chirp_…

A key acts as the user who created it: it has that user's role and sees only the inboxes that user can see. A conversation the user cannot see answers 404, as if it did not exist. A key stops working when it is revoked or expires, or when its user is disabled or leaves the workspace.

Scopes

A key holds one or more scopes. Each operation names the scope it needs in x-required-scope.

  • read: list inboxes and conversations, and read a conversation.
  • write: change the status or assignee, add and remove tags, and add notes.
  • send: send replies and start conversations.

A call without the scope answers 403 with the code insufficient_scope. A Viewer's key keeps only read, whatever scopes it was made with.

Rate limits

Each key may make a fixed number of requests per minute. Every authenticated response carries X-RateLimit-Limit and X-RateLimit-Remaining. When the limit is used up, the API answers 429 with a Retry-After header in seconds.

Pagination

GET /conversations returns one page and a nextCursor. Pass it as cursor to get the next page; it is null on the last page.

Idempotency

The send operations and adding a note accept an Idempotency-Key header holding a UUID. A retry with the same key returns the first result instead of sending or adding again.

Conflicts

Each conversation has a version that goes up with each change. Send it back as expectedVersion to have a change refused with 409 if someone changed the conversation first.

Errors

Every error has the same body: {"error": {"code": "not_found", "message": "Conversation not found"}}. Branch on code; the message is for people. The codes are listed in the Error schema.

Inboxes

The inboxes the key's user can see.

List inboxes

GET/inboxesscope read

The inboxes the key's user can see, in the order they were added.

Response

200 Inboxes · Inbox

Errors

401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl https://app.teamchirp.io/v1/inboxes \
  -H "Authorization: Bearer $CHIRP_API_KEY"
Response 200
{
  "data": [
    {
      "id": 1,
      "address": "[email protected]",
      "name": "Acme Support",
      "color": "#e9a74a",
      "openCount": 12
    }
  ]
}

Conversations

List, read and triage conversations.

List conversations

GET/conversationsscope read

Conversations in the inboxes the key's user can see, newest activity first, in every status unless you filter.

Filters use the app's search syntax: pass tokens in q (such as status:open tag:"billing issue") or as parameters. Different filters must all match; a repeated status, assignee, inbox or linked matches any of its values, and a repeated tag needs every tag. q and the filters together may hold at most 200 characters.

The result is one page. Pass nextCursor as cursor to get the next one. A conversation that gets new activity during a walk moves to the front, so a walk that started earlier does not return it again.

Parameters

qquery · string
Search text, matched in addresses, subjects, bodies and notes; may hold filter tokens.
statusquery · array of string
open, pending, snoozed or closed.
assigneequery · array of string
me, none or a teammate's display name.
tagquery · array of string
A tag name; every tag given must be on the conversation.
inboxquery · array of string
An inbox id, name, address or local part.
afterquery · string
Latest activity on or after this day, in the user's time zone: YYYY-MM-DD, today, a weekday such as mon, lastweek, 1d–365d days back, or a month day such as sep15.
beforequery · string
Latest activity before this day, in the same forms as after.
linkedquery · array of string
open, done, any, none or a Linear issue identifier.
isquery · string
attention: the Needs attention list (open and waiting on you, or with a delivery problem).
limitquery · integer
Page size, 1–100. The default is 50.
cursorquery · string
The nextCursor of the previous page.

Response

200 A page of conversations · ConversationSummary

Errors

400invalid_request
A filter, limit or cursor is not valid, or q and the filters exceed 200 characters.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl https://app.teamchirp.io/v1/conversations \
  -H "Authorization: Bearer $CHIRP_API_KEY"
Response 200
{
  "data": [
    {
      "id": "1042",
      "inboxId": 1,
      "subject": "Refund for order 5521",
      "sender": "[email protected]",
      "preview": "Hi, I was charged twice for order 5521.",
      "latestAt": "2026-09-29T15:04:05.000Z",
      "messageCount": 2,
      "status": "open",
      "assignee": {
        "id": "7",
        "displayName": "Sam Rivera"
      },
      "tags": [
        {
          "id": "12",
          "name": "billing"
        }
      ],
      "version": 4
    }
  ],
  "nextCursor": "1790694245000000.1042"
}

Get a conversation

GET/conversations/{id}scope read

A conversation with its messages, its timeline of notes and changes, and its linked Linear issues.

Parameters

idpath · stringrequired
The conversation's id.

Response

200 The conversation · Conversation

Errors

401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl https://app.teamchirp.io/v1/conversations/1042 \
  -H "Authorization: Bearer $CHIRP_API_KEY"
Response 200
{
  "id": "1042",
  "inbox": {
    "id": 1,
    "address": "[email protected]"
  },
  "status": "open",
  "assignee": {
    "id": "7",
    "displayName": "Sam Rivera"
  },
  "tags": [
    {
      "id": "12",
      "name": "billing"
    }
  ],
  "version": 4,
  "subject": "Refund for order 5521",
  "messages": [
    {
      "id": "88210",
      "direction": "incoming",
      "from": "[email protected]",
      "to": "[email protected]",
      "subject": "Refund for order 5521",
      "body": "Hi, I was charged twice for order 5521.",
      "createdAt": "2026-09-29T14:58:12.000Z",
      "author": null,
      "attachments": [],
      "state": null
    },
    {
      "id": "88211",
      "direction": "sent",
      "from": "[email protected]",
      "to": "[email protected]",
      "subject": "Re: Refund for order 5521",
      "body": "Sorry about that, Dana. We have refunded the second charge.",
      "createdAt": "2026-09-29T15:04:05.000Z",
      "author": "7",
      "attachments": [],
      "state": "sent"
    }
  ],
  "activity": [
    {
      "id": "9000",
      "kind": "assignment",
      "body": null,
      "detail": {
        "from": null,
        "to": "7"
      },
      "actor": {
        "id": "7",
        "displayName": "Sam Rivera"
      },
      "createdAt": "2026-09-29T15:00:30.000Z",
      "parentNoteId": null
    },
    {
      "id": "9001",
      "kind": "note",
      "body": "Refund issued in Stripe; waiting for the bank.",
      "detail": {},
      "actor": {
        "id": "7",
        "displayName": "Sam Rivera"
      },
      "createdAt": "2026-09-29T15:10:00.000Z",
      "parentNoteId": null
    }
  ],
  "issueLinks": [
    {
      "identifier": "ENG-42",
      "title": "Duplicate charge on retry",
      "url": "https://linear.app/example/issue/ENG-42",
      "state": "In Progress",
      "stateType": "started"
    }
  ]
}

Change status or assignee

PATCH/conversations/{id}scope write

Change the status or the assignee: one of the two per call. To snooze, send status snoozed with snoozedUntil: a future time, or null to wait for the customer's reply. The change shows in the timeline as the key's user.

Parameters

idpath · stringrequired
The conversation's id.

Request body

statusstring
The new status. One of open, pending, snoozed, closed.
assigneeIdstring | null
The teammate to assign, or null to unassign. The teammate must be able to see the inbox.
snoozedUntilstring (date-time) | null
Required with status snoozed: when to wake it, or null to wait for the customer's reply.
expectedVersioninteger
Refuse the change with 409 if the conversation's version is no longer this.

Response

200 The conversation's workflow state · Workflow

Errors

400invalid_request
Not exactly one change, an unknown field, a bad status or time, or an assignee who cannot see the inbox.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox.
409conflict
expectedVersion no longer matches: someone changed the conversation first. Read it again and retry.
413payload_too_large
The JSON body exceeds 64 KB.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X PATCH https://app.teamchirp.io/v1/conversations/1042 \
  -H "Authorization: Bearer $CHIRP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"pending","expectedVersion":4}'
Response 200
{
  "id": "1042",
  "inbox": {
    "id": 1,
    "address": "[email protected]"
  },
  "status": "pending",
  "assignee": {
    "id": "7",
    "displayName": "Sam Rivera"
  },
  "tags": [
    {
      "id": "12",
      "name": "billing"
    }
  ],
  "version": 5
}

Tags

Add and remove tags on a conversation.

Add a tag

POST/conversations/{id}/tagsscope write

Add a tag by name. A new name creates the tag and adds it to the inbox's tag list. Adding a tag the conversation already has changes nothing.

Parameters

idpath · stringrequired
The conversation's id.

Request body

namestringrequired
The tag name. It is stored in lower case, with hyphens for spaces.
expectedVersioninteger
Refuse the change with 409 if the conversation's version is no longer this.

Response

200 The conversation's workflow state · Workflow

Errors

400invalid_request
The name is empty or longer than 32 characters, or a field is not valid.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox.
409conflict
expectedVersion no longer matches: someone changed the conversation first. Read it again and retry.
413payload_too_large
The JSON body exceeds 64 KB.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X POST https://app.teamchirp.io/v1/conversations/1042/tags \
  -H "Authorization: Bearer $CHIRP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"refund"}'
Response 200
{
  "id": "1042",
  "inbox": {
    "id": 1,
    "address": "[email protected]"
  },
  "status": "open",
  "assignee": {
    "id": "7",
    "displayName": "Sam Rivera"
  },
  "tags": [
    {
      "id": "12",
      "name": "billing"
    },
    {
      "id": "15",
      "name": "refund"
    }
  ],
  "version": 5
}

Remove a tag

DELETE/conversations/{id}/tags/{tagId}scope write

Remove a tag by id. Removing a tag the conversation does not have changes nothing.

Parameters

idpath · stringrequired
The conversation's id.
tagIdpath · stringrequired
The tag's id.
expectedVersionquery · integer
Refuse with 409 if the conversation's version is no longer this.

Response

200 The conversation's workflow state · Workflow

Errors

400invalid_request
expectedVersion is not a whole number.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox.
409conflict
expectedVersion no longer matches: someone changed the conversation first. Read it again and retry.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X DELETE https://app.teamchirp.io/v1/conversations/1042/tags/12 \
  -H "Authorization: Bearer $CHIRP_API_KEY"
Response 200
{
  "id": "1042",
  "inbox": {
    "id": 1,
    "address": "[email protected]"
  },
  "status": "open",
  "assignee": {
    "id": "7",
    "displayName": "Sam Rivera"
  },
  "tags": [],
  "version": 5
}

Notes

Internal notes that customers never see.

Add a note

POST/conversations/{id}/notesscope write

Add an internal note, or a reply to a note. Customers never see notes. An @mention of a teammate notifies them as in the app.

Parameters

idpath · stringrequired
The conversation's id.
Idempotency-Keyheader · string (uuid)
A UUID. A retry with the same key returns the first result instead of doing the work again.

Request body

bodystringrequired
The note's text, 1–5000 characters.
parentNoteIdstring | null
To reply to a note, that note's id. Replies go one level deep.

Response

201 The note · Activity

Errors

400invalid_request
The body is empty or too long, parentNoteId is not an id, or Idempotency-Key is not a UUID.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox. Also sent when parentNoteId is not a top-level note on this conversation.
409conflict
The Idempotency-Key was already used for a different note on this conversation.
413payload_too_large
The JSON body exceeds 64 KB.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X POST https://app.teamchirp.io/v1/conversations/1042/notes \
  -H "Authorization: Bearer $CHIRP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"Refund issued in Stripe; waiting for the bank."}'
Response 201
{
  "id": "9001",
  "kind": "note",
  "body": "Refund issued in Stripe; waiting for the bank.",
  "detail": {},
  "actor": {
    "id": "7",
    "displayName": "Sam Rivera"
  },
  "createdAt": "2026-09-29T15:10:00.000Z",
  "parentNoteId": null
}

Sending

Send mail from an inbox, at once.

Start a conversation

POST/conversationsscope send

Start a new conversation with one message, sent now from the inbox with its From name and signature. The app's undo window does not apply. Send an Idempotency-Key so a retry cannot send twice.

A 201 means Chirp recorded the message, not that it was delivered. Check state: only sent means the mail provider accepted it. On failed the provider refused it; on unknown, read the conversation before you send again.

Parameters

Idempotency-Keyheader · string (uuid)
A UUID. A retry with the same key returns the first result instead of doing the work again.

Request body

inboxIdintegerrequired
The inbox to send from. The key's user must be able to see it.
tostring (email)required
One recipient address.
subjectstringrequired
The subject, 1–200 characters.
bodystringrequired
The plain text, 1–10000 characters. The inbox signature is added.

Response

201 The sent message · SendResult

Errors

400invalid_request
A field is missing, unknown or not valid, or Idempotency-Key is not a UUID.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such inbox, or the key's user cannot see it.
409conflict
The inbox cannot send mail right now; its connection needs attention in Chirp.
413payload_too_large
The JSON body exceeds 64 KB.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X POST https://app.teamchirp.io/v1/conversations \
  -H "Authorization: Bearer $CHIRP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inboxId":1,"to":"[email protected]","subject":"Your refund for order 5521","body":"Hi Dana, we have refunded the second charge."}'
Response 201
{
  "conversationId": "1043",
  "messageId": "88214",
  "state": "sent"
}

Send a reply

POST/conversations/{id}/repliesscope send

Send a reply to the customer now, from the conversation's inbox with its From name and signature. The app's undo window does not apply. Send an Idempotency-Key so a retry cannot send twice.

A 201 means Chirp recorded the message, not that it was delivered. Check state: only sent means the mail provider accepted it. On failed the provider refused it; on unknown, read the conversation before you send again.

Parameters

idpath · stringrequired
The conversation's id.
Idempotency-Keyheader · string (uuid)
A UUID. A retry with the same key returns the first result instead of doing the work again.

Request body

bodystringrequired
The reply's plain text, 1–10000 characters. The inbox signature is added.

Response

201 The sent message · SendResult

Errors

400invalid_request
The body is empty or too long, a field is unknown, or Idempotency-Key is not a UUID.
401unauthorized
The API key is missing, invalid, revoked or expired.
403insufficient_scope
The key lacks the scope this operation needs.
404not_found
No such conversation, or the key's user cannot see its inbox.
413payload_too_large
The JSON body exceeds 64 KB. Also sent when the finished message exceeds 10 MB.
429rate_limited
The key used up its requests for this minute.
500internal_error
The server failed to complete the request.
Request
curl -X POST https://app.teamchirp.io/v1/conversations/1042/replies \
  -H "Authorization: Bearer $CHIRP_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"Sorry about that, Dana. We have refunded the second charge."}'
Response 201
{
  "conversationId": "1042",
  "messageId": "88213",
  "state": "sent"
}

Schemas

The shared models the endpoints return.

Error

The body of every error response.

errorobjectrequired
What went wrong.
error.codestringrequired
A machine-readable code. Branch on this, not on the message. One of invalid_request, unauthorized, forbidden, not_found, conflict, payload_too_large, rate_limited, upstream_error, insufficient_scope, internal_error.
error.messagestringrequired
A sentence for people. The wording can change.

Inbox

An inbox: one connected email address.

idintegerrequired
The inbox's id.
addressstring (email)required
The inbox's email address.
namestring | nullrequired
The inbox's display name, or null when it has none.
colorstringrequired
The inbox's colour in Chirp, as a CSS hex colour.
openCountintegerrequired
How many of its conversations are open.

ConversationSummary

A conversation as it appears in a list.

idstringrequired
The conversation's id.
inboxIdintegerrequired
The id of the conversation's inbox.
subjectstringrequired
The subject of its first message.
senderstringrequired
The customer's address: the latest incoming sender, or the recipient when no message came in.
previewstringrequired
The first line of the latest message.
latestAtstring (date-time)required
When the latest message arrived or was sent. The list is ordered by this, newest first.
messageCountintegerrequired
How many messages the conversation holds.
statusstringrequired
The conversation's status. One of open, pending, snoozed, closed.
assigneeobject | nullrequired
The assigned teammate, or null when unassigned.
tagsarray of objectrequired
Its tags, by name.
tags[].idstringrequired
The tag's id.
tags[].namestringrequired
The tag name, 1–32 characters.
versionintegerrequired
Goes up by one with each change. Send it back as expectedVersion to refuse a change made on stale data.

Workflow

A conversation's workflow state: what a write call changes.

idstringrequired
The conversation's id.
inboxobjectrequired
The conversation's inbox.
inbox.idintegerrequired
The inbox's id.
inbox.addressstring (email)required
The inbox's email address.
statusstringrequired
The conversation's status. One of open, pending, snoozed, closed.
assigneeobject | nullrequired
The assigned teammate, or null when unassigned.
tagsarray of objectrequired
Its tags, by name.
tags[].idstringrequired
The tag's id.
tags[].namestringrequired
The tag name, 1–32 characters.
versionintegerrequired
Goes up by one with each change. Send it back as expectedVersion to refuse a change made on stale data.

Message

One email in a conversation.

idstringrequired
The message's id.
directionstringrequired
incoming from the customer, or sent from the inbox. One of incoming, sent.
fromstringrequired
The From address.
tostringrequired
The To address.
subjectstringrequired
The subject.
bodystringrequired
The plain-text body.
createdAtstring (date-time)required
When it arrived or was sent.
authorstring | nullrequired
The id of the teammate who sent it, or null for an incoming message.
attachmentsarray of objectrequired
Its attachments. The API does not serve their content.
attachments[].filenamestringrequired
The file name.
attachments[].contentTypestringrequired
The MIME type.
attachments[].sizeintegerrequired
The size in bytes.
statestring | nullrequired
For a sent message, its outbox state: held (in the app's undo window), sending, sent, failed or unknown. Null for an incoming message. One of held, sending, sent, failed, unknown.

Activity

One entry in a conversation's timeline: a note or a change.

idstringrequired
The entry's id.
kindstringrequired
What happened. A note carries its text in body; a change carries its data in detail. One of note, status, assignment, tag_add, tag_remove, reopened, issue_link, issue_unlink, issue_state, automation.
bodystring | nullrequired
A note's text; null for other kinds.
detailobjectrequired
The change's data, which depends on kind (for example from and to for status). Empty for a note.
actorobject | nullrequired
Who made it, or null for the system (an automation or an incoming message).
createdAtstring (date-time)required
When it happened.
parentNoteIdstring | nullrequired
For a reply to a note, the note's id; otherwise null.

Conversation

A conversation with its messages, timeline and linked issues.

idstringrequired
The conversation's id.
inboxobjectrequired
The conversation's inbox.
inbox.idintegerrequired
The inbox's id.
inbox.addressstring (email)required
The inbox's email address.
statusstringrequired
The conversation's status. One of open, pending, snoozed, closed.
assigneeobject | nullrequired
The assigned teammate, or null when unassigned.
tagsarray of objectrequired
Its tags, by name.
tags[].idstringrequired
The tag's id.
tags[].namestringrequired
The tag name, 1–32 characters.
versionintegerrequired
Goes up by one with each change. Send it back as expectedVersion to refuse a change made on stale data.
subjectstringrequired
The subject of its first message.
messagesarray of Messagerequired
Its messages, oldest first.
activityarray of Activityrequired
Its notes and changes, oldest first.
issueLinksarray of IssueLinkrequired
Its linked Linear issues, oldest link first.

SendResult

The outcome of a send. The response is 201 whenever Chirp recorded the message, even when delivery failed: check state.

conversationIdstringrequired
The conversation's id.
messageIdstringrequired
The sent message's id.
statestringrequired
sent: the mail provider accepted the message. failed: the provider refused it. unknown: the outcome is not known, for example after a timeout; check the conversation before you send again. sending: a retry with the same Idempotency-Key arrived while the first send was still running. One of sending, sent, failed, unknown.