Developer docsAPI reference
API reference

The Rookery API

Read and work on your inbox from your own code. Every endpoint is a JSON call, made with an API key, that acts as its own teammate.

Every endpoint is under https://app.userookery.com/api/v1. Send your key in the Authorization header. Make one in Settings → Integrations → API keys, and see Keys and scopes for what it can see and do.

Try it runs real requests with a key you paste. Make one with only Read to look around safely. Replies are tried as drafts and never sent.

Your first call
curl
curl https://app.userookery.com/api/v1/me \
  -H "Authorization: Bearer $ROOKERY_KEY"
Response200
{
  "agent": {
    "id": "agt_3n7q2wv9k4hxm8tr",
    "name": "Support bot"
  },
  "scopes": [
    "read",
    "draft"
  ]
}

Your key

Check which agent a key acts as, and what it may do.

Check your key

GET/v1/me

Needs readSafe to retry

Returns the agent this key acts as and the scopes it has. A quick way to check a key works.

Returns

200 An object.

Fields
  • agentobjectRequired
    Fields
    • idstringRequired

      The agent's id, like agt_….

    • namestringRequired

      The key's name.

  • scopesarray of stringsRequired

Errors

  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 429More than 120 requests in a minute with this key. Wait, then try again.

REST only.

GET/v1/me
curl
curl 'https://app.userookery.com/api/v1/me' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/me",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
)
data = response.json()
Response200
{
  "agent": {
    "id": "agt_3n7q2wv9k4hxm8tr",
    "name": "Support bot"
  },
  "scopes": [
    "read",
    "draft"
  ]
}

Conversations

Find, read and work on conversations: notes, status, tags and assignment.

List conversations

GET/v1/conversations

Needs readSafe to retry

Lists conversations, newest first. Use view for open, waiting, snoozed or closed; query searches all mail text.

You only see what the person who made the key can see: every team inbox, plus their own private inboxes.

Results are newest first, up to limit (at most 100). There's no cursor yet: narrow the list with view, mailbox or q.

Query parameters

  • viewstring

    Which conversations to list. waiting means waiting on the customer.

    One of open, waiting, snoozed, closed · Default open

  • mailboxstring

    A mailbox id from list_mailboxes.

  • qstring

    Full-text search.

    Up to 200 characters

  • assignedstring

    me means assigned to this agent.

    One of me, unassigned

  • limitinteger

    How many to return, newest first.

    Default 25 · 1 to 100

Returns

200 An array of objects, newest first.

Fields in each item
  • idstringRequired

    The conversation's id, like cnv_….

  • subjectstringRequired
  • snippetstringRequired

    The start of the latest message.

  • statusstringRequired

    One of open, waiting, closed

  • contactobjectRequired

    The customer the conversation is with.

    Fields
    • namestringRequired

      Can be null

    • emailstringRequired

      Can be null

  • mailboxstringRequired

    The inbox's name.

  • lastMessageAttimestampRequired

    ISO 8601, UTC.

  • messageCountintegerRequired
  • assigneeobjectRequired

    Who the conversation is assigned to, or null.

    Can be null

    Fields
    • idstringRequired

      A usr_… or agt_… id.

    • namestringRequired
    • typestringRequired

      One of user, agent

  • tagsarray of stringsRequired
  • topicstringRequired

    The topic Scout sorted it under, or null.

    Can be null

  • urgencystringRequired

    Set by Scout when it sorts new mail.

    One of low, normal, high · Can be null

  • riskstringRequired

    Scout's warning, like phishing, or null when nothing looks wrong.

    Can be null

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: list_conversations. Connect with MCP

GET/v1/conversations
curl
curl 'https://app.userookery.com/api/v1/conversations?view=open&limit=1' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations?view=open&limit=1", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/conversations",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    params={
        "view": "open",
        "limit": 1,
    },
)
data = response.json()
Response200
[
  {
    "id": "cnv_8k2m4q7rx9tbw3hd",
    "subject": "Where is my order?",
    "snippet": "Order 4502 hasn't arrived and the tracking hasn't moved since Monday.",
    "status": "open",
    "contact": {
      "name": "Lee Park",
      "email": "lee@example.com"
    },
    "mailbox": "Support",
    "lastMessageAt": "2026-10-09T14:32:05.000Z",
    "messageCount": 2,
    "assignee": null,
    "tags": [
      "shipping"
    ],
    "topic": "order status",
    "urgency": "normal",
    "risk": null
  }
]

Read a conversation

GET/v1/conversations/{id}

Needs readSafe to retry

Reads a conversation: messages (quoted history removed), internal notes, events, tags and who's on the team.

The timeline is oldest first and mixes three kinds of item: message, note and event. Message text is plain text, with quoted history removed and anything past 8,000 characters cut.

team lists the people and agents you can assign it to, with the ids assign_conversation takes.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Returns

200 An object.

Fields
  • idstringRequired

    The conversation's id, like cnv_….

  • subjectstringRequired
  • statusstringRequired

    One of open, waiting, closed

  • mailboxstringRequired

    The inbox's address.

  • contactobjectRequired

    The customer the conversation is with.

    Fields
    • namestringRequired

      Can be null

    • emailstringRequired

      Can be null

  • assigneeobjectRequired

    Who the conversation is assigned to, or null.

    Can be null

    Fields
    • idstringRequired

      A usr_… or agt_… id.

    • namestringRequired
    • typestringRequired

      One of user, agent

  • tagsarray of stringsRequired
  • topicstringRequired

    The topic Scout sorted it under, or null.

    Can be null

  • urgencystringRequired

    Set by Scout when it sorts new mail.

    One of low, normal, high · Can be null

  • riskstringRequired

    Scout's warning, like phishing, or null when nothing looks wrong.

    Can be null

  • riskReasonsarray of stringsRequired

    Up to three plain reasons for risk.

  • snoozedUntiltimestampRequired

    Can be null

  • timelinearray of objectsRequired
    When type is message
    • type"message"Required
    • idstringRequired

      The message's id, like msg_….

    • attimestampRequired

      ISO 8601, UTC.

    • directionstringRequired

      One of inbound, outbound

    • fromobjectRequired
      Fields
      • namestringRequired

        Can be null

      • emailstringRequired
    • statusstringRequired

      For replies: where it stands. Null for mail that came in.

      One of draft, scheduled, sending, sent, failed · Can be null

    • textstringRequired
    • attachmentsarray of objectsRequired
      Fields in each item
      • filenamestringRequired
      • contentTypestringRequired
      • sizeintegerRequired
    When type is note
    • type"note"Required
    • idstringRequired
    • attimestampRequired

      ISO 8601, UTC.

    • authorstringRequired
    • bodystringRequired
    When type is event
    • type"event"Required
    • attimestampRequired

      ISO 8601, UTC.

    • actorstringRequired
    • eventstringRequired

      What happened, like assigned or status.

    • dataobjectRequired
  • teamobjectRequired
    Fields
    • peoplearray of objectsRequired
      Fields in each item
      • idstringRequired

        user:usr_…

      • namestringRequired
    • agentsarray of objectsRequired
      Fields in each item
      • idstringRequired

        agent:agt_…

      • namestringRequired

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: get_conversation. Connect with MCP

GET/v1/conversations/{id}
curl
curl 'https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
)
data = response.json()
Response200
{
  "id": "cnv_8k2m4q7rx9tbw3hd",
  "subject": "Where is my order?",
  "status": "open",
  "mailbox": "help@yourshop.com",
  "contact": {
    "name": "Lee Park",
    "email": "lee@example.com"
  },
  "assignee": null,
  "tags": [
    "shipping"
  ],
  "topic": "order status",
  "urgency": "normal",
  "risk": null,
  "riskReasons": [],
  "snoozedUntil": null,
  "timeline": [
    {
      "type": "message",
      "id": "msg_5d9w2k7hq3mx8rtn",
      "at": "2026-10-09T14:32:05.000Z",
      "direction": "inbound",
      "from": {
        "name": "Lee Park",
        "email": "lee@example.com"
      },
      "status": null,
      "text": "Order 4502 hasn't arrived and the tracking hasn't moved since Monday.",
      "attachments": []
    },
    {
      "type": "note",
      "id": "not_2h8q4w7m9kx3drtb",
      "at": "2026-10-09T14:40:11.000Z",
      "author": "Jess",
      "body": "Carrier says it's at the depot. @Sam can you check?"
    }
  ],
  "team": {
    "people": [
      {
        "id": "user:usr_4k8m2q9wx7hd3rtn",
        "name": "Jess"
      }
    ],
    "agents": [
      {
        "id": "agent:agt_3n7q2wv9k4hxm8tr",
        "name": "Support bot"
      }
    ]
  }
}

Add an internal note

POST/v1/conversations/{id}/notes

Needs write

Adds an internal note the team sees and the customer never does. Mention people with @FirstName.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • bodystringRequired

    The note, as plain text.

    Up to 10,000 characters

Returns

201 An object.

Fields
  • oktrueRequired

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the write scope.
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: add_note. Connect with MCP

POST/v1/conversations/{id}/notes
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/notes \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Tracking shows it'\''s at the depot. @Jess can you call the carrier?"
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/notes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "body": "Tracking shows it's at the depot. @Jess can you call the carrier?"
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/notes",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "body": "Tracking shows it's at the depot. @Jess can you call the carrier?",
    },
)
data = response.json()
Response201
{
  "ok": true
}

Change status

POST/v1/conversations/{id}/status

Needs writeSafe to retry

Sets a conversation to open, waiting (on the customer) or closed.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • statusstringRequired

    The new status. waiting means waiting on the customer.

    One of open, waiting, closed

Returns

200 An object.

Fields
  • oktrueRequired

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the write scope.
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: set_status. Connect with MCP

POST/v1/conversations/{id}/status
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/status \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "waiting"
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/status", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "status": "waiting"
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/status",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "status": "waiting",
    },
)
data = response.json()
Response200
{
  "ok": true
}

Tag or untag

POST/v1/conversations/{id}/tags

Needs writeSafe to retry

Adds tags to a conversation, or removes them. New tag names are created.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • addarray of strings

    Tag names to add.

    Up to 10 items

  • removearray of strings

    Tag names to remove.

    Up to 10 items

Returns

200 An object.

Fields
  • oktrueRequired

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the write scope.
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: tag_conversation. Connect with MCP

POST/v1/conversations/{id}/tags
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/tags \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "add": [
    "vip"
  ],
  "remove": [
    "new"
  ]
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/tags", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "add": [
      "vip"
    ],
    "remove": [
      "new"
    ]
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/tags",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "add": [
            "vip",
        ],
        "remove": [
            "new",
        ],
    },
)
data = response.json()
Response200
{
  "ok": true
}

Assign

POST/v1/conversations/{id}/assign

Needs writeSafe to retry

Assigns a conversation to a person or agent (ids like user:… or agent:… from get_conversation's team), to yourself with me, or unassigns with none.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • tostringRequired

    user:usr_…, agent:agt_…, me or none.

    Up to 80 characters

Returns

200 An object.

Fields
  • oktrueRequired

Errors

  • 400Something in the request is missing or not allowed. The message says which field. Or: The person or agent isn't on the team, or can't see this inbox.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the write scope.
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: assign_conversation. Connect with MCP

POST/v1/conversations/{id}/assign
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/assign \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "user:usr_4k8m2q9wx7hd3rtn"
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/assign", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "to": "user:usr_4k8m2q9wx7hd3rtn"
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/assign",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "to": "user:usr_4k8m2q9wx7hd3rtn",
    },
)
data = response.json()
Response200
{
  "ok": true
}

Replies

Write to the customer. Replies are drafts for a person to approve unless the key may send.

Reply to the customer

POST/v1/conversations/{id}/replies

Needs draftsend to send

Writes a reply to the conversation's contact. With send: false (the default) it's a draft a person approves under Needs your yes. With send: true it goes out after a 10-minute undo window; that needs the send scope. To ask for documents, list them in documents: a private upload link and checklist go under the reply, and reminders follow. Don't write the link yourself.

A draft lands in Needs your yes, next to Quill's, with your summary on top. Nothing reaches the customer until a person approves it.

With send: true the reply is scheduled and leaves after a 10-minute undo window, during which anyone on the team can stop it.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • bodystringRequired

    The reply, as plain text.

    Up to 50,000 characters

  • sendboolean

    False (the default) makes a draft for a person to approve. True sends it, and needs the send scope.

    Default false

  • summarystring

    One line for the person approving: what this reply does.

    Up to 500 characters

  • documentsarray of objects

    Documents to ask the customer to upload.

    Up to 20 items

    Fields in each item
    • labelstringRequired

      Short name, e.g. 'Proof of purchase'.

      Up to 120 characters

    • descriptionstring

      What counts, in a sentence. Optional.

      Up to 500 characters

    • requiredboolean

      False marks it as nice to have.

      Default true

Returns

201 An object.

Fields
  • messageIdstringRequired

    The message's id, like msg_….

  • statusstringRequired

    draft waits for a person's yes; scheduled goes out on its own.

    One of draft, scheduled

  • sendsAttimestamp

    When a sent reply leaves, after the 10-minute undo window.

  • requestIdstring

    The document request's id, like req_….

  • notestring

    What happens next, in a sentence.

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the draft scope (or send, to send).
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: reply. Connect with MCP

POST/v1/conversations/{id}/replies
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/replies \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Hi Lee, thanks for your patience. Your parcel is at the local depot and should reach you tomorrow.",
  "send": false,
  "summary": "Tells Lee the parcel arrives tomorrow"
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/replies", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "body": "Hi Lee, thanks for your patience. Your parcel is at the local depot and should reach you tomorrow.",
    "send": false,
    "summary": "Tells Lee the parcel arrives tomorrow"
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/replies",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "body": "Hi Lee, thanks for your patience. Your parcel is at the local depot and should reach you tomorrow.",
        "send": False,
        "summary": "Tells Lee the parcel arrives tomorrow",
    },
)
data = response.json()
Response201
{
  "messageId": "msg_7r3k9w2hq8mx4dtn",
  "status": "draft",
  "note": "Waiting for a person to approve it."
}

Ask the customer for documents

POST/v1/conversations/{id}/document-requests

Needs draftsend to send

Asks the conversation's contact to upload documents through a private page with a checklist; reminders follow on days 2, 4 and 7, and each upload is checked. A draft for approval by default; with send: true (needs the send scope) it goes out after a 10-minute undo window.

Path

  • idstringRequired

    The conversation's id, like cnv_….

Body

  • documentsarray of objectsRequired

    What to ask for: up to 20 items.

    Up to 20 items

    Fields in each item
    • labelstringRequired

      Short name, e.g. 'Proof of purchase'.

      Up to 120 characters

    • descriptionstring

      What counts, in a sentence. Optional.

      Up to 500 characters

    • requiredboolean

      False marks it as nice to have.

      Default true

  • messagestring

    A short note above the checklist. Greet the customer.

    Up to 5,000 characters

  • dueDatestring

    YYYY-MM-DD

    Matches ^\d{4}-\d{2}-\d{2}$

  • sendboolean

    False (the default) makes a draft for a person to approve. True sends it, and needs the send scope.

    Default false

Returns

201 An object.

Fields
  • messageIdstringRequired

    The message's id, like msg_….

  • statusstringRequired

    draft waits for a person's yes; scheduled goes out on its own.

    One of draft, scheduled

  • sendsAttimestamp

    When a sent reply leaves, after the 10-minute undo window.

  • requestIdstring

    The document request's id, like req_….

  • notestring

    What happens next, in a sentence.

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the draft scope (or send, to send).
  • 404There's no such conversation, or the key can't see it (someone's private inbox).
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: request_documents. Connect with MCP

POST/v1/conversations/{id}/document-requests
curl
curl -X POST https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/document-requests \
  -H "Authorization: Bearer $ROOKERY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "documents": [
    {
      "label": "Proof of purchase"
    },
    {
      "label": "Photo of the damage",
      "required": false
    }
  ],
  "message": "Hi Lee, could you send these so we can sort out the refund?",
  "dueDate": "2026-11-01",
  "send": false
}'
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/document-requests", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROOKERY_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "documents": [
      {
        "label": "Proof of purchase"
      },
      {
        "label": "Photo of the damage",
        "required": false
      }
    ],
    "message": "Hi Lee, could you send these so we can sort out the refund?",
    "dueDate": "2026-11-01",
    "send": false
  }),
});
const data = await response.json();
Python
import os
import requests

response = requests.post(
    "https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd/document-requests",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    json={
        "documents": [
            {
                "label": "Proof of purchase",
            },
            {
                "label": "Photo of the damage",
                "required": False,
            },
        ],
        "message": "Hi Lee, could you send these so we can sort out the refund?",
        "dueDate": "2026-11-01",
        "send": False,
    },
)
data = response.json()
Response201
{
  "messageId": "msg_9q2w7k4hx3mr8dtb",
  "status": "draft",
  "note": "Waiting for a person to approve it. The upload link is added when it's approved."
}

Workspace

The inboxes and tags in this workspace.

List mailboxes

GET/v1/mailboxes

Needs readSafe to retry

Lists the inboxes, with how many conversations are open in each.

Returns

200 An array of objects, newest first.

Fields in each item
  • idstringRequired

    The mailbox's id, like mbx_….

  • addressstringRequired
  • namestringRequired
  • openintegerRequired

    Open conversations.

Errors

  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: list_mailboxes. Connect with MCP

GET/v1/mailboxes
curl
curl 'https://app.userookery.com/api/v1/mailboxes' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/mailboxes", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/mailboxes",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
)
data = response.json()
Response200
[
  {
    "id": "mbx_6t2k9q4wm8xh3drn",
    "address": "help@yourshop.com",
    "name": "Support",
    "open": 12
  }
]

List tags

GET/v1/tags

Needs readSafe to retry

Lists the tags in use and how many conversations have each.

Returns

200 An array of objects, newest first.

Fields in each item
  • idstringRequired

    The tag's id, like tag_….

  • namestringRequired
  • countintegerRequired

Errors

  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: list_tags. Connect with MCP

GET/v1/tags
curl
curl 'https://app.userookery.com/api/v1/tags' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/tags", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/tags",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
)
data = response.json()
Response200
[
  {
    "id": "tag_2m8k4q9wx3hd7rtn",
    "name": "shipping",
    "count": 18
  },
  {
    "id": "tag_9w3k7q2hm4xd8rtb",
    "name": "vip",
    "count": 4
  }
]

Documents

The document library: invoices, receipts, contracts and other files, with what Fetch read from them.

Search documents

GET/v1/documents

Needs readSafe to retry

Searches the document library: invoices, receipts, contracts and other files, with extracted fields.

Query parameters

  • qstring

    Words in the title, file name or fields.

    Up to 200 characters

  • kindstring

    Only this kind of document.

    One of invoice, receipt, statement, contract, id, tax, photo, other

  • limitinteger

    How many to return, newest first.

    Default 25 · 1 to 100

Returns

200 An array of objects, newest first.

Fields in each item
  • idstringRequired

    The document's id, like doc_….

  • titlestringRequired

    A readable title, or the file name.

  • filenamestringRequired
  • kindstringRequired

    One of invoice, receipt, statement, contract, id, tax, photo, other · Can be null

  • fieldsobjectRequired

    What Fetch read from it. Any of these can be missing.

    Fields
    • vendorstring

      Can be null

    • amountnumber

      Can be null

    • currencystring

      Can be null

    • datestring

      Can be null

    • dueDatestring

      Can be null

    • referencestring

      Can be null

  • conversationIdstringRequired

    Can be null

  • createdAttimestampRequired

    ISO 8601, UTC.

Errors

  • 400Something in the request is missing or not allowed. The message says which field.
  • 401No key, or the key is wrong or revoked.
  • 403The key doesn't have the read scope.
  • 429More than 120 requests in a minute with this key. Wait, then try again.

Also an MCP tool: search_documents. Connect with MCP

GET/v1/documents
curl
curl 'https://app.userookery.com/api/v1/documents?q=acme&kind=invoice&limit=1' \
  -H "Authorization: Bearer $ROOKERY_KEY"
JavaScript
const response = await fetch("https://app.userookery.com/api/v1/documents?q=acme&kind=invoice&limit=1", {
  headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();
Python
import os
import requests

response = requests.get(
    "https://app.userookery.com/api/v1/documents",
    headers={"Authorization": f"Bearer {os.environ['ROOKERY_KEY']}"},
    params={
        "q": "acme",
        "kind": "invoice",
        "limit": 1,
    },
)
data = response.json()
Response200
[
  {
    "id": "doc_4h8q2w9k7mx3drtn",
    "title": "Acme Supplies invoice 1182",
    "filename": "INV-1182.pdf",
    "kind": "invoice",
    "fields": {
      "vendor": "Acme Supplies",
      "amount": 412.5,
      "currency": "USD",
      "dueDate": "2026-10-30",
      "reference": "1182"
    },
    "conversationId": "cnv_8k2m4q7rx9tbw3hd",
    "createdAt": "2026-10-09T14:32:05.000Z"
  }
]

Webhook events

What Rookery POSTs to your webhook. Every event has the same envelope; these are the fields in each one's data. Set up, verify and retry: Webhooks.

Ask completed

POSTask.completed

Version 1

Everything you asked a client for is in (documents today). Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • ask_idstringRequired

    The Ask, like req_… for a document request.

  • kindstringRequired

    What was asked for. documents today; more kinds will come.

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "ask.completed",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "ask_id": "req_8k2m4q7rx9tbw3hd",
    "kind": "documents",
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "summary": "Everything you asked for is in",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Client replied

POSTclient.replied

Version 1

Someone wrote back on a conversation. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • message_idstringRequired

    Their message, like msg_….

  • mailbox_idstringRequired

    The inbox it arrived in, like mbx_….

  • securebooleanRequired

    True when they replied on a secure page, not by email.

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "client.replied",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "message_id": "msg_7h2kq9m3x1c8v5bn",
    "mailbox_id": "mbx_d56zb3gp9g8qj3qt",
    "secure": false,
    "summary": "Dana Wu replied",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Conversation assigned

POSTconversation.assigned

Version 1

A conversation was given to someone, or to nobody. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • assigneeobjectRequired

    Who has it now, or null when nobody does.

    Can be null

    Fields
    • typestringRequired

      One of user, agent

    • idstringRequired

      A usr_… or agt_… id.

    • namestringRequired

      Can be null

  • assigned_byobjectRequired
    Fields
    • typestringRequired

      One of user, agent, system

    • idstringRequired

      Null when Rookery itself did it.

      Can be null

    • namestringRequired
  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "conversation.assigned",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "assignee": {
      "type": "user",
      "id": "usr_4k8m2q9wx7hd3rtn",
      "name": "Jess"
    },
    "assigned_by": {
      "type": "agent",
      "id": "agt_3n7q2wv9k4hxm8tr",
      "name": "Support bot"
    },
    "summary": "Assigned to Jess",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Turn changed

POSTturn.changed

Version 1

Whose turn it is changed: yours, theirs, or done. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • turnstringRequired

    ours: your team's turn. theirs: waiting on them. done: closed.

    One of ours, theirs, done

  • previousstringRequired

    The turn before, or null the first time.

    One of ours, theirs, done · Can be null

  • assigneeobjectRequired

    Who it's assigned to, if anyone.

    Can be null

    Fields
    • typestringRequired

      One of user, agent

    • idstringRequired
    • namestringRequired

      Can be null

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "turn.changed",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "turn": "ours",
    "previous": "theirs",
    "assignee": {
      "type": "user",
      "id": "usr_4k8m2q9wx7hd3rtn",
      "name": "Jess"
    },
    "summary": "Jess's turn",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Draft approved

POSTdraft.approved

Version 1

A person approved a helper's or an agent's draft. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • message_idstringRequired

    The approved reply, like msg_…. It goes out after the undo window.

  • drafted_byobjectRequired
    Fields
    • typestringRequired

      One of user, agent · Can be null

    • idstringRequired

      Can be null

  • approved_byobjectRequired
    Fields
    • typestringRequired

      One of user, agent, system

    • idstringRequired

      Null when Rookery itself did it.

      Can be null

    • namestringRequired
  • editedbooleanRequired

    True when the person changed the draft before approving it.

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "draft.approved",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "message_id": "msg_9q2w7k4hx3mr8dtb",
    "drafted_by": {
      "type": "agent",
      "id": "agt_3n7q2wv9k4hxm8tr"
    },
    "approved_by": {
      "type": "user",
      "id": "usr_4k8m2q9wx7hd3rtn",
      "name": "Jess"
    },
    "edited": true,
    "summary": "Jess approved a draft",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Email sent

POSTmessage.sent

Version 1

An email went out. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • message_idstringRequired

    The email that went out, like msg_….

  • mailbox_idstringRequired

    The inbox it went from, like mbx_….

  • authorobjectRequired

    Who wrote it, or null for Rookery's own mail.

    Can be null

    Fields
    • typestringRequired

      One of user, agent

    • idstringRequired

      Can be null

  • securebooleanRequired

    True when it went as a secure link.

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "message.sent",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "message_id": "msg_9q2w7k4hx3mr8dtb",
    "mailbox_id": "mbx_d56zb3gp9g8qj3qt",
    "author": {
      "type": "user",
      "id": "usr_4k8m2q9wx7hd3rtn"
    },
    "secure": false,
    "summary": "Email sent",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Booking made

POSTbooking.created

Version 1

Someone booked a time with you. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • booking_idstringRequired
  • booking_type_idstringRequired
  • host_user_idstringRequired

    The person they booked with, like usr_….

  • conversation_idstringRequired

    The conversation it's part of, if any.

    Can be null

  • starts_attimestampRequired
  • ends_attimestampRequired
  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "booking.created",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "booking_id": "bkg_2m8k4q9wx3hd7rtn",
    "booking_type_id": "bkt_6t2k9q4wm8xh3drn",
    "host_user_id": "usr_4k8m2q9wx7hd3rtn",
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "starts_at": "2026-10-14T15:00:00.000Z",
    "ends_at": "2026-10-14T15:30:00.000Z",
    "summary": "Dana Wu booked Intro call",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Booking cancelled

POSTbooking.cancelled

Version 1

A booking was cancelled. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • booking_idstringRequired
  • booking_type_idstringRequired
  • host_user_idstringRequired

    The person they had booked with, like usr_….

  • conversation_idstringRequired

    The conversation it's part of, if any.

    Can be null

  • starts_attimestampRequired
  • ends_attimestampRequired
  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "booking.cancelled",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "booking_id": "bkg_2m8k4q9wx3hd7rtn",
    "booking_type_id": "bkt_6t2k9q4wm8xh3drn",
    "host_user_id": "usr_4k8m2q9wx7hd3rtn",
    "conversation_id": null,
    "starts_at": "2026-10-14T15:00:00.000Z",
    "ends_at": "2026-10-14T15:30:00.000Z",
    "summary": "Dana Wu cancelled Intro call"
  }
}

Held by Scout

POSTscout.held

Version 1

Scout held a likely scam. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • conversation_idstringRequired

    The conversation, like cnv_…. Read it with the API.

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "scout.held",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "conversation_id": "cnv_zys19vqj63sb1z4v",
    "summary": "Held by Scout: The sender asks you to pay a new bank account.",
    "url": "https://app.userookery.com/c/cnv_zys19vqj63sb1z4v"
  }
}

Test event

POSTwebhook.test

Version 1

Sent when you press Send test event. Rookery POSTs it to every webhook that ticked it and may see it. See Webhooks for the headers, verification and retries.

Fields in data

  • endpoint_idstringRequired

    The webhook it was sent to, like whk_….

  • summarystringRequired

    One line, in plain words. Never the email's own words.

    Up to 200 characters

  • urlstring

    A link to the conversation in Rookery, when there is one.

Example bodyPOST
{
  "id": "evt_4pj6sy63n8spf4h9",
  "type": "webhook.test",
  "version": 1,
  "created_at": "2026-10-13T09:14:02.118Z",
  "workspace": "wsp_0qadwjbmg1dw6wws",
  "data": {
    "endpoint_id": "whk_5d9w2k7hq3mx8rtn",
    "summary": "A test event from Rookery. If you can read this, it works."
  }
}