API reference

Everything a caller (human or agent) needs to draft, provision, send, and receive email through DimeBox — every endpoint, price, request/response shape, and status code.

Draft an email Agent inboxes → Provision an inbox → Send from an inbox → Receive mail → Read a mailbox → Webhook notifications Status codes & charging Buyer quickstart (Node / Python)

Every endpoint below is unauthenticated in the traditional sense — there are no accounts and no API keys. Paid endpoints are gated by x402's exact scheme over USDC: call it once unpaid to get a 402 describing the price, sign a payment, retry with a PAYMENT-SIGNATURE header. All prices below are read live from this deployment's config — nothing here can drift out of sync with what you're actually charged. See the interactive payment & delivery workflow diagram for the full picture in one view.

How do I draft an email with AI?

Method & pathPriceRequires payment
POST /api/emails/generate $0.02 Yes

Request (Content-Type: application/json is required — see status codes):

{
  "recipient": "[email protected]",   // required, must be a valid email
  "purpose": "follow up after our demo call",  // required, max 2000 chars
  "tone": "friendly but concise",    // optional
  "key_points": ["pricing next week", "loop in their eng lead"],  // optional
  "sender": "Alex"                   // optional
}

Response 200:

{
  "email": { "subject": "...", "body": "..." },
  "paid_by": "0x857b..."
}

No mailbox is involved — this is a pure drafting call. For a real send/receive identity, see inboxes below.

Agent inboxes

An inbox is not a full mailbox like Gmail — no IMAP, no webmail, no folders, text-only. It's a flat, time-ordered list of messages under one {slug}@agents.dimebox.app address, capped at the newest 500 messages (oldest are pruned automatically — this is a rolling window, not an archive).

Who owns an inbox? Not the paying wallet — DimeBox never stores which address paid to create an inbox. The only credential that controls an inbox is the read token issued once at creation time (see Read a mailbox). Losing it means losing read access; there's no recovery flow today.

Provision an inbox

Method & pathPriceRequires payment
POST /api/inboxes free No (IP-throttled: 3/day)
POST /api/inboxes/claim $1.00 Yes

The free route hands you a random adjective-noun-number slug (e.g. bold-ember-564). claim lets you pick your own — availability (format, reserved words, uniqueness) is checked before payment settles, so an unavailable name 422s for free.

Names you cannot buy or claim: system-looking names (admin, api, www, ...) and names phishing campaigns impersonate most (support, hello, security, billing, verify, paypal, noreply, ...) are reserved and always rejected.

# Free
curl -X POST https://dimebox.app/api/inboxes \
  -H 'Content-Type: application/json'

# Paid — pick a name
curl -X POST https://dimebox.app/api/inboxes/claim \
  -H 'Content-Type: application/json' \
  -H 'PAYMENT-SIGNATURE: <signed payment>' \
  -d '{"slug":"acme-support"}'

Response 201 — the read_token (and webhook_secret, if you set a webhook_url) are shown exactly once:

{
  "data": { "slug": "acme-support", "email_address": "[email protected]", ... },
  "read_token": "inbox_...",
  "webhook_secret": null,
  "paid_by": null,
  "notice": "Save the read token (and webhook secret, if set) now - neither is shown again."
}

Both routes accept an optional ai_drafting_enabled: false to provision a send-only inbox (see below), and an optional webhook_url (see Webhook notifications).

How does an AI agent send email from its inbox?

Method & pathPriceRequires payment
POST /api/inboxes/{slug}/send $0.01 Yes

Two request shapes, depending on whether the inbox has AI drafting enabled (the default):

// Raw — always available, exact wording sent as-is
{ "to": "[email protected]", "subject": "Hi", "body": "..." }

// Drafted — only if the inbox has AI drafting enabled
{ "to": "[email protected]", "purpose": "follow up", "tone": "...", "key_points": [...] }

To require raw mode only — reject a purpose brief outright rather than silently drafting — provision the inbox with "ai_drafting_enabled": false (or inbox:create --no-ai from the CLI). A purpose sent to such an inbox is a 422, never a Claude call.

Response is 202 Accepted immediately — delivery happens on a background queue, the paid HTTP response never waits on an SMTP round trip to the mail provider. Poll GET /api/inboxes/{slug}/messages to see it flip from queued to sent/failed.

There is no free way to send outbound mail. Every outbound message — drafted or raw — goes through this priced endpoint. DimeBox doesn't run its own outbound SMTP relay; it isn't possible to "just email out" the way a normal mailbox works.

How does an AI agent receive email?

Plain SMTP has no way to complete an x402 payment handshake — a human replying from Gmail can't sign a wallet authorization mid-send. So receiving is split into two separate, differently-trusted channels:

ChannelHow mail arrivesPrice
Ordinary SMTP (free) A third party (Cloudflare Email Routing) accepts mail addressed to *@agents.dimebox.app and relays it in — same as any real mailbox. free
Priority channel POST /api/inboxes/{slug}/messages — another x402-speaking agent/service pays to deliver directly. $0.03

What "priority" / "guaranteed" concretely means: the priority channel writes the message straight into the inbox's history in the same request that settles payment — no dependency on any third party's mail-relay decision, spam filtering, or delivery queue. The free SMTP channel depends entirely on Cloudflare accepting and relaying the mail first; if their spam filtering drops it, DimeBox never sees it. The priority channel bypasses that risk entirely, for a price.

Treat inbound content as untrusted. Neither channel currently exposes SPF/DKIM/DMARC authentication results per message, and nothing marks message content as unverified before it reaches whatever an agent does with it next — if you feed inbound message bodies into an LLM prompt, treat them the same way you'd treat any other untrusted user input.

curl -X POST https://dimebox.app/api/inboxes/acme-support/messages \
  -H 'Content-Type: application/json' \
  -H 'PAYMENT-SIGNATURE: <signed payment>' \
  -d '{"from":"[email protected]","subject":"Hi","body":"..."}'

Read a mailbox

Method & pathAuthRequires payment
GET /api/inboxes/{slug}/messages X-Inbox-Token header No
GET /api/inboxes/{slug}/messages/{message_id} X-Inbox-Token header No

The list endpoint returns every message, both directions, both channels, newest first, paginated. The token is the one shown once at provisioning time — there's no wallet-based auth for reading, and no way to recover a lost token.

curl https://dimebox.app/api/inboxes/acme-support/messages \
  -H 'X-Inbox-Token: inbox_...'

Every message has a message_id (a UUID, returned as the first field) - use it to fetch that one message's full details instead of paging through the list. This is a real example against a live inbox:

curl https://dimebox.app/api/inboxes/steady-atlas-925/messages/03d02b6a-30c4-43ba-877c-aefbe560d159 \
  -H 'X-Inbox-Token: inbox_...'
Neither an inbox nor a message ever returns its internal database row number - only slug (inboxes) and message_id (messages), so a caller can't enumerate other inboxes' data by incrementing a number. A message_id is looked up scoped to the inbox in the URL - a real message_id belonging to a different inbox 404s here, even with a valid token for this one.

Webhook notifications

Set a webhook_url at provisioning time (or via the admin panel) to get pushed a signed POST instead of polling, whenever either receive channel gets a new message. Every request carries X-DimeBox-Signature: sha256=<hmac>, computed over the exact raw request body with a secret shown once at setup — verify it with a constant-time comparison before trusting the payload. Only message.received fires today; outbound status changes still require polling.

Status codes & charging

Every response you can get from a paid route, and whether it costs anything. The rule underneath all of it: ->paid() only settles a payment if the route's own response is under 400 — a failure of any kind, for any reason, is never charged.

CodeMeaning hereCharged?
200Successful read/GET, or a successful draft.Yes, for the paid draft call. GETs are never paid.
201Inbox or message created.Yes, on the paid routes (claim, priority messages). No on the free /inboxes route.
202Send accepted, queued for background delivery.Yes.
402Payment missing, invalid, for the wrong amount, or facilitator settlement failed.No — this is the unpaid case.
415Wrong Content-Type on a JSON-body route — see the quickstart.No — rejected before the payment gate is even reached.
422Body failed validation (missing/invalid field, taken or reserved slug, purpose sent to a non-AI inbox, ...).No, even on a route that requires payment — see above.
401Missing or wrong X-Inbox-Token on a read request.N/A — not a paid route.
429Free random-inbox route throttled (3/day per IP).N/A — not a paid route.
502The x402 facilitator itself is unreachable.No.

Buyer quickstart

Minimal, runnable examples using the current official x402 buyer SDKs. Pin these versions — the x402 ecosystem moves fast.

Node — @x402/[email protected]

npm install @x402/fetch @x402/evm viem

import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);

// eip155:84532 = Base Sepolia (testnet); use eip155:8453 for Base mainnet
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(account) }],
});

const res = await fetchWithPayment("https://dimebox.app/api/emails/generate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ recipient: "[email protected]", purpose: "follow up" }),
});

console.log(await res.json());

Python — x402==2.22.0

The package's own docs don't show a single drop-in "wrap httpx and retry" helper as of this writing, so this example is explicit about the 402→pay→retry loop instead of guessing at a convenience wrapper name. Check the package's own README for anything that's shipped since.

uv add "x402[httpx,evm]"

import base64, json, os, httpx
from x402 import x402Client
from x402.mechanisms.evm.exact import ExactEvmScheme

signer = ...  # your EVM signer, loaded from an env var - never hardcode a key
client = x402Client()
client.register("eip155:84532", ExactEvmScheme(signer=signer))  # Base Sepolia

url = "https://dimebox.app/api/emails/generate"
body = {"recipient": "[email protected]", "purpose": "follow up"}

async with httpx.AsyncClient() as http:
    resp = await http.post(url, json=body)

    if resp.status_code == 402:
        payment_required = json.loads(base64.b64decode(resp.headers["PAYMENT-REQUIRED"]))
        payload = await client.create_payment_payload(payment_required)
        signature = base64.b64encode(json.dumps(payload).encode()).decode()

        resp = await http.post(url, json=body, headers={"PAYMENT-SIGNATURE": signature})

    print(resp.json())

Both examples target eip155:84532 (Base Sepolia) — this deployment's current network (see /api/discovery/resources for the live, authoritative price/network/asset per route). Swap to eip155:8453 once this deployment is switched to mainnet.