Mail Endpoints
The preview Mail API reads and changes mailboxes that a team manager has linked to a Chab project. Every route requires a team API key, a current project grant, the listed mail scope, an eligible paid plan with API mail enabled, and the exact capability on an active connection. Browser mailbox access and Mail AI access are separate; neither grants API mail access.
Mail calls are synchronous and not billable. They do not create operations, reserve credits, use an API-key spending allowance, or spend team credits. Attachment downloads record delivered bytes for accounting but settle zero credits.
Until the projects API is published, copy project_id from the numeric project
identifier in the dashboard project URL. Get connection_id from the project
Access page or GET /v1/mail/connections.
GET routes take project and connection context only from the query string. POST and PATCH routes take their complete input from one JSON object and reject query parameters or non-JSON bodies.
Read mail
The following routes require api:mail:read:
| Route | Additional capability | Result |
|---|---|---|
GET /v1/mail/connections |
none beyond the authorized mail collection | Project mail connections |
GET /v1/mail/folders |
mail.folders.list |
Persisted, non-Drafts folders |
GET /v1/mail/personas |
mail.personas.list |
Available and unavailable sender personas; listing does not authorize send |
GET /v1/mail/threads |
mail.messages.list |
Draft-free thread summaries in one folder |
POST /v1/mail/search |
mail.messages.search |
One newest matching non-draft message per thread |
GET /v1/mail/threads/{thread_id} |
mail.messages.read |
A thread and its draft-free messages |
GET /v1/mail/messages/{message_id}/body |
mail.messages.read |
One sanitized body variant |
GET /v1/mail/messages/{message_id}/attachments/{attachment_id} |
mail.attachments.download |
One direct attachment stream |
List routes accept limit from 1 to 100, default 50, and an opaque cursor.
Cursors expire after 15 minutes and are bound to the API key, project,
connection, folder, query, and sort boundary. Do not inspect or edit them.
Changing any bound input requires starting again without a cursor.
JSON request bodies are type-strict. IDs, revisions, folder IDs, and limits
must be JSON integers; integral floats and numeric strings are rejected.
Boolean filters and dry_run must be JSON true or false, not 0, 1, or
their string forms.
Folders sort as Inbox, Sent, Archive, Trash, Junk, then other folders; folders
within a group sort by case-insensitive name. Personas sort primary first, then
by address. Threads sort newest first. If folder_id is omitted for thread list
or search, Chab uses the persisted selectable Inbox. It never substitutes an
all-mail or provider-native folder.
curl "https://www.chab.ai/v1/mail/connections?project_id=7" \
-H "Authorization: Bearer CHAB_API_KEY"
curl "https://www.chab.ai/v1/mail/folders?project_id=7&connection_id=12" \
-H "Authorization: Bearer CHAB_API_KEY"
curl "https://www.chab.ai/v1/mail/personas?project_id=7&connection_id=12" \
-H "Authorization: Bearer CHAB_API_KEY"
curl "https://www.chab.ai/v1/mail/threads?project_id=7&connection_id=12&limit=50" \
-H "Authorization: Bearer CHAB_API_KEY"
curl "https://www.chab.ai/v1/mail/threads/42?project_id=7&connection_id=12" \
-H "Authorization: Bearer CHAB_API_KEY"
Connection responses contain only the customer alias, provider-neutral label,
safe health/sync state, safe reason, and currently published capabilities. They
do not expose mailbox addresses, provider identifiers, resource keys,
credentials, or management facts. Folder counts can be null when a bounded
local projection is unavailable. Persona send_ready is descriptive and does
not replace the send-time checks.
Thread and message resources expose bounded participants, recipients,
previews, attachment summaries, and local folder IDs. participants and each
recipient list are capped at 100 and include a truncation flag. Previews are
capped at 500 characters. Draft messages, collaboration state, labels, raw
headers, raw provider identifiers, and raw message source are never returned.
Search
POST /v1/mail/search accepts a closed JSON object. Supply a non-empty query,
at least one filter, or both. Criteria are ANDed. Every whitespace-separated
query term must occur as a case-insensitive substring in the subject, sender
address, To address, or current message preview.
{
"project_id": 7,
"connection_id": 12,
"folder_id": 3,
"query": "quarterly update",
"filters": {
"from": "[email protected]",
"to": "[email protected]",
"date_from": "2026-08-01T00:00:00+00:00",
"date_to": "2026-09-01T00:00:00+00:00",
"is_read": false,
"has_attachment": true
},
"limit": 20
}
curl -X POST https://www.chab.ai/v1/mail/search \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"query":"quarterly update","limit":20}'
from and to are exact normalized addresses; To does not include Cc or Bcc.
date_from is inclusive and date_to is exclusive. Dates require RFC 3339
with an offset. has_attachment refers to a non-inline attachment. Labels,
status, size, raw provider syntax, full-body-only criteria, arrays, and OR
filters are unsupported.
Each hit contains thread, matched_message_id, and matched_at. Chab checks
at most 500 candidate messages and returns meta.coverage as
current_projection, extended_mailbox, or partial. A bounded result that
reaches its cap has coverage=partial, has_more=false, a null cursor, and a
structured result_truncated warning. A degraded provider path can similarly
report provider_degraded; an unusable empty provider result is an error, not
an empty degraded success.
Message bodies
Use GET /v1/mail/messages/{message_id}/body with project_id,
connection_id, and optional format=plain|html|markdown (default plain).
The response contains exactly message_id, format, content,
content_available, and derived.
HTML permits only basic text, list, quote, code, link, and table tags. Chab
removes styles, classes, events, forms, scripts, metadata, SVG, media, frames,
objects, embeds, and remote images. CID images become visible [image]
placeholders. Links keep only href and title; only HTTP, HTTPS, and mailto
targets survive, with no-opener and no-referrer protections.
When the requested source form is absent, Chab derives it from the sanitized
available form and sets derived=true. No source returns empty content with
content_available=false. The complete JSON response may not exceed 1 MiB;
Chab does not silently truncate a body.
curl "https://www.chab.ai/v1/mail/messages/84/body?project_id=7&connection_id=12&format=html" \
-H "Authorization: Bearer CHAB_API_KEY"
Attachment downloads
Message attachment summaries contain a versioned, opaque attachment_id.
Use it only with the same message, project, connection, and team, together with
an API key that is currently authorized for that context:
curl -OJ "https://www.chab.ai/v1/mail/messages/84/attachments/OPAQUE_ID?project_id=7&connection_id=12" \
-H "Authorization: Bearer CHAB_API_KEY"
Downloads are direct whole-object streams with a 26,214,400-byte maximum.
Ranges and redirects are unsupported. Chab rechecks current access immediately
before the first byte, sanitizes the filename, detects the completed file type,
uses application/octet-stream for unknown, active, executable, or disagreeing
type information, and sends attachment disposition, nosniff, and
Cache-Control: private, no-store.
This path does not claim that incoming mail attachments were malware-scanned, quarantined, or safe. Treat downloaded files as untrusted input.
Drafts
Draft routes require api:mail:write. Only drafts created by the same API key,
project, and exact originating connection are visible. Browser drafts and
drafts from a replacement connection are never adopted.
POST /v1/mail/draftscreates aneworreplydraft.GET /v1/mail/drafts/{draft_id}reads a draft.PATCH /v1/mail/drafts/{draft_id}changes present composition fields.POST /v1/mail/drafts/{draft_id}/discarddiscards a draft.
A reply requires reply_to_message_id under the same connection and current
read capability. A new draft forbids that field. Mode and reply target cannot
change later.
Recipient fields are ordered arrays of {address,name?}: at most 50 per list
and 100 total, with no duplicate normalized address across To, Cc, and Bcc.
subject is nullable and at most 500 characters. body is nullable or exactly
{format:"plain"|"html",content}, with content at most 262,144 UTF-8 bytes.
persona_id may be null while editing. On PATCH, omission preserves a field
and explicit null clears a nullable field.
Every draft starts at draft_revision=1. Supply the exact
expected_draft_revision for update, discard, attachment changes, and send.
A normalized no-op returns changed=false without advancing it. Composition
or attachment membership changes advance it once. Draft responses include
ready_to_send, sorted blocking_reasons, and outbound_status; these are
descriptive snapshots and are checked again before sending.
Live mutations require an Idempotency-Key. The fingerprint includes the
operation and normalized path target, while replay matching uses the exact raw
JSON bytes. Reusing a key for another draft or message is therefore distinct;
changing JSON bytes on the same target conflicts.
All live mutation routes accept dry_run=true. A dry run validates current
access and inputs and returns the operation-specific projected data shape with
meta.dry_run=true and meta.live_requires_idempotency=true. It does not copy
bytes, allocate IDs, write success audit events, mutate mail, create a send
attempt, or call the mail provider.
curl -X POST https://www.chab.ai/v1/mail/drafts \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-create-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"mode":"new","persona_id":4,"to":[{"address":"[email protected]"}],"subject":"Quarterly update","body":{"format":"plain","content":"Hello"}}'
curl "https://www.chab.ai/v1/mail/drafts/21?project_id=7&connection_id=12" \
-H "Authorization: Bearer CHAB_API_KEY"
curl -X PATCH https://www.chab.ai/v1/mail/drafts/21 \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-update-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_draft_revision":1,"subject":"Updated subject"}'
curl -X POST https://www.chab.ai/v1/mail/drafts/21/discard \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-discard-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_draft_revision":2}'
Draft attachments
Add exactly one existing operation artifact per request:
{
"project_id": 7,
"connection_id": 12,
"expected_draft_revision": 1,
"source": {"type": "artifact_id", "id": "art_01JZ7H0A1B2C3D4E5F6G7H8J9K"}
}
curl -X POST https://www.chab.ai/v1/mail/drafts/21/attachments \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-attachment-add-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_draft_revision":1,"source":{"type":"artifact_id","id":"art_01JZ7H0A1B2C3D4E5F6G7H8J9K"}}'
curl -X POST https://www.chab.ai/v1/mail/drafts/21/attachments/staged_01/remove \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-attachment-remove-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_draft_revision":2}'
The artifact must be available, unexpired, customer-visible, stream-backed, created by the same API key for the exact project, and reusable rather than a download-only export. URLs, paths, raw bytes, multipart uploads, provider handles, and object-store keys are not accepted.
Chab copies the artifact atomically into draft-owned staging while hashing it. A draft can have at most 25 attachments, one source can be at most 26,214,400 bytes, and the combined total has the same limit. Staging expires after 24 hours and reading or replaying a request does not extend it. Expired staging blocks send until removed and re-added. Removing a staged attachment never deletes the original artifact.
The API preserves a sanitized filename and detected or fallback media type. It does not claim to scan, quarantine, or establish the safety of a staged file.
Send a draft
POST /v1/mail/drafts/{draft_id}/send requires api:mail:send, the exact draft
revision, at least one To recipient, a currently available persona, valid
attachments, current connection/send readiness, and current team policy
acceptance. Final content cannot be overridden on this route.
A durable send identity is created before submission. Definite acceptance
returns a SendOutcome with status=queued. Definite rejection marks the
attempt failed and reopens the unchanged draft for a new explicitly idempotent
attempt only if no provider queue was created. If a queued send is canceled,
the original draft stays closed and its provider queue identity is retained.
Its outcome remains unresolved because a late delivery event can race the
cancellation. Check the original message before intentionally composing a new
draft. A timeout, reset, or unusable response is ambiguous: Chab returns a
retryable provider error, retains pending_provider_confirmation, keeps the
public idempotency claim reserved, and never blindly sends with a new provider identity.
Background reconciliation looks for confirmation that the original send
was accepted. It does not submit the message again. If that confirmation is not
yet available, the send stays pending; keep the same draft and idempotency key when
checking or retrying the request. Creating another draft or send request can
send a second copy.
An uncertain submission may return an unmarked provider_unavailable or
provider_response_invalid response. That is not a released idempotency key:
reuse the same key and body, and do not create a replacement send while the
provider outcome is unresolved.
The original send key stays reserved while that provider attempt is pending or
accepted, including when connection capability changes during reconciliation.
A bounce or delivery failure after submission does not reopen the draft or
authorize another send. The original provider identity and delivery records
remain attached; attempting to send that draft again returns
resource_state_conflict. Review the failed delivery before deliberately
composing a new message.
Exact retries use the retained draft revision and snapshot fingerprint even
after the 24-hour public idempotency window. Once the sent provider copy is
readable, Chab removes compose body and staged attachment content while keeping
a content-free receipt anchor for that retry. Reconciliation continues for up
to 30 days. Proven acceptance becomes queued; proven rejection becomes failed.
An unresolved attempt becomes outcome_unknown, closing that draft to edit,
discard, and resend. If confirmation of the original provider acceptance
later appears, Chab can settle that same attempt as queued without submitting
it again. Records of unknown and accepted sends are retained beyond routine
receipt pruning until it can be safely reconciled or the team is safely purged.
There is intentionally no public send-status route and no scheduling or
approval workflow.
curl -X POST https://www.chab.ai/v1/mail/drafts/21/send \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: draft-send-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_draft_revision":3}'
Message state
POST /v1/mail/messages/{message_id}/state requires api:mail:write,
mail.messages.state.update, expected_state_revision, and one action:
| Action | Folder fields |
|---|---|
mark_read, mark_unread, star, unstar |
Source and target forbidden |
archive, trash |
source_folder_id required; target forbidden |
move |
Distinct source_folder_id and target_folder_id required |
Archive and Trash use persisted special-use folders; Chab never guesses a
provider path. Move targets must be selectable non-Drafts folders, and the
message must currently belong to the source folder. An already-satisfied flag
action returns changed=false, performs no provider call, and keeps the state
revision. Accepted changes advance the revision once. Provider failure leaves
the local revision and state unchanged; accepted effects may report
reconciliation_status=pending until local projections converge.
curl -X POST https://www.chab.ai/v1/mail/messages/84/state \
-H "Authorization: Bearer CHAB_API_KEY" \
-H "Idempotency-Key: message-state-01" \
-H "Content-Type: application/json" \
--data '{"project_id":7,"connection_id":12,"expected_state_revision":4,"action":"mark_read"}'
Errors and audit
Mail routes use the standard authentication, plan, scope, rate, validation, idempotency, connected-data, and provider errors. Mail-specific size, range, revision, and state codes are listed under Mail API errors.
Successful live content actions write token-attributed mail audit events with the project, connection, request ID, local resource IDs, revisions, counts, byte totals, action, and safe outcome facts as applicable. Search text, addresses, recipients, subject, body, filenames, attachment references, provider identities, and provider payloads are never written to these events. Mail audit and durable send-attempt metadata are retained for 365 days.