Send, reply, and forward API

Send new mail with POST /v1/messages, replies with /reply, and forwards with /forward. All three pass the same sending gate.

View as MarkdownLast updated

All three paths need an Idempotency-Key header and the Send (messages:send) scope.

Send new mail

http
POST /v1/messages
FieldRequiredDescription
agent_idYesThe token's agent ID
fromYesThe agent's address, lowercase and without a +tag
toYesAn array of recipient addresses (not empty)
subjectYesSubject
textYesPlain-text body
htmlNoHTML body
trace_idNoYour own tracking string. Kept in the audit record.
bash
curl -sS https://api.atmark.ai/v1/messages \
  -H "Authorization: Bearer $ATMARK_AGENT_TOKEN" \
  -H "Idempotency-Key: 2026-09-27-weekly-report" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "7c21…f5a1",
    "from": "scout@atmark.ai",
    "to": ["partner@example.com"],
    "subject": "Weekly report",
    "text": "Hello, here is this week'"'"'s report."
  }'

Results

StatusResultBody
200Sentstatus: "sent", message_id, message_id_header, provider_message_id, decision
202Awaiting approvalstatus: "pending_approval", message_id, approval_id, expires_at, decision
403Denied by policyerror: "policy_denied", message_id, decision
409Idempotency key conflicterror: "idempotency_key_reused"
502Send failederror: "send_failed". Nothing went out. You can retry with a new key.
As aboveReplayedreplayed: true with the message's current state. See below.

provider_message_id is the delivery ID assigned when the mail goes out. It's null until then.

decision holds the verdict (verdict), an array of reasons (reasons), the policy version (policy_version), when it was evaluated (evaluated_at), and how long to wait (retry_after_seconds).

Example 200 response
{
  "status": "sent",
  "message_id": "0f8a…",
  "message_id_header": "<…@atmark.ai>",
  "provider_message_id": "…",
  "decision": {
    "verdict": "allowed",
    "reasons": ["allowlist_match"],
    "policy_version": 3,
    "evaluated_at": "2026-09-27T09:00:00.000Z",
    "retry_after_seconds": null
  }
}

Allowed sends carry reasons too. allowlist_match in the example above means every recipient was on the outbound allowlist. All codes are in Reason codes.

A 403 isn't an outage; it's the policy doing its job. Retrying with the same key returns the same denial. After fixing the cause, or waiting retry_after_seconds, send with a new Idempotency-Key.

Replayed requests

Repeat the same request with the same Idempotency-Key and no second email goes out; you get the message's current state with replayed: true. The body has status, message_id, message_id_header, and provider_message_id, but no decision, approval_id, or expires_at. Read the decision with GET /v1/audit?message_id=… and the approval with GET /v1/approvals/{id}.

StatusCurrent state
200status: "sent": went out
202status: "pending" or "dispatching": not sent yet (including awaiting approval)
403status: "denied", error: "policy_denied"
502status: "failed", error: "send_failed"

Reply

http
POST /v1/threads/{threadId}/reply

threadId is the received email's thread_id.

FieldRequiredDescription
agent_idYesThe token's agent ID
in_reply_toYesThe id of the received email you're replying to
textYesPlain-text body
htmlNoHTML body
toNoDefaults to the sender of that email
subjectNoDefaults to Re: original subject
fromNoDefaults to the address that received the email

If the received email doesn't exist, you get 404 message_not_found. Results are the same as for new mail.

Forward

http
POST /v1/messages/{messageId}/forward

messageId is the received email's id.

FieldRequiredDescription
agent_idYesThe token's agent ID
fromYesThe agent's address
toYesAn array of recipient addresses
textNoA note placed above the forwarded email
subjectNoDefaults to Fwd: original subject
  • Only the original body text is carried over. Sending html returns 400. Attachments aren't forwarded; attachments_omitted in the response says how many were left out.
  • If the received email's injection_risk is anything other than none, the forward is refused with 422 forward_refused_injection_risk. If the content matters, write a new email in your own words.

Feedback on this page? Write to support@atmark.ai.