API reference · v1 · private beta

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.

Private beta. The API is live for invited accounts. API keys are issued when you come off the waitlist. Nothing here requires a dashboard: every capability is exposed through the API.

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.

ScopeGrants
inboxes:readList and read inboxes
inboxes:writeCreate, update, and delete inboxes
messages:readList and read messages, including the long-poll wait
messages:sendSend messages
billing:readRead 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..."
  }
}
TypeStatusMeaning
invalid_request400 / 413Malformed body, bad parameter, or a size limit hit
authentication401Missing, unknown, or revoked API key
permission403Missing scope, or acting on something you do not own
not_found404No such resource on this account
conflict409Address taken or reserved
insufficient_credit402No message credits left; sending is refused, never queued
rate_limit429Too many sends this minute; honor Retry-After
provider502 / 503The mail provider refused the message or is not configured
internal500Our fault; quote the request_id

Limits

LimitValue
Recipients per message50 across to, cc, and bcc
Attachments per message10 files, 5 MiB decoded total
Body sizestext up to 1 MB, html up to 2 MB, request body up to 10 MB
Subject998 characters
Send rate60 sends per minute per API key
Long-poll waitwait_seconds up to 60
Ephemeral TTL60 seconds to 30 days
Inbound message sizeup to 25 MiB accepted; bodies stored, attachment content not stored during the beta (metadata only)
List page sizelimit 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.

POST/v1/inboxesinboxes:write
FieldTypeDescription
local_partstring?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_namestring?Friendly From name used on sends from this inbox
kindstring?standard (default) or ephemeral
ttl_secondsint?Ephemeral only: 60 to 2592000. Default 3600. The inbox expires and stops receiving.
webhook_urlstring?HTTPS URL that receives a signed POST per inbound message
metadataobject?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"
}
GET/v1/inboxesinboxes:read

Lists inboxes, newest first. Query: limit (1-100, default 25), cursor (from next_cursor).

GET/v1/inboxes/:idinboxes:read

Fetches one inbox, including its webhook_secret if a webhook is configured.

PATCH/v1/inboxes/:idinboxes:write

Updates display_name, webhook_url, or metadata. Pass null to clear a field. Setting a new webhook URL rotates the webhook secret.

DELETE/v1/inboxes/:idinboxes:write

Soft 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

POST/v1/messagesmessages:send

from 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.

FieldTypeDescription
fromstringAddress of one of your active inboxes
from_namestring?Overrides the inbox display_name for this send
toaddress[]1 to 50 recipients: {"email": "...", "name": "?"}
cc, bccaddress[]?Count toward the 50-recipient total
reply_toaddress?Reply-To header
subjectstringUp to 998 characters
text, htmlstring?At least one required
headersobject?Custom headers (threading, X-Agent-Id). Core headers like From and Subject cannot be overridden.
attachmentsarray?{"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.

GET/v1/inboxes/:id/messagesmessages:read
QueryTypeDescription
directionstring?inbound or outbound; omit for both
sincestring?ISO 8601; only messages that arrived after this instant
wait_secondsint?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.

GET/v1/messages/:idmessages:read

Full 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));
}
Delivery is best effort in the beta: one attempt, 10 second timeout, no retries yet. Treat webhooks as a latency optimization and the list endpoints as the source of truth.

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.
GET/v1/billingbilling:read
{
  "credit_balance": 184,
  "unit": "message_credits",
  "ledger": [
    { "delta": -1, "reason": "send", "created_at": "..." },
    { "delta": 200, "reason": "signup_grant", "created_at": "..." }
  ]
}
POST/v1/billing/checkoutbilling:read

Returns 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.