How it works

402, pay, retry.

HTTP has had a status code for this since 1997. The agent never fills in a form, never pastes a key, never waits for a human.

1

Call with no credentials

POST /v1/inboxes with no Authorization header. The API answers 402 Payment Required with a PAYMENT-REQUIRED header (x402) and, for inbox creation, a WWW-Authenticate: Payment challenge (Stripe MPP).

2

Pay inline

The agent signs a USDC transfer authorization for one of the offered networks, or presents a Stripe Shared Payment Token, and retries the same request with the payment attached in a header.

3

Get the result and a receipt

We verify the payment, run the request, settle, and answer 201 with the inbox, a PAYMENT-RESPONSE or Payment-Receipt header, and, on a wallet's first inbox, an API key.

the whole loop, in curl
# 1. Ask. No key.
curl -i -X POST https://api.deadsimple.email/v1/inboxes \
  -H "Content-Type: application/json" -d '{"display_name": "Paid Bot"}'

# HTTP/1.1 402 Payment Required
# PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwi...   (base64 JSON)
# WWW-Authenticate: Payment id="...", realm="api.deadsimple.email", method="stripe", intent="charge", expires="...", request="..."

# 2. Pay. Sign the USDC authorization for one of the "accepts" entries,
#    base64 the x402 payment payload, and retry with it attached.
curl -i -X POST https://api.deadsimple.email/v1/inboxes \
  -H "Content-Type: application/json" -d '{"display_name": "Paid Bot"}' \
  -H "PAYMENT-SIGNATURE: eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0ZWQiOnsic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoi..."

# 3. HTTP/1.1 201 Created
# PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6IjB4...   (base64 JSON)
# body: the inbox, plus credentials {api_key, ...} on this wallet's first purchase

Prices

Twelve priced routes.

Everything else on the API needs an API key. Prices are USD; x402 amounts are USDC with six decimals, so $1.00 is "1000000" and $0.005 is "5000".

RoutePriceUSDC atomicx402Stripe MPP
POST /v1/inboxes (create an inbox; returns an API key on first purchase)$1.001000000YesYes
POST /v1/inboxes/{inbox_id}/messages (send)$0.0110000YesNo
POST .../messages/{message_id}/reply$0.0110000YesNo
POST .../messages/{message_id}/reply-all$0.0110000YesNo
POST .../messages/{message_id}/forward$0.0110000YesNo
GET /v1/inboxes (list inboxes)$0.0055000YesNo
GET /v1/inboxes/{inbox_id} (get an inbox)$0.0055000YesNo
GET /v1/inboxes/{inbox_id}/messages (list messages)$0.0055000YesNo
GET .../messages/{message_id} (read a message)$0.0055000YesNo
GET /v1/inboxes/{inbox_id}/verification (latest OTP or magic link)$0.0055000YesNo
GET /v1/inboxes/{inbox_id}/threads (list threads)$0.0055000YesNo
GET .../threads/{thread_id} (read a thread)$0.0055000YesNo

Stripe MPP appears only on routes priced at or above Stripe's $0.50 minimum, which today means inbox creation. Every priced route also accepts an ordinary API key, and reads and sends within a plan are free with one.

Your wallet becomes your account

Pay per call is a door, not a meter. The payer address on an x402 payment keys a Pay as you go account. The first time a wallet pays for an inbox, the response carries credentials: an API key for that account, shown once. Every later paid request from the same wallet operates the same account, and so does the key.

  • Each paid inbox adds 1 inbox and 1,000 sends per month to the account.
  • With the key, reads and sends within those limits are free. Send Authorization: Bearer dse_... like any other client.
  • Paid sends bypass the monthly ceiling, so a wallet can always send by paying $0.01 even after the plan's sends are used up.
  • The key has the same scoped permissions as an agent-signup key: inboxes and messages, no domains, billing, team, or extra keys.
  • Claim the account with a verified email via POST /v1/auth/claim to move to a regular plan, lift the limits and add custom domains.
  • A wallet that has never bought an inbox has nothing to read, so a paid GET from an unknown wallet returns 402 with error.reason explaining that POST /v1/inboxes comes first.
201 on a wallet's first paid inbox
{
  "data": {
    "inbox_id": "6f1c2d3e-8a4b-4c5d-9e6f-0a1b2c3d4e5f",
    "email": "paid-bot_3e7a9c1d@box1.deadsimple.email",
    "display_name": "Paid Bot",
    "credentials": {
      "api_key": "dse_...",
      "key_id": "key_...",
      "account_id": "0b7d4a6e-2c19-4f8e-b3a1-5d6e7f8a9b0c",
      "note": "Use this key (Authorization: Bearer ...) for free reads and sends within the plan; or keep paying per request from the same wallet. Claim the account with a verified email via POST /v1/auth/claim to lift limits and add custom domains.",
      "warning": "Store this key now. It is shown once and cannot be retrieved again."
    },
    "payment": {
      "transaction": "0x9f3c...",
      "network": "eip155:8453",
      "payer": "0xAbCd...0001"
    }
  }
}

# Second paid inbox from the same wallet: same account, inbox_limit +1,
# no "credentials" block. With Stripe MPP, "payment" is {"reference": "pi_...", "method": "stripe"}.

x402: USDC on Base, Polygon or Solana

We speak x402 protocol version 2 and still accept version 1 clients. Scheme is exact; verification and settlement go through the PayAI facilitator (facilitator.payai.network), which is keyless, so there is nothing for you to register with either.

NetworkCAIP-2 id (v2)v1 nameUSDC contract or mint
Baseeip155:8453base0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Polygoneip155:137polygon0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Solana mainnetsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpsolanaEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

The 402

The PAYMENT-REQUIRED header is base64 JSON: an x402 v2 PaymentRequired object. The same fields are repeated in the JSON body for v1 clients, next to our usual error object. EVM entries carry the EIP-712 domain in extra; the Solana entry carries the facilitator's feePayer.

402 Payment Required
# headers
PAYMENT-REQUIRED: <base64 of the JSON below, minus our error object>
WWW-Authenticate: Payment id="...", realm="api.deadsimple.email", method="stripe", intent="charge", expires="...", request="..."
Cache-Control: no-store
Access-Control-Expose-Headers: PAYMENT-REQUIRED, PAYMENT-RESPONSE, WWW-Authenticate, Payment-Receipt

# body
{
  "error": {
    "code": "payment_required",
    "message": "This call costs $1.00 USD. Pay inline with x402 (USDC on Base, Polygon or Solana; retry with PAYMENT-SIGNATURE) or, for inbox creation, with Stripe MPP (retry with Authorization: Payment). Or send an API key: Authorization: Bearer dse_...",
    "price_usd": "1.00",
    "docs": "https://deadsimple.email/pay-per-request.html"
  },
  "meta": { "request_id": "..." },
  "x402Version": 2,
  "resource": {
    "url": "https://api.deadsimple.email/v1/inboxes",
    "description": "Dead Simple Email: Create an inbox (returns an API key on first purchase)",
    "mimeType": "application/json"
  },
  "accepts": [
    { "scheme": "exact", "network": "eip155:8453", "amount": "1000000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0x...",
      "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2" } },
    { "scheme": "exact", "network": "eip155:137", "amount": "1000000",
      "asset": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "payTo": "0x...",
      "maxTimeoutSeconds": 300, "extra": { "name": "USD Coin", "version": "2" } },
    { "scheme": "exact", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "amount": "1000000",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "payTo": "...",
      "maxTimeoutSeconds": 300, "extra": { "feePayer": "..." } }
  ],
  "extensions": {}
}

The retry and the receipt

Attach the signed payment payload as PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1), base64 encoded, on an otherwise identical request. We check that the chosen network, asset, payTo and amount match what we offered, ask the facilitator to verify, run the request, ask the facilitator to settle, and answer with the result plus PAYMENT-RESPONSE (and X-PAYMENT-RESPONSE).

retry headers and receipt
# request (v2)
PAYMENT-SIGNATURE: <base64 JSON>
{
  "x402Version": 2,
  "resource": { "url": "https://api.deadsimple.email/v1/inboxes", "...": "..." },
  "accepted": { /* the "accepts" entry you chose, verbatim */ },
  "payload": {
    "signature": "0x...",
    "authorization": { "from": "0xYourWallet", "to": "<payTo>", "value": "1000000",
                      "validAfter": "0", "validBefore": "...", "nonce": "0x..." }
  }
}

# request (v1): the same idea under X-PAYMENT, with "network": "base" and no "accepted"

# response
PAYMENT-RESPONSE: <base64 JSON>
X-PAYMENT-RESPONSE: <same>
{ "success": true, "transaction": "0x9f3c...", "network": "eip155:8453", "payer": "0xYourWallet" }

# a rejected payment is a fresh 402: the requirements are re-emitted and
# error.reason says why (amount mismatch, network not accepted, insufficient_funds, ...)

Clients

You do not have to build the loop yourself. The official x402 clients wrap an HTTP client so that a 402 is paid and retried transparently. Package APIs move; the snippets below show the shape, and the package READMEs are the authority on current signatures. The curl loop at the top of the page is the protocol itself if you would rather not depend on either.

agent.ts (@x402/fetch, viem, Base)
import { wrapFetchWithPayment } from "@x402/fetch"
import { createWalletClient, http } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { base } from "viem/chains"

const account = privateKeyToAccount(process.env.AGENT_WALLET_KEY as `0x${string}`)
const wallet = createWalletClient({ account, chain: base, transport: http() })
const fetchWithPay = wrapFetchWithPayment(fetch, wallet)   // pays 402s with USDC on Base

const r = await fetchWithPay("https://api.deadsimple.email/v1/inboxes", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ display_name: "Paid Bot" }),
})
const { data } = await r.json()
console.log(data.email, data.credentials?.api_key)   // store the key; it is shown once
agent.py (pip install "x402[evm,httpx]")
import os, httpx
from eth_account import Account
from x402.clients.httpx import x402_payment_hooks   # the httpx event hooks; see the x402 README

account = Account.from_key(os.environ["AGENT_WALLET_KEY"])

with httpx.Client(base_url="https://api.deadsimple.email") as client:
    client.event_hooks = x402_payment_hooks(account)   # 402 -> sign -> retry, transparently
    r = client.post("/v1/inboxes", json={"display_name": "Paid Bot"})
    data = r.json()["data"]
    print(data["email"], data.get("credentials", {}).get("api_key"))

Stripe MPP: pay with a card or a Link wallet

Agents that hold a Stripe Link wallet or a Shared Payment Token instead of crypto can pay with Stripe's Machine Payments Protocol. Two rules follow from how cards work. Stripe's minimum charge is $0.50, so the challenge is only offered on routes priced at or above that, which today means POST /v1/inboxes. And a card has no stable payer identity, so each card purchase creates its own Pay as you go account and returns an API key; use that key for everything after the first call.

The challenge

The same 402 carries a WWW-Authenticate: Payment header. request is base64url JCS JSON describing the charge; id is an HMAC over the challenge fields, so a tampered or expired challenge is refused before anything reaches Stripe.

WWW-Authenticate
WWW-Authenticate: Payment id="k9Qw...", realm="api.deadsimple.email", method="stripe", intent="charge",
                  expires="2026-09-11T14:10:00Z", request="eyJhbW91bnQiOiIxMDAiLCJjdXJyZW5jeSI6InVzZCIs..."

# request, decoded
{
  "amount": "100",                       // cents
  "currency": "usd",
  "description": "Dead Simple Email: Create an inbox (returns an API key on first purchase)",
  "methodDetails": { "paymentMethodTypes": ["card", "link"] }
}

The credential and the receipt

Retry with Authorization: Payment <credential>, where the credential is base64url JSON that echoes the challenge and carries a Shared Payment Token. We charge the token with a PaymentIntent (Stripe-Version 2026-07-29.preview), create the inbox, and return a Payment-Receipt header. The challenge lives five minutes and is good exactly once.

credential and receipt
# request
Authorization: Payment <base64url JSON>
{
  "challenge": { "id": "k9Qw...", "realm": "api.deadsimple.email", "method": "stripe", "intent": "charge",
                 "expires": "2026-09-11T14:10:00Z", "request": "eyJhbW91bnQiOiIxMDAi..." },
  "payload": { "spt": "spt_..." }
}

# response: 201 with the inbox, credentials and payment {"reference": "pi_...", "method": "stripe"}
Payment-Receipt: <base64url JCS JSON>
{ "amount": "100", "currency": "usd", "method": "stripe", "reference": "pi_...",
  "status": "success", "timestamp": "2026-09-11T14:06:12Z" }

If the inbox cannot be created after the card was charged, the PaymentIntent is refunded automatically and the request returns the error. A declined card is a fresh 402 with error.reason carrying Stripe's message, and nothing is created.

Clients

MPP clients handle the challenge, the token and the retry: mppx on npm and pympp on PyPI, and Stripe's own CLI for a one-off purchase from a terminal.

terminal
# Stripe Link CLI: fetch the URL, pay the challenge with your Link wallet, print the result
npx @stripe/link-cli mpp pay https://api.deadsimple.email/v1/inboxes

# In code: mppx (npm) or pympp (PyPI) wrap fetch / httpx the same way the x402 clients do.
# The response is the same 201 with credentials.api_key; keep the key for every call after.

Guarantees

  • Verify before work, settle after. An x402 payment is verified with the facilitator before the request runs, and settled only once the request has succeeded. A rejected payment does no work; a request that fails after verification is never settled, so you are not charged for our failures.
  • Refund on failure for cards. MPP has to charge first. If inbox creation then fails, the PaymentIntent is refunded automatically.
  • Requirements are checked against our own copy. The accepted entry you send back must match the network, asset, recipient and amount we offered; a mismatch is a 402, and the requirements are re-emitted so a client can recover.
  • Challenges are signed, short-lived and single use. An MPP challenge id is an HMAC over its fields, expires in five minutes, and is accepted exactly once. The Stripe call carries an idempotency key derived from the challenge and token, so a retried charge cannot double-bill.
  • Payer address is the identity. A wallet can only see and operate the inboxes it paid for. A wallet with no purchases gets 402 on every read, never someone else's data.
  • Unpriced routes are unchanged. Anything not in the price table still requires an API key and returns 401 without one.
  • Standard CORS exposure. PAYMENT-REQUIRED, PAYMENT-RESPONSE, WWW-Authenticate and Payment-Receipt are exposed to browser clients, and 402 responses are Cache-Control: no-store.

Prefer a key without a wallet? POST /v1/auth/agent-signup provisions a free trial account, key and live inbox in one unauthenticated call, no payment involved.

FAQ

Questions people ask first.

Creating an inbox costs $1.00. Sending, replying, reply-all and forwarding cost $0.01 per call. Listing inboxes, reading an inbox, listing and reading messages, extracting a verification code, and listing and reading threads cost $0.005 per call. Prices are in USD; x402 payments are made in USDC with six decimals, so $1.00 is 1000000 atomic units.

USDC on Base (eip155:8453), Polygon (eip155:137) and Solana mainnet through x402 with the exact scheme, verified and settled by the PayAI facilitator. Cards and Stripe Link wallets are accepted through Stripe's Machine Payments Protocol, but only on inbox creation because Stripe has a $0.50 minimum charge.

Yes. The first paid inbox creation from a wallet returns credentials in the response: an API key, key id and account id for a Pay as you go account keyed on that wallet. Each paid inbox adds one inbox and 1,000 sends per month to that account, so reads and sends within the plan are free with the key. You can keep paying per call from the same wallet instead, or claim the account with a verified email to lift limits.

With x402 the payment is verified before any work runs and settled only after the work succeeds, so a failed request is never charged. With Stripe MPP the card is charged first, and if inbox creation then fails the charge is refunded automatically.

Two reasons. Stripe's minimum charge is $0.50, and the only route priced at or above that is inbox creation at $1.00. And a card has no stable payer identity to key an account on, so a card purchase creates its own account and hands back an API key, which is the right credential for everything after the first call.

Yes. POST /v1/auth/agent-signup provisions a free trial account with an API key and a live inbox in one unauthenticated call. Pay per request is for agents that hold a wallet and want to skip signup entirely, or that need more than the trial tier allows.

One round trip to an inbox

Point a wallet at POST /v1/inboxes. Or take the free route and sign up in a click.

Get Started Free Read the walkthrough API Reference