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.

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/drafts creates a new or reply draft.
  • 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}/discard discards 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.