API docs and quickstart
The whole REST surface on one page: how to send, what comes back when a send is refused, and how a client gets a key.
Send one email
The base url is https://api.pony.email, and every authenticated call carries Authorization: Bearer <api key>. A send needs four things: from, to, subject, and one of html or text.
curl https://api.pony.email/emails \
-H "Authorization: Bearer $PONY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "agent@t-<team-id>.agents.pony.email",
"to": "you@example.com",
"subject": "First send",
"text": "Sent with Pony."
}'The from address is the one a purchase hands back, described under sending identity below. If you have no key yet, an agent can buy its own plan and get a key and a sending address in the same response.
The same content, written for a model rather than for a person, is at /llms.txt.
The rest of the send surface
POST /emails—from,to,subject, and one ofhtmlortext.to,cc,bccandreply_toeach accept a string or an array.POST /emails/batch— up to 100 messages.GET /emails/{id}— status, attempts, last error, last event.GET /emails— the list, filtered bylimit,status,to,subject,beforeandsince.GET /emails/{id}/events— the append-only event log.GET /usage— sends spent against the current entitlement.
One message carries up to 50 recipients across to, cc and bcc.
Send an Idempotency-Key header on any send you might retry. A key is honoured for 24 hours. Reusing one with different content is a 409, named invalid_idempotent_request.
Sending identity
A purchase provisions a verified sending identity and returns it as sending_address. It has the shape agent@t-<team-id>.agents.pony.email, where <team-id> is the whole team_id from that response — a UUID, not a prefix of one — so the host is t- followed by it.
Use the address as returned rather than rebuilding it from the parts. The response already holds the string you need, and a rebuilt one is a string you have to keep correct.
The first send needs no DNS. The identity carries a volume ceiling, so for real volume add a domain you own with POST /domains and publish the four records it returns.
Errors
Every error body is {"statusCode", "message", "name"}. Branch on name, never on the message text. The notable names:
missing_api_key—401invalid_api_key—403validation_error—400or422missing_required_field—422invalid_idempotent_request—409daily_quota_exceeded—429payment_required—402
A 402 always carries a PAYMENT-REQUIRED header. It means the refusal is one you can fix by paying, and the header says exactly what to pay, which is how an agent buys its own plan without a human in the loop.
A 429 carries Ratelimit-Limit, Ratelimit-Remaining, Ratelimit-Reset and Retry-After. All four are whole seconds, not epochs.
Webhooks
POST /webhooks registers an endpoint and returns a signing secret once. Keep it on that first response.
Payloads are signed Pony-Signature: t=<unix>,v1=<hmac-sha256>. The events are delivered, bounced, complained, opened and rejected.
Delivery is at-least-once with a dead-letter queue, so an endpoint can see the same event more than once. Dedupe on Pony-Delivery-Id.
MCP
https://api.pony.email/mcp mounts this API as MCP tools. It is the streamable HTTP transport in stateless mode with JSON responses: every POST is answered with one application/json body, there is no SSE stream, and no Mcp-Session-Id is issued or expected. GET and DELETE answer 405 with an Allow header.
Send Accept: application/json, text/event-stream on every POST — both types, even though only JSON ever comes back. The specification requires it and the transport enforces it, so one type alone is a 400. This is the first thing a hand-written client gets wrong.
Authentication is the Authorization: Bearer <api key> header and nothing else — not the url, not a query parameter, not a tool argument.
get_plans, no key needed — every plan, with the full x402 terms for each purchasable one:scheme,network,asset,payTo,amountandmaxTimeoutSeconds.buy_plan, no key needed — buys a plan with a signed x402 payment and returns an API key. Called with no signature, it returns that plan’s terms.send_email— sends one email and returns the message id.get_email— one message’s delivery record: status, attempts, last error, last event. The body is deliberately not returned.get_usage— sends spent against the current window, and when that window ends.
A client that cannot attach a custom header can call get_plans and buy_plan, and is refused on the other three. Check that before you connect.
What this API does not do
Attachments, open and click tracking, templates, contact lists and audiences are not supported. This is a transactional send API only.
The FAQ covers what a plan buys and what happens when its volume runs out.