Docs · Reference

API reference

Every /v1 endpoint, with parameters, pagination, errors and limits.

Base URL: https://mailroom.capumattu.com. All /v1 routes need Authorization: Bearer <api_key>. The dashboard calls the same routes with its session instead; see Dashboard and account API.

#Conventions

  • Lists use cursor pagination: ?cursor=&limit= (max 100). Responses are { data, next_cursor }.
  • Errors are always { "error": { "code", "message" } }.
  • Send and reply honour an Idempotency-Key header, so retries never send twice.
  • Times are ISO 8601 in UTC.

#Inboxes

Method & pathNotes
POST /v1/inboxes{ username?, display_name?, client_id?, metadata?, domain? }. Random readable username if omitted. Idempotent on client_id (200 with the existing inbox). domain is one of your verified domains (default agents.capumattu.com). Send { ttl_seconds } (60–86 400) instead for a temporary inbox; username, domain and client_id are not allowed with it.
GET /v1/inboxesList inboxes. Filter on your own fields with ?metadata.<key>=<value> (exact string match). Temporary inboxes are left out unless you pass ?include_temporary=true; ?temporary=true lists only them.
GET /v1/inboxes/:idOne inbox, with its metadata. Every inbox has temporary (boolean) and expires_at (ISO time, or null).
PATCH /v1/inboxes/:id{ display_name?, metadata?, ttl_seconds? }. Metadata is merged; a null value deletes the key. See Metadata. ttl_seconds extends a temporary inbox, counted from now, up to 24 hours after it was created.
DELETE /v1/inboxes/:idSoft delete. The address is never reused. On a temporary inbox this wipes its messages, attachments and threads right away.

#Messages

Method & pathNotes
POST /v1/inboxes/:id/messages/send{ to, cc?, bcc?, subject, text?, html?, attachments?: [{ filename, content_type, content_base64 }], labels? }. Returns 202 with status: "queued".
POST /v1/inboxes/:id/messages/:messageId/reply{ text?, html?, attachments?, reply_all? }. Sets In-Reply-To and References, adds Re:, stays in the thread.
GET /v1/inboxes/:id/messagesFilters: labels=a,b (must have all), is_read, direction.
GET /v1/inboxes/:id/messages/waitLong-poll. timeout (1–60 s, default 30), since (ISO, default now), from, subject, direction (default inbound). Returns { data: Message | null }: the oldest message after since, or null on timeout.
GET /v1/inboxes/:id/verificationLong-poll for a login code or magic link. timeout, since, from. Returns the newest matching message’s code, link and confidence, or null.
GET /v1/messages/:idOne message.
PATCH /v1/messages/:id{ add_labels?, remove_labels?, is_read? }.
GET /v1/messages/:id/rawOriginal MIME (message/rfc822, download only).
POST /v1/messages/:id/draft-reply{ instructions? } (up to 2,000 characters). Returns { text, citations: [{ knowledge_id, title }], model }. Never sends. Needs AI processing full; see Reply drafts.
GET /v1/attachments/:idStreams the file (download only, nosniff, sandbox CSP).
Method & pathNotes
GET /v1/inboxes/:id/threadsThreads in an inbox, newest first.
GET /v1/threads/:idThe thread plus its messages in order. Threads carry labels and metadata.
PATCH /v1/threads/:id{ metadata?, add_labels?, remove_labels? }. Metadata is merged; null deletes a key.
GET /v1/search?q=&inbox_id=Postgres full-text search with web-search syntax ("exact phrase", -exclude, or), ranked, with a snippet.

#Contacts

Method & pathNotes
GET /v1/contacts?q=&label=&cursor=&limit=. Newest last_seen_at first. q matches address or name. ?metadata.<key>=<value> filters.
GET /v1/contacts/:idThe contact plus recent_threads (up to 10, across all inboxes).
GET /v1/contacts/by-address/:addressLook a contact up by email address. 404 if you have never exchanged mail with it.
PATCH /v1/contacts/:id{ name?, notes?, metadata?, add_labels?, remove_labels? }. Notes up to 10,000 characters. See Contacts and memory.

#Knowledge

Method & pathNotes
GET /v1/knowledge?inbox_id=&cursor=&limit=. Items have an excerpt (up to 200 characters) instead of the body.
POST /v1/knowledge{ title, body, inbox_id? }. Body up to 200,000 characters of plain text or markdown. No inbox_id means all inboxes. 201 with the document.
GET /v1/knowledge/:idOne document, with its body.
PATCH /v1/knowledge/:id{ title?, body?, inbox_id? }.
DELETE /v1/knowledge/:idDeletes the document.
GET /v1/knowledge/search?q=&inbox_id=&limit=. Ranked full-text search. With inbox_id, workspace-wide documents are included. Returns { data: [{ id, title, inbox_id, rank, snippet }] }.

#Webhooks

Method & pathNotes
POST /v1/webhooks{ url, events?, inbox_ids?, enabled?, secret? }. The secret is returned only here.
GET /v1/webhooks · GET /v1/webhooks/:idReturns secret_hint, never the secret.
PATCH /v1/webhooks/:id · DELETE /v1/webhooks/:idChange events, scope or enabled; delete.
POST /v1/webhooks/:id/testSends a webhook.test event.
GET /v1/webhooks/:id/deliveriesRecent attempts: status, attempts, last status code and error, next retry.

#Domains

Method & pathNotes
POST /v1/domains{ domain, receiving? } → 201 with status: "pending" and the DNS records to publish. How many depends on the plan (Free 1, Builder 10, Team 50; 403 plan_limit_domains past it). See Custom domains.
GET /v1/domainsList your domains.
GET /v1/domains/:idOne domain with its records and each record’s last check result.
POST /v1/domains/:id/verifyChecks the DNS records now and returns the domain. At most once every 10 seconds per domain.
PATCH /v1/domains/:id{ receiving } turns receiving on or off.
DELETE /v1/domains/:id409 domain_in_use while inboxes use the domain. ?force=true deletes those inboxes too.

#The message object

Every message has text, html, extracted_text, headers, labels, status (queued | sent | delivered | bounced | complained | failed | received), attachment metadata, contact_id (inbound: the sender’s contact; outbound: the first recipient’s; or null), and ai.verification ({ code, link, confidence, jev_probability } or null). Inbound messages also get the JEV fields described below once they have been categorised.

Labels you may see on inbound mail: spf-fail and dmarc-fail (authentication failed), the ai:* labels below, and bounce-report on delivery-failure notices.

#Categories and risk flags

Shortly after an inbound message is stored, JEV reads the sender, subject, the first 3,000 characters of the text, our own SPF/DKIM/DMARC results and the attachment names, and answers six questions in one call. The results are merged into message.ai and a message.enriched webhook fires. message.received is never delayed by this. Bounce reports are not categorised.

message.ai
{
  "verification": null,
  "category": { "label": "billing", "confidence": 0.94, "probabilities": { "billing": 0.94, "other": 0.06 } },
  "risk": { "injection": 0.01, "phishing": 0.03 },
  "needs_human": 0.22,
  "urgency": { "level": "normal", "score": 1.4, "confidence": 0.72 },
  "auto_reply": 0.03,
  "model": "jev",
  "enriched_at": "2026-09-25T12:00:04.000Z"
}
FieldMeaning
categoryOne of support, sales, billing, verification, notification, newsletter, personal, other, with its confidence and the probability of each option.
risk.injectionProbability that the message tries to instruct an AI (ignore its rules, reveal data, act for the sender).
risk.phishingProbability of credential harvesting, a scam or impersonation. The authentication results are part of the input.
needs_humanProbability that a person should handle it: legal matters, complaints, threats, sensitive personal data.
urgencylevel is the most likely of low, normal, high, critical; score is the expected value from 0 to 3.
auto_replyProbability of an out-of-office, auto-responder, ticket acknowledgement or bulk mail.
enrichment_errorSet instead of the fields above when enrichment gave up after its retries. The message is otherwise unaffected.

All probabilities are 0 to 1. A label is only added when a score passes its threshold, so a message without ai:phishing can still have a non-zero risk.phishing. Read the scores if you need a different cut-off.

LabelAdded when
ai:<category>, e.g. ai:billingCategory confidence is 0.6 or more.
ai:uncertainCategory confidence is under 0.6 (instead of a category label).
ai:injection-riskrisk.injection is 0.8 or more.
ai:phishingrisk.phishing is 0.8 or more.
ai:needs-humanneeds_human is 0.7 or more.
ai:auto-replyauto_reply is 0.8 or more.
ai:urgenturgency.level is high or critical.

Labels are only ever added; your own labels are never removed or reordered. Filter on them with GET /v1/inboxes/:id/messages?labels=ai:billing,ai:urgent (a message must have every label you list).

#Limits and errors

Status & codeWhen
401 unauthorizedMissing or revoked API key.
403 forbiddenThe action needs a workspace owner, for example changing AI processing.
403 ai_disabledA reply draft was requested but AI processing is not full.
403 org_suspendedSending is paused for the workspace (bounce rate over 5% or complaint rate over 0.1% in the last 7 days). Clean the recipient list, then write to abuse@capumattu.com.
403 temporary_inbox_receive_onlySend or reply from a temporary inbox. They only receive.
403 domain_not_verifiedSend from an inbox whose domain failed its DNS checks. Fix the records and check the domain again.
409 domain_taken · 409 domain_in_useThe domain is verified by another workspace; or you tried to delete a domain that inboxes still use.
413Message or attachment over the size limit.
422 recipient_suppressedThe address hard-bounced or complained before.
402 plan_limit_emailsThe plan’s monthly emails are used up. Receiving keeps working. See Plans and limits for every plan error.
403 plan_limit_inboxesThe plan’s inboxes are all in use.
429 daily_send_limit_exceededThis inbox’s daily send limit reached. Resets at 00:00 UTC.
429 org_daily_limit_exceededThe workspace’s daily send cap (from its plan) reached. Resets at 00:00 UTC.
429 rate_limitedToo many requests for this API key in a minute.
429 temporary_inbox_limit25 temporary inboxes are already active in the workspace. Creating them is also limited to 60 an hour.
503 llm_unavailableThe drafting model didn’t answer. Nothing was sent; try again later.

#Dashboard and account API

The dashboard at capumattu.com uses these routes with a session cookie (cm_session) instead of an API key. Requests that change data (POST, PATCH, DELETE) must send X-Requested-With: capumattu-web, or they get 403 csrf. You don’t need them to build an agent; they are listed so nothing the dashboard does is hidden.

Method & pathNotes
POST /platform/auth/magic-link{ email }. Always 204. Emails a sign-in link from login@capumattu.com, valid 15 minutes, usable once. Rate-limited per address and per IP.
POST /platform/auth/verify{ token } from the link. 200 { user, org } and sets the session cookie; 400 invalid_token if the link expired or was used.
GET /platform/auth/githubStarts GitHub sign-in (?next=/app). Not enabled at launch.
POST /platform/auth/logout204. Ends the session.
GET /platform/me{ user, org }, or 401 unauthorized. The org includes status, daily_send_limit and ai_processing.
GET /platform/api-keysAPI keys: name, prefix, created, last used, revoked. Never the secret.
POST /platform/api-keys{ name }. 201 { api_key, key }: the full key is shown this once.
DELETE /platform/api-keys/:idRevokes the key. 204.
GET /platform/usage?days=30 (up to 90). Sends and receives today, the limits, the inbox count and a daily series.
PATCH /platform/org{ name?, ai_processing? }. Owners only (403 forbidden otherwise). ai_processing is off, categorize or full.