Bot API cookbook
Practical requests for messages, files, reactions, channels, events, interactions, modals, and consented direct messages.
Authentication and response envelope
All bot routes use the installation token:
Authorization: Bearer nookbot_…
Bot routes live under /api/v1/app/*. Paths contain no workspace ID. Success and failure use the
standard envelope:
{ "success": true, "data": {}, "requestId": "…" }
{
"success": false,
"error": { "code": "APP_SCOPE_MISSING", "message": "…" },
"requestId": "…"
}
Log requestId, status, and safe error code for diagnostics. Do not log the authorization header or
private response data.
The shell examples below read that header from a protected runtime file so the token never appears
in command arguments or shell history. Have your secret manager write this file with mode 0600;
do not commit it or place it below a web root:
# /run/secrets/nook-bot.curl
header = "Authorization: Bearer <installation-token-from-secret-manager>"
Post a channel message
Requires messages:write and an allowed channel:
curl --config /run/secrets/nook-bot.curl \
--request POST "https://api.your-nook.example/api/v1/app/messages" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: incident-42-summary-v1" \
--data '{
"channelId": "<allowed-channel-id>",
"body": "Incident 42 is resolved."
}'
Use a unique idempotency key per logical send (≤255 characters):
- same key + same body after success → HTTP 200 with the original message and
replayed: true; - same key + different body →
409 APP_IDEMPOTENCY_KEY_INVALID; - duplicate while the first request is in flight →
409 CONFLICT, honorRetry-After: 1; - a crashed attempt unlocks after approximately 30 seconds.
Rich blocks and components
Blocks require messages:write; actionable components also require interactions:respond:
{
"channelId": "<channel-id>",
"body": "Deployment approval required",
"fallbackText": "Approve or reject deployment 184",
"blocks": [
{ "type": "section", "text": "Deploy build 184 to production?" },
{
"type": "actions",
"elements": [
{
"type": "button",
"actionId": "deploy.approve",
"label": "Approve",
"style": "primary",
"value": "184"
},
{
"type": "button",
"actionId": "deploy.reject",
"label": "Reject",
"style": "danger",
"value": "184"
}
]
}
]
}
Keep actionId stable and namespaced. Treat every click as untrusted input, verify the callback
signature, validate the shared schema, and authorize the business action in your own system.
Read channels, history, and members
GET /api/v1/app/channels
scope: channels:read
GET /api/v1/app/channels/:channelId/messages?limit=<bounded>
scope: messages:read
GET /api/v1/app/members
scope: members:read
Results are filtered by current installation policy. Member results expose display identity and roles needed for the app, never email addresses. Do not cache visibility beyond the time your action needs; policy can change immediately.
Edit or delete your own message
PATCH /api/v1/app/messages/:messageId
DELETE /api/v1/app/messages/:messageId
scope: messages:write
An app can mutate only its own installation-attributed messages. Store returned message IDs rather than searching channel history to rediscover them.
Reactions
Requires reactions:write:
POST /api/v1/app/messages/:messageId/reactions
body: { "emoji": "👍" }
DELETE /api/v1/app/messages/:messageId/reactions/:emoji
The bot adds/removes only its own reaction. An optional scope may be absent; degrade without failing the primary workflow.
Upload and attach a file
Requires files:write. Upload first:
curl --config /run/secrets/nook-bot.curl \
--request POST "https://api.your-nook.example/api/v1/app/files" \
--form "file=@./report.pdf;type=application/pdf"
When the instance uses the V2 file pipeline, the returned file is a private installation-scoped draft. Claim it atomically by including up to ten IDs in the message send:
{
"channelId": "<channel-id>",
"body": "Here is the report.",
"fileIds": ["<uploaded-file-id>"]
}
Only the uploading installation may claim the draft. Expired, cancelled, already-claimed, or
foreign drafts fail the send. If V2 files are disabled, omit fileIds; the server answers 400
rather than pretending to attach.
Commands
Declare typed options in the manifest. A command callback contains:
- immutable manifest command
name; - workspace-local invocation alias for display only;
- resolved string, number, boolean, user, channel, or other supported option values;
- current user/channel context;
- response token and interaction ID.
Answer inside three seconds:
{ "kind": "ephemeral", "text": "Creating the report…" }
Then use the response token for slower work:
const response = await fetch(`${apiBaseUrl}/api/v1/interactions/${interaction.id}/followups`, {
method: 'POST',
headers: {
authorization: `Bearer ${interaction.responseToken}`,
'content-type': 'application/json',
},
body: JSON.stringify({ kind: 'message', text: 'The report is ready.' }),
});
if (!response.ok) throw new Error(`Follow-up failed with ${response.status}`);
The response token expires after 15 minutes and allows at most five follow-ups.
Components and modals
Component callbacks include actionId, optional value, actor, and channel or DM context. An
immediate ack is useful when processing continues asynchronously:
{ "kind": "ack" }
Open a modal immediately when interactions:respond + modals:open are granted:
{
"kind": "modal",
"modal": {
"title": "Send feedback",
"inputs": [
{ "type": "text", "id": "subject", "label": "Subject", "required": true },
{ "type": "textarea", "id": "details", "label": "Details" },
{
"type": "select",
"id": "rating",
"label": "Rating",
"required": true,
"options": [
{ "label": "Great", "value": "great" },
{ "label": "Needs work", "value": "needs-work" }
]
}
]
}
}
Delayed modal open is unsupported. Validate submitted values against your own business rules after the shared callback schema.
Events
Declare supported event names and events:receive. Events arrive at the event URL with at-least-once
delivery. Common examples include message and mention events; dm.message.created additionally
requires dms:write and current DM consent.
{
"type": "event.app.mentioned",
"meta": {
"installationId": "…",
"idempotencyKey": "stable-logical-event-key"
},
"message": {
"id": "…",
"channelId": "…",
"authorUserId": "…",
"body": "@Acme Helper status?"
}
}
Dedupe before side effects. Return success only after durable acceptance, or Nook may retry.
Send a consented DM
Requires dms:write and current interaction-derived consent:
curl --config /run/secrets/nook-bot.curl \
--request POST "https://api.your-nook.example/api/v1/app/dms" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: reminder-<recipient>-<logical-event>" \
--data '{
"userId": "<consented-user-id>",
"text": "Your requested export is ready."
}'
There is no cold-DM scope or administrator bypass. Treat every APP_DM_NOT_ALLOWED as terminal until
the user performs a new permitted interaction. Per-recipient and installation daily limits apply.
Error-handling policy
| Class | Action |
|---|---|
401 token/signature | Stop, reject, and investigate; do not retry with the same unknown authority |
403 runtime/scope/DM gate | Stop the affected action until policy or consent changes |
404 channel/resource | Treat as unknown or invisible; do not enumerate |
409 idempotency/in-flight | Compare code; replay same logical request or honor Retry-After |
413 file quota | Stop upload and surface an operator-actionable quota error |
422 interaction response | Fix request shape/kind; do not retry unchanged |
429 | Honor Retry-After with bounded jitter and the original idempotency key |
5xx/network | Retry boundedly only for an idempotent/deduplicated operation |
Continue with Deploy and operate bots before production use.