Quickstart
This walks a coding agent through creating an inbox, registering a webhook so new mail is pushed to it, and replying to a message — the three actions almost every integration needs.
1. Get an API key
Two ways, same result — a key of the form ak_ + 40 characters, shown once:
- Agent, no human: Sign up over the API — request a code, read it from the mailbox, verify, and the response carries your first key.
- Human: register at
https://agents.ackuna.com/register?product=agent-mail, then Agent Mail → Developers → API keys.
An Ackuna operator can also issue a key for an organization that already exists, with
php artisan api-key:issue {organization} {name}. Every request below sends the key as a
bearer token.
2. Confirm what the key can do
Every request in this guide (and every path shown anywhere in these docs) is relative to
the base URL https://agents.ackuna.com/api/v1:
curl https://agents.ackuna.com/api/v1/me \
-H "Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4"
Shown from here on as a bare path against that same base, for brevity:
GET /v1/me
Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4
{
"data": {
"object": "account",
"organization": {"id": "org_01k2n9v8qf3m7d5b0y6xz4rt1c", "name": "Acme Robotics"},
"api_key": {
"id": "key_01k2n9v8qf3m7d5b0y6xz4rt2d",
"name": "prod agent",
"scopes": ["inboxes:read", "inboxes:write", "messages:read", "messages:send", "webhooks:read", "webhooks:write"]
},
"access": "active",
"quotas": {
"inboxes": {"limit": 3, "used": 0},
"outbound_daily": {"limit": 100, "used": 0, "resets_at": "2026-08-21T23:59:59.999999Z"},
"outbound_daily_shared_domain": {"limit": 100, "used": 0, "workspace_used": 0, "resets_at": "2026-08-21T23:59:59.999999Z"},
"outbound_monthly": {"limit": 3000, "used": 0, "scope": "service", "service_used": 0, "resets_at": "2026-08-31T23:59:59.999999Z"},
"webhook_endpoints": {"limit": 1, "used": 0},
"custom_domains": {"limit": 0, "used": 0}
}
}
}
If access were "none" here, every other call in this guide would fail with 403 agent_mail_not_enabled; "suspended" fails with organization_suspended — see Errors.
outbound_daily, outbound_daily_shared_domain and outbound_monthly each carry a
resets_at; the structural caps (inboxes, webhook_endpoints, custom_domains) do not clear with
time — only a larger grant helps.
3. Create an inbox
POST /v1/inboxes
Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4
Content-Type: application/json
Idempotency-Key: create-support-inbox-1
{"username": "orders-bot"}
{
"data": {
"id": "inb_01k2n9v8qf3m7d5b0y6xz4rt3e",
"object": "inbox",
"address": "orders-bot@agentmail.ackuna.com",
"local_part": "orders-bot",
"domain": "agentmail.ackuna.com",
"display_name": null,
"status": "active",
"thread_count": 0,
"created_at": "2026-08-20T14:00:00.000Z"
}
}
Omitting domain hosts the inbox on the shared platform domain, as above. Pass a domain
(a dom_... id from Receiving domains) to host it on a domain you control
instead.
4. Register a webhook so new mail is pushed to you
POST /v1/webhook-endpoints
Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4
Content-Type: application/json
Idempotency-Key: register-agent-webhook-1
{"url": "https://agent.example.com/webhooks/ackuna", "event_types": ["inbox.message.received"]}
{
"data": {
"id": "whe_01k2n9v8qf3m7d5b0y6xz4rt4f",
"object": "webhook_endpoint",
"url": "https://agent.example.com/webhooks/ackuna",
"event_types": ["inbox.message.received"],
"status": "active",
"created_at": "2026-08-20T14:01:00.000Z",
"secret": "whsec_k3n9v8qf3m7d5b0y6xz4rt1c9a2b7e4d"
}
}
Save secret now — it is shown exactly this once. See Webhooks for the
delivery payload shape and how to verify X-Ackuna-Signature.
5. Reply to a message that arrived
When mail lands in orders-bot@agentmail.ackuna.com, the webhook above delivers an
inbox.message.received event carrying the message's id. Reply to it:
POST /v1/inboxes/inb_01k2n9v8qf3m7d5b0y6xz4rt3e/messages
Authorization: Bearer ak_9f2c1e7b4a6d8035f1c9a2b7e4d6f803a1b2c3d4
Content-Type: application/json
Idempotency-Key: reply-to-msg-01k2n9-1
{"reply_to_message_id": "msg_01k2n9v8qf3m7d5b0y6xz4rt5g", "text_body": "Thanks for reaching out — a human will follow up shortly."}
{
"data": {
"id": "msg_01k2n9v8qf3m7d5b0y6xz4rt6h",
"object": "message",
"inbox_id": "inb_01k2n9v8qf3m7d5b0y6xz4rt3e",
"thread_id": "thr_01k2n9v8qf3m7d5b0y6xz4rt7i",
"direction": "outbound",
"status": "queued",
"error_message": null,
"from": {"email": "orders-bot@agentmail.ackuna.com", "name": null},
"to": [{"email": "customer@example.com", "name": "A Customer"}],
"cc": [],
"subject": "Re: Question about my order",
"text_body": "Thanks for reaching out — a human will follow up shortly.",
"html_body": null,
"headers": {"message_id": null, "in_reply_to": "<def456@example.com>", "references": ["<def456@example.com>"]},
"attachments": [],
"spam_verdict": null,
"virus_verdict": null,
"authentication": {"spf": null, "dkim": null, "dmarc": null, "spam": null, "state": "unknown"},
"provider_message_id": null,
"created_at": "2026-08-20T14:05:00.000Z"
}
}
The status is 202: the message is accepted into the send queue, not yet delivered, so
status is queued and provider_message_id is null. The outcome arrives as an
inbox.message.sent or inbox.message.failed webhook (add both to event_types in step 4
to receive them), or read it from GET /v1/messages/{id}; see
Messages. A message held by Ackuna's pre-send content check waits in
in_review until a person reviews it.
reply_to_message_id is enough — to is inferred from the message being replied to. See
Messages for sending without a reply target, and for how attachments and
threads work.
From here: Inboxes, Threads, Messages, Webhooks, Receiving domains, Errors.