getinbox API
Email infrastructure for AI agents over one JSON API: create inboxes, send and receive mail, and wait for the message your agent needs.
Overview
All endpoints live under one base URL and speak JSON. Requests use standard HTTP verbs, responses use standard status codes, and every response carries an X-Request-Id header you can quote to support.
https://api.getinbox.cc/v1
Timestamps are ISO 8601 in UTC. List endpoints return { "data": [...], "has_more": bool, "next_cursor": string|null } and paginate with an opaque cursor parameter. Send Content-Type: application/json on every request with a body.
Quickstart
The core loop: create an inbox, hand its address to your agent, then wait for the mail that arrives. One inbox per agent is the intended pattern, the address is the agent's identity.
# 1. create an inbox curl -s -X POST https://api.getinbox.cc/v1/inboxes \ -H "Authorization: Bearer $GETINBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"display_name": "Signup Agent"}' { "id": "inb_...", "address": "agent-7f2a@getinbox.cc", ... } # 2. the agent signs up somewhere with that address, # then long-polls for the verification mail curl -s -H "Authorization: Bearer $GETINBOX_API_KEY" \ "https://api.getinbox.cc/v1/inboxes/inb_.../messages?direction=inbound&wait_seconds=30&since=$SINCE" { "data": [ { "subject": "Your code is 481920", "text": "..." } ] } # 3. reply from the agent's own address curl -s -X POST https://api.getinbox.cc/v1/messages \ -H "Authorization: Bearer $GETINBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "agent-7f2a@getinbox.cc", "to": [{"email": "user@example.com"}], "subject": "Confirmed", "text": "Signed up and verified."}'
import datetime, requests API = "https://api.getinbox.cc/v1" H = {"Authorization": "Bearer gi_live_..."} # 1. create an inbox inbox = requests.post(f"{API}/inboxes", json={"display_name": "Signup Agent"}, headers=H).json() print(inbox["address"]) # agent-7f2a@getinbox.cc # 2. note the time, trigger the signup, then wait for the mail since = datetime.datetime.now(datetime.timezone.utc).isoformat() msgs = requests.get( f"{API}/inboxes/{inbox['id']}/messages", params={"direction": "inbound", "wait_seconds": 30, "since": since}, headers=H, ).json()["data"] print(msgs[0]["text"] if msgs else "nothing yet")
const API = "https://api.getinbox.cc/v1"; const H = { Authorization: "Bearer gi_live_...", "Content-Type": "application/json" }; // 1. create an inbox const inbox = await fetch(`${API}/inboxes`, { method: "POST", headers: H, body: JSON.stringify({ display_name: "Signup Agent" }), }).then(r => r.json()); // 2. note the time, trigger the signup, then wait for the mail const since = new Date().toISOString(); const { data } = await fetch( `${API}/inboxes/${inbox.id}/messages?direction=inbound&wait_seconds=30&since=${since}`, { headers: H }, ).then(r => r.json()); console.log(data[0]?.text ?? "nothing yet");
Authentication
Every request is authenticated with a bearer API key. Keys start with gi_live_, are shown once at creation, and are stored by us only as a hash. Treat them like passwords: server side only, never in a browser or a repo.
Authorization: Bearer gi_live_0123abcd...
Each key carries scopes. Beta keys get all of the ones below.
| Scope | Grants |
|---|---|
| inboxes:read | List and read inboxes |
| inboxes:write | Create, update, and delete inboxes |
| messages:read | List and read messages, including the long-poll wait |
| messages:send | Send messages |
| billing:read | Read the credit balance and ledger |
Requests without a valid key get 401 authentication; a valid key without the needed scope gets 403 permission.
Errors
Errors share one envelope. type is the broad class, code is the machine-readable specific, param points at the offending field when there is one.
{
"error": {
"type": "invalid_request",
"code": "too_many_recipients",
"message": "At most 50 recipients per message across to, cc, and bcc.",
"param": "to",
"request_id": "req_6a4c08d8..."
}
}
| Type | Status | Meaning |
|---|---|---|
| invalid_request | 400 / 413 | Malformed body, bad parameter, or a size limit hit |
| authentication | 401 | Missing, unknown, or revoked API key |
| permission | 403 | Missing scope, or acting on something you do not own |
| not_found | 404 | No such resource on this account |
| conflict | 409 | Address taken or reserved |
| insufficient_credit | 402 | No message credits left; sending is refused, never queued |
| rate_limit | 429 | Too many sends this minute; honor Retry-After |
| provider | 502 / 503 | The mail provider refused the message or is not configured |
| internal | 500 | Our fault; quote the request_id |
Limits
| Limit | Value |
|---|---|
| Recipients per message | 50 across to, cc, and bcc |
| Attachments per message | 10 files, 5 MiB decoded total |
| Body sizes | text up to 1 MB, html up to 2 MB, request body up to 10 MB |
| Subject | 998 characters |
| Send rate | 60 sends per minute per API key |
| Long-poll wait | wait_seconds up to 60 |
| Ephemeral TTL | 60 seconds to 30 days |
| Inbound message size | up to 25 MiB accepted; bodies stored, attachment content not stored during the beta (metadata only) |
| List page size | limit up to 100, default 25 |
Platform sending quotas also apply during the beta and grow with sending reputation. When a send is refused for quota reasons you get a provider error and your credit is refunded.
Inboxes
An inbox is a real address under getinbox.cc and the identity of one agent. It sends, it receives, and its history is queryable. Ephemeral inboxes expire on a TTL and are meant to be created in bulk and thrown away.
/v1/inboxesinboxes:write| Field | Type | Description |
|---|---|---|
| local_part | string? | Chosen address part (a-z, 0-9, dot, underscore, hyphen; max 64). Omit for a random agent-xxxxxx. Role names like postmaster and abuse are reserved. |
| display_name | string? | Friendly From name used on sends from this inbox |
| kind | string? | standard (default) or ephemeral |
| ttl_seconds | int? | Ephemeral only: 60 to 2592000. Default 3600. The inbox expires and stops receiving. |
| webhook_url | string? | HTTPS URL that receives a signed POST per inbound message |
| metadata | object? | Free-form JSON, up to 8 KB. Tag inboxes with your own agent ids. |
curl -s -X POST https://api.getinbox.cc/v1/inboxes \ -H "Authorization: Bearer $GETINBOX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"local_part": "support-agent", "display_name": "Acme Support Agent", "metadata": {"agent_id": "agent_42"}}' { "id": "inb_6a4c08d7c2...", "address": "support-agent@getinbox.cc", "local_part": "support-agent", "domain": "getinbox.cc", "display_name": "Acme Support Agent", "kind": "standard", "status": "active", "webhook_url": null, "webhook_secret": null, "metadata": { "agent_id": "agent_42" }, "expires_at": null, "created_at": "2026-07-06T20:01:12.000Z" }
/v1/inboxesinboxes:readLists inboxes, newest first. Query: limit (1-100, default 25), cursor (from next_cursor).
/v1/inboxes/:idinboxes:readFetches one inbox, including its webhook_secret if a webhook is configured.
/v1/inboxes/:idinboxes:writeUpdates display_name, webhook_url, or metadata. Pass null to clear a field. Setting a new webhook URL rotates the webhook secret.
/v1/inboxes/:idinboxes:writeSoft delete: the inbox becomes disabled, mail to it is rejected at SMTP level, and the address stays reserved to your account so it can never be claimed by someone else. Message history remains readable.
Sending
/v1/messagesmessages:sendfrom must be the address of an active inbox on your account, that is the identity model, agents only speak as themselves. Costs one message credit; refused with 402 when the balance is empty.
| Field | Type | Description |
|---|---|---|
| from | string | Address of one of your active inboxes |
| from_name | string? | Overrides the inbox display_name for this send |
| to | address[] | 1 to 50 recipients: {"email": "...", "name": "?"} |
| cc, bcc | address[]? | Count toward the 50-recipient total |
| reply_to | address? | Reply-To header |
| subject | string | Up to 998 characters |
| text, html | string? | At least one required |
| headers | object? | Custom headers (threading, X-Agent-Id). Core headers like From and Subject cannot be overridden. |
| attachments | array? | {"filename", "content_base64", "content_type"?}, 10 max, 5 MiB decoded total |
Idempotency
Send an Idempotency-Key header (up to 200 chars) and retries become safe: the same key returns the original message with status 200 instead of sending twice. Use it, agents retry.
curl -s -X POST https://api.getinbox.cc/v1/messages \ -H "Authorization: Bearer $GETINBOX_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: signup-confirm-42" \ -d '{"from": "support-agent@getinbox.cc", "to": [{"email": "user@example.com"}], "subject": "Your account is ready", "text": "Everything is set up.", "headers": {"X-Agent-Id": "agent_42"}}' { "id": "msg_6a4c08d855...", "direction": "outbound", "status": "sent", "from": { "email": "support-agent@getinbox.cc", "name": "Acme Support Agent" }, "to": [ { "email": "user@example.com" } ], "subject": "Your account is ready", "message_id": "<b838...@getinbox.cc>", "created_at": "2026-07-06T20:02:41.000Z" }
Deliverability (SPF, DKIM, DMARC, bounce handling) is managed on the shared domain for you. Transactional and operational mail only; see the Acceptable Use Policy.
Receiving
Mail sent to any active inbox address is parsed and stored within seconds of arrival. Read it by polling, long-polling, or webhooks. Unknown addresses are rejected at SMTP level, so senders get a real bounce.
/v1/inboxes/:id/messagesmessages:read| Query | Type | Description |
|---|---|---|
| direction | string? | inbound or outbound; omit for both |
| since | string? | ISO 8601; only messages that arrived after this instant |
| wait_seconds | int? | 0 to 60. When the result would be empty, the request holds until a matching message arrives or the window closes, then returns. |
| limit, cursor | ? | Pagination, newest first |
The wait-for-the-code pattern
Take a timestamp before triggering the email, then wait with since set to it. If the mail beat you to the inbox you get it instantly; if not, the request holds until it lands. No sleep loops, no missed races.
# before clicking "sign up": SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ) # ...agent signs up with agent-7f2a@getinbox.cc, then: curl -s -H "Authorization: Bearer $GETINBOX_API_KEY" \ "https://api.getinbox.cc/v1/inboxes/inb_.../messages?direction=inbound&since=$SINCE&wait_seconds=45" { "data": [ { "id": "msg_...", "direction": "inbound", "from": { "email": "no-reply@service.example" }, "subject": "Your verification code is 481920", "text": "Your code is 481920. It expires in 10 minutes." } ], "has_more": false, "next_cursor": null }
Extract the code from text with your model or a regex. Server-side extraction with confidence scoring is on the roadmap.
/v1/messages/:idmessages:readFull message including html and stored headers (Message-ID, In-Reply-To, References, Authentication-Results). List responses omit those two for size; everything else matches.
Webhooks
Set webhook_url on an inbox and every inbound message POSTs to it as it arrives. Each inbox gets its own webhook_secret, readable via the API, used to sign deliveries.
POST your-url X-Getinbox-Signature: sha256=<hex hmac of the raw body> { "type": "message.received", "data": { ...full message object... } }
Verify by recomputing the HMAC over the exact raw body:
import crypto from "node:crypto"; function verify(rawBody, signatureHeader, webhookSecret) { const expected = "sha256=" + crypto.createHmac("sha256", webhookSecret).update(rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected)); }
Billing and credits
Prepaid credits, no subscription. One send costs one credit; receiving is free. Sending is fail-closed: an empty balance means 402, never silent queuing, so a runaway agent cannot run up a bill.
- Beta accounts start with 200 free credits.
- Credits never expire.
- Failed provider sends are refunded automatically.
/v1/billingbilling:read{
"credit_balance": 184,
"unit": "message_credits",
"ledger": [
{ "delta": -1, "reason": "send", "created_at": "..." },
{ "delta": 200, "reason": "signup_grant", "created_at": "..." }
]
}
/v1/billing/checkoutbilling:readReturns a Stripe Checkout link for a credit pack. During the beta this returns 503 billing_not_configured; email hello@getinbox.cc for a top-up instead.
Beta notes
- Surface may grow, existing fields will not change meaning. Breaking changes get a new version prefix.
- Custom domains (your-agent@yourdomain.com), threads, search, and server-side verification-code extraction are the next planned surfaces, in that order.
- Attachment content on inbound mail is not stored yet, only filename, type, and size.
- Abuse: abuse@getinbox.cc. Read the Acceptable Use Policy; transactional and operational mail only.
- Questions or a stuck request id: hello@getinbox.cc.