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": "you@yourdomain.com",
"to": "you@example.com",
"subject": "First send",
"text": "Sent with Pony."
}'The from address must be at a domain your team has verified, described under sending identity below. Create the key in the dashboard; it is shown once.
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
Every team sends from a domain it has verified itself, on every plan. Nothing is provisioned for you, so there is no address to send from until a domain is verified.
Add one with POST /domains and publish the four records it returns. Pony polls your registrar until they land, and from may be any address at that domain once it does.
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—403,401on/mcpvalidation_error—400or422missing_required_field—422invalid_idempotent_request—409daily_quota_exceeded—429payment_required—402insufficient_scope—403agent_token_not_allowed—403invalid_agent_token—401
The last three belong to tokens an agent obtained for itself over MCP. insufficient_scope means the token is live and was not granted that call, so retrying will not help until somebody approves a wider one. agent_token_not_allowed means such a token was presented to the REST API, which it never authorizes — that takes an API key. invalid_agent_token means the token no longer authenticates at all: unknown, expired, revoked, or minted for another audience. There is no refresh grant, so authorizing again is the whole remedy.
On /mcp, an unusable credential is 401 rather than 403, because 401 carries the header that tells a client where to authorize. missing_api_key means nothing was sent, invalid_api_key means a key was sent and does not resolve, and invalid_agent_token means the same of a token from that flow. The REST API has no such header to carry, so there invalid_api_key stays 403.
A 402 means the volume is spent or a plan’s window has ended. A paid plan clears it by renewing; before then the message names the billing page in the dashboard, and no retry and no header will do it.
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 its included sends, daily cap, window length and price in cents. The price is null on the free tier.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 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.