---
name: pony
description: Send transactional email through Pony, and buy a plan with x402 when you do not have one. Use when an agent needs to send email, check delivery status, register a webhook, or resolve a 402 Payment Required from api.pony.email.
---

# Pony

A transactional email API you can buy access to without a human. If you have a
funded wallet, you can go from never having heard of this service to a delivered
email in one conversation.

Base URL: `https://api.pony.email`
Auth: `Authorization: Bearer <api key>`

**There is no SDK. Write the client yourself** — it is REST and JSON, and you
already speak the language you are working in.

## If you have no API key

Do not look for a signup form. There is none. Buy a plan:

1. `POST /emails` with no credentials. You get `402 Payment Required` and a
   `PAYMENT-REQUIRED` header.
2. Base64-decode that header. It is an x402 **version 2** document.
   `resource.url` is absolute, and on this refusal it names the catalogue,
   `https://api.pony.email/plans`. `accepts[]` carries one entry per plan on
   sale, giving `scheme` (`exact`), `network` (CAIP-2, `eip155:8453` for Base),
   `amount` (USDC atomic units, six decimals), `asset`, `payTo` and
   `maxTimeoutSeconds`. An entry names its plan by `amount` only, so match it
   against `GET /plans`.
3. Sign an EIP-3009 `transferWithAuthorization` for exactly that amount, to
   exactly that address.
4. `POST /plans/{plan}/purchase` for the plan you chose, with the base64
   payload in a `PAYMENT-SIGNATURE` header. That route is the only one that
   reads the header; sending it back to `/emails` buys nothing.

The response carries `api_key`, `plan`, `sends_included`, `expires_at`, a
`sending_address` you may send from immediately, and `transaction` — the
settlement's on-chain id, which is your receipt. Keep it. It is present and
empty in one case: an authorization we had already settled without learning the
hash. The plan is granted anyway, and the transfer in your own wallet is the
receipt.

**Retrying is safe and you should not fear it.** Payment is idempotent on
`(payer, nonce)`. Replaying the same signed authorization returns the original
result including the same API key — it does not charge again. If your first
request times out, send it again rather than signing a new authorization.

Legacy note: an `X-PAYMENT` header (x402 v1) is also accepted. Prefer v2.

## Sending

`POST /emails`

```json
{
  "from": "agent@t-<team-id>.agents.pony.email",
  "to": "ops@example.com",
  "subject": "Deploy finished",
  "html": "<p>Shipped.</p>"
}
```

- `from`, `to`, `subject`, and one of `html` or `text` are required.
- `to`, `cc`, `bcc` and `reply_to` each accept a string or an array of strings.
- At most 50 recipients across `to`, `cc` and `bcc`.
- `POST /emails/batch` takes up to 100 messages as a bare JSON array.
- Attachments are **not** supported. Send a link.

Send an `Idempotency-Key` header on anything you might retry. It is honoured for
24 hours. Reusing a key with different content is a `409`.

## Reading

- `GET /emails/{id}` — status, attempts, last error, last event.
- `GET /emails` — filters: `limit` (1–100), `status`, `to`, `subject`,
  `before`, `since`.
- `GET /emails/{id}/events` — the append-only event log.
- `GET /usage` — sends spent against the current plan window.
- `GET /plans` — the catalogue, unauthenticated, priced in USDC.

## Plans are windows, not subscriptions

A plan is a fixed number of sends over a fixed 30-day window. **Nothing
auto-renews and there is no overage.** When the sends run out or the window
ends, you get `402` with a fresh quote — pay it the same way you paid the first
one.

| plan_id | Sends per 30 days | Daily cap | Price |
|---|---|---|---|
| `free` | 3,000 | 100 | not sold, and not granted at launch |

`free` is listed by `GET /plans` with a null price and cannot be obtained.
Buy `pro` or `scale`.
| `pro` | 50,000 | none | 19.00 USDC |
| `scale` | 100,000 | none | 79.00 USDC |

## Sending identity

A purchase provisions a verified identity at
`agent@t-<team-id>.agents.pony.email`, and returns it as `sending_address`.
`<team-id>` is the whole `team_id` from that same response — a UUID, not a
prefix of one — so the host is `t-` followed by it. Send the address back
verbatim rather than rebuilding it.

Your first send needs no DNS. The identity carries a volume ceiling. For real
volume, add a domain you control with `POST /domains` and publish the four
records it returns.

## MCP

`https://api.pony.email/mcp` mounts this API as MCP tools. Everything below
is also reachable over plain REST; the tools are adapters over the same code,
so a rule enforced there is enforced here.

Transport: streamable HTTP, **stateless, JSON responses**. Every POST is
answered with one `application/json` body. There is no SSE stream and no
`Mcp-Session-Id`. `GET` and `DELETE` answer `405` with an `Allow` header, which
is the specification's stateless mode rather than a limitation.

**Two rules a hand-written client gets wrong:**

1. Send `Accept: application/json, text/event-stream` on every POST — both
   types, even though only JSON comes back. The specification requires it and
   the transport enforces it. One type alone is a `400`.
2. Present the key as an `Authorization: Bearer <api key>` header on the HTTP
   request carrying the tool call. There is no other way: not the URL, not a
   query parameter, not a tool argument. **If your client cannot attach a
   custom header, you can call `get_plans` and `buy_plan` and nothing else.**

| Tool | API key | What it does |
|---|---|---|
| `get_plans` | not needed | Every plan with the full x402 terms for each purchasable one — `scheme`, `network`, `asset`, `payTo`, `amount`, `maxTimeoutSeconds`. The same terms a 402 carries |
| `buy_plan` | not needed | Buys a plan with a signed x402 payment and returns an API key. With no signature it returns that plan's terms. Presenting a key renews the team that holds it instead of creating a second. Idempotent on `(payer, nonce)` |
| `send_email` | required | Sends one email, returns the message id. An exhausted or expired plan fails with `payment_required` and the terms to buy another |
| `get_email` | required | One message's delivery record: status, attempts, last error, last event. Not the body — this answers where a message got to, not what it said |
| `get_usage` | required | Sends spent against the current window and when it ends. No active plan answers `payment_required` with a quote rather than zeroes |

`get_plans` and `buy_plan` sit outside authentication on purpose: a buyer has
no key yet, and those two are how it gets one. Call `get_plans`, sign an
EIP-3009 `transferWithAuthorization` for the plan you want, then call
`buy_plan` with the base64 payload.

A failed tool call returns `isError` with structured content shaped
`{"error": {"statusCode", "message", "name"}}` — the same names as the HTTP
errors below. Branch on `name`, never on the message. A `payment_required`
carries `error.payment_required`, the x402 document naming what to pay.

## Errors

Every error body is `{"statusCode", "message", "name"}`. **Branch on `name`,
never on the message text.**

| name | Status | What to do |
|---|---|---|
| `missing_api_key` | 401 | Send `Authorization: Bearer …` |
| `invalid_api_key` | 403 | The key is revoked or wrong |
| `validation_error` | 400/422 | Fix the field named in the message |
| `missing_required_field` | 422 | Add the field |
| `invalid_idempotent_request` | 409 | Same key, different body. Use a new key |
| `daily_quota_exceeded` | 429 | Wait. `Retry-After` is whole seconds |
| `payment_required` | 402 | Pay. The `PAYMENT-REQUIRED` header has the quote |
| `invalid_payment` | 400/402 | The payload was malformed or refused |

**402 and 429 are different and the difference matters.** A 429 means wait — the
daily cap resets. A 402 means pay — nothing refills on its own, and waiting will
never fix it.

## Webhooks

`POST /webhooks` registers an endpoint and returns a signing secret **once**.
Payloads carry `Pony-Signature: t=<unix>,v1=<hmac-sha256>`. Events are
`delivered`, `bounced`, `complained`, `opened` and `rejected`. Delivery is
at-least-once with a dead-letter queue — deduplicate on `Pony-Delivery-Id`.

## Not supported

Attachments, open and click tracking, templates, contact lists, audiences. This
is a transactional send API only.
