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.
curl https://app.userookery.com/api/v1/me \
-H "Authorization: Bearer $ROOKERY_KEY"{
"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
Returns the agent this key acts as and the scopes it has. A quick way to check a key works.
Returns
The agent's id, like The key's name.200 An object.Fields
agentobjectRequiredFields
idstringRequiredagt_….namestringRequiredscopesarray of stringsRequired
Errors
401No key, or the key is wrong or revoked.403The key doesn't have thereadscope.429More than 120 requests in a minute with this key. Wait, then try again.
REST only.
curl 'https://app.userookery.com/api/v1/me' \
-H "Authorization: Bearer $ROOKERY_KEY"const response = await fetch("https://app.userookery.com/api/v1/me", {
headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();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(){
"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
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
-
viewstringWhich conversations to list.
waitingmeans waiting on the customer.One of
open,waiting,snoozed,closed· Defaultopen -
mailboxstringA mailbox id from list_mailboxes.
-
qstringFull-text search.
Up to 200 characters
-
assignedstringmemeans assigned to this agent.One of
me,unassigned -
limitintegerHow many to return, newest first.
Default
25· 1 to 100
Returns
The conversation's id, like The start of the latest message. One of The customer the conversation is with. Can be null Can be null The inbox's name. ISO 8601, UTC. Who the conversation is assigned to, or null. Can be null A One of The topic Scout sorted it under, or null. Can be null Set by Scout when it sorts new mail. One of Scout's warning, like Can be null200 An array of objects, newest first.Fields in each item
idstringRequiredcnv_….subjectstringRequiredsnippetstringRequiredstatusstringRequiredopen, waiting, closedcontactobjectRequiredFields
namestringRequiredemailstringRequiredmailboxstringRequiredlastMessageAttimestampRequiredmessageCountintegerRequiredassigneeobjectRequiredFields
idstringRequiredusr_… or agt_… id.namestringRequiredtypestringRequireduser, agenttagsarray of stringsRequiredtopicstringRequiredurgencystringRequiredlow, normal, high · Can be nullriskstringRequiredphishing, or null when nothing looks wrong.
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 thereadscope.429More than 120 requests in a minute with this key. Wait, then try again.
Also an MCP tool: list_conversations. Connect with MCP
curl 'https://app.userookery.com/api/v1/conversations?view=open&limit=1' \
-H "Authorization: Bearer $ROOKERY_KEY"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();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()[
{
"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}
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
-
idstringRequiredThe conversation's id, like
cnv_….
Returns
The conversation's id, like One of The inbox's address. The customer the conversation is with. Can be null Can be null Who the conversation is assigned to, or null. Can be null A One of The topic Scout sorted it under, or null. Can be null Set by Scout when it sorts new mail. One of Scout's warning, like Can be null Up to three plain reasons for Can be null The message's id, like ISO 8601, UTC. One of Can be null For replies: where it stands. Null for mail that came in. One of ISO 8601, UTC. ISO 8601, UTC. What happened, like 200 An object.Fields
idstringRequiredcnv_….subjectstringRequiredstatusstringRequiredopen, waiting, closedmailboxstringRequiredcontactobjectRequiredFields
namestringRequiredemailstringRequiredassigneeobjectRequiredFields
idstringRequiredusr_… or agt_… id.namestringRequiredtypestringRequireduser, agenttagsarray of stringsRequiredtopicstringRequiredurgencystringRequiredlow, normal, high · Can be nullriskstringRequiredphishing, or null when nothing looks wrong.riskReasonsarray of stringsRequiredrisk.snoozedUntiltimestampRequiredtimelinearray of objectsRequiredWhen
type is messagetype"message"RequiredidstringRequiredmsg_….attimestampRequireddirectionstringRequiredinbound, outboundfromobjectRequiredFields
namestringRequiredemailstringRequiredstatusstringRequireddraft, scheduled, sending, sent, failed · Can be nulltextstringRequiredattachmentsarray of objectsRequiredFields in each item
filenamestringRequiredcontentTypestringRequiredsizeintegerRequiredWhen
type is notetype"note"RequiredidstringRequiredattimestampRequiredauthorstringRequiredbodystringRequiredWhen
type is eventtype"event"RequiredattimestampRequiredactorstringRequiredeventstringRequiredassigned or status.dataobjectRequiredteamobjectRequiredFields
peoplearray of objectsRequiredFields in each item
idstringRequireduser:usr_…namestringRequiredagentsarray of objectsRequiredFields in each item
idstringRequiredagent: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 thereadscope.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
curl 'https://app.userookery.com/api/v1/conversations/cnv_8k2m4q7rx9tbw3hd' \
-H "Authorization: Bearer $ROOKERY_KEY"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();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(){
"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
Adds an internal note the team sees and the customer never does. Mention people with @FirstName.
Path
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
bodystringRequiredThe 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 thewritescope.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
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?"
}'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();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(){
"ok": true
}
Change status
POST/v1/conversations/{id}/status
Sets a conversation to open, waiting (on the customer) or closed.
Path
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
statusstringRequiredThe new status.
waitingmeans 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 thewritescope.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
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"
}'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();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(){
"ok": true
}
Tag or untag
POST/v1/conversations/{id}/tags
Adds tags to a conversation, or removes them. New tag names are created.
Path
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
addarray of stringsTag names to add.
Up to 10 items
-
removearray of stringsTag 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 thewritescope.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
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"
]
}'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();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(){
"ok": true
}
Assign
POST/v1/conversations/{id}/assign
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
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
tostringRequireduser:usr_…,agent:agt_…,meornone.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 thewritescope.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
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"
}'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();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(){
"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
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
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
bodystringRequiredThe reply, as plain text.
Up to 50,000 characters
-
sendbooleanFalse (the default) makes a draft for a person to approve. True sends it, and needs the
sendscope.Default
false -
summarystringOne line for the person approving: what this reply does.
Up to 500 characters
-
documentsarray of objectsDocuments to ask the customer to upload.
Up to 20 items
Fields in each item
-
labelstringRequiredShort name, e.g. 'Proof of purchase'.
Up to 120 characters
-
descriptionstringWhat counts, in a sentence. Optional.
Up to 500 characters
-
requiredbooleanFalse marks it as nice to have.
Default
true
-
Returns
The message's id, like One of When a sent reply leaves, after the 10-minute undo window. The document request's id, like What happens next, in a sentence.201 An object.Fields
messageIdstringRequiredmsg_….statusstringRequireddraft waits for a person's yes; scheduled goes out on its own.draft, scheduledsendsAttimestamprequestIdstringreq_….notestring
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 thedraftscope (orsend, 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
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"
}'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();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(){
"messageId": "msg_7r3k9w2hq8mx4dtn",
"status": "draft",
"note": "Waiting for a person to approve it."
}
Ask the customer for documents
POST/v1/conversations/{id}/document-requests
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
-
idstringRequiredThe conversation's id, like
cnv_….
Body
-
documentsarray of objectsRequiredWhat to ask for: up to 20 items.
Up to 20 items
Fields in each item
-
labelstringRequiredShort name, e.g. 'Proof of purchase'.
Up to 120 characters
-
descriptionstringWhat counts, in a sentence. Optional.
Up to 500 characters
-
requiredbooleanFalse marks it as nice to have.
Default
true
-
-
messagestringA short note above the checklist. Greet the customer.
Up to 5,000 characters
-
dueDatestringYYYY-MM-DD
Matches
^\d{4}-\d{2}-\d{2}$ -
sendbooleanFalse (the default) makes a draft for a person to approve. True sends it, and needs the
sendscope.Default
false
Returns
The message's id, like One of When a sent reply leaves, after the 10-minute undo window. The document request's id, like What happens next, in a sentence.201 An object.Fields
messageIdstringRequiredmsg_….statusstringRequireddraft waits for a person's yes; scheduled goes out on its own.draft, scheduledsendsAttimestamprequestIdstringreq_….notestring
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 thedraftscope (orsend, 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
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
}'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();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(){
"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
Lists the inboxes, with how many conversations are open in each.
Returns
The mailbox's id, like Open conversations.200 An array of objects, newest first.Fields in each item
idstringRequiredmbx_….addressstringRequirednamestringRequiredopenintegerRequired
Errors
401No key, or the key is wrong or revoked.403The key doesn't have thereadscope.429More than 120 requests in a minute with this key. Wait, then try again.
Also an MCP tool: list_mailboxes. Connect with MCP
curl 'https://app.userookery.com/api/v1/mailboxes' \
-H "Authorization: Bearer $ROOKERY_KEY"const response = await fetch("https://app.userookery.com/api/v1/mailboxes", {
headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();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()[
{
"id": "mbx_6t2k9q4wm8xh3drn",
"address": "help@yourshop.com",
"name": "Support",
"open": 12
}
]
List tags
GET/v1/tags
Lists the tags in use and how many conversations have each.
Returns
The tag's id, like 200 An array of objects, newest first.Fields in each item
idstringRequiredtag_….namestringRequiredcountintegerRequired
Errors
401No key, or the key is wrong or revoked.403The key doesn't have thereadscope.429More than 120 requests in a minute with this key. Wait, then try again.
Also an MCP tool: list_tags. Connect with MCP
curl 'https://app.userookery.com/api/v1/tags' \
-H "Authorization: Bearer $ROOKERY_KEY"const response = await fetch("https://app.userookery.com/api/v1/tags", {
headers: { Authorization: `Bearer ${process.env.ROOKERY_KEY}` },
});
const data = await response.json();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()[
{
"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
Searches the document library: invoices, receipts, contracts and other files, with extracted fields.
Query parameters
-
qstringWords in the title, file name or fields.
Up to 200 characters
-
kindstringOnly this kind of document.
One of
invoice,receipt,statement,contract,id,tax,photo,other -
limitintegerHow many to return, newest first.
Default
25· 1 to 100
Returns
The document's id, like A readable title, or the file name. One of What Fetch read from it. Any of these can be missing. Can be null Can be null Can be null Can be null Can be null Can be null Can be null ISO 8601, UTC.200 An array of objects, newest first.Fields in each item
idstringRequireddoc_….titlestringRequiredfilenamestringRequiredkindstringRequiredinvoice, receipt, statement, contract, id, tax, photo, other · Can be nullfieldsobjectRequiredFields
vendorstringamountnumbercurrencystringdatestringdueDatestringreferencestringconversationIdstringRequiredcreatedAttimestampRequired
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 thereadscope.429More than 120 requests in a minute with this key. Wait, then try again.
Also an MCP tool: search_documents. Connect with MCP
curl 'https://app.userookery.com/api/v1/documents?q=acme&kind=invoice&limit=1' \
-H "Authorization: Bearer $ROOKERY_KEY"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();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()[
{
"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
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_idstringRequiredThe Ask, like
req_…for a document request. -
kindstringRequiredWhat was asked for.
documentstoday; more kinds will come. -
conversation_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
message_idstringRequiredTheir message, like
msg_…. -
mailbox_idstringRequiredThe inbox it arrived in, like
mbx_…. -
securebooleanRequiredTrue when they replied on a secure page, not by email.
-
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
assigneeobjectRequiredWho has it now, or null when nobody does.
Can be null
Fields
-
typestringRequiredOne of
user,agent -
idstringRequiredA
usr_…oragt_…id. -
namestringRequiredCan be null
-
-
assigned_byobjectRequiredFields
-
typestringRequiredOne of
user,agent,system -
idstringRequiredNull when Rookery itself did it.
Can be null
-
namestringRequired
-
-
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
turnstringRequiredours: your team's turn.theirs: waiting on them.done: closed.One of
ours,theirs,done -
previousstringRequiredThe turn before, or null the first time.
One of
ours,theirs,done· Can be null -
assigneeobjectRequiredWho it's assigned to, if anyone.
Can be null
Fields
-
typestringRequiredOne of
user,agent -
idstringRequired -
namestringRequiredCan be null
-
-
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
message_idstringRequiredThe approved reply, like
msg_…. It goes out after the undo window. -
drafted_byobjectRequiredFields
-
typestringRequiredOne of
user,agent· Can be null -
idstringRequiredCan be null
-
-
approved_byobjectRequiredFields
-
typestringRequiredOne of
user,agent,system -
idstringRequiredNull when Rookery itself did it.
Can be null
-
namestringRequired
-
-
editedbooleanRequiredTrue when the person changed the draft before approving it.
-
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
message_idstringRequiredThe email that went out, like
msg_…. -
mailbox_idstringRequiredThe inbox it went from, like
mbx_…. -
authorobjectRequiredWho wrote it, or null for Rookery's own mail.
Can be null
Fields
-
typestringRequiredOne of
user,agent -
idstringRequiredCan be null
-
-
securebooleanRequiredTrue when it went as a secure link.
-
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe person they booked with, like
usr_…. -
conversation_idstringRequiredThe conversation it's part of, if any.
Can be null
-
starts_attimestampRequired -
ends_attimestampRequired -
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe person they had booked with, like
usr_…. -
conversation_idstringRequiredThe conversation it's part of, if any.
Can be null
-
starts_attimestampRequired -
ends_attimestampRequired -
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe conversation, like
cnv_…. Read it with the API. -
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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
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_idstringRequiredThe webhook it was sent to, like
whk_…. -
summarystringRequiredOne line, in plain words. Never the email's own words.
Up to 200 characters
-
urlstringA link to the conversation in Rookery, when there is one.
{
"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."
}
}