# API reference

Base URL: `https://www.mailbypony.com/api/v1`. The machine-readable contract is [`/api/v1/openapi.json`](/api/v1/openapi.json).

Each endpoint lists its MCP tool name and the [scope](/docs/authentication.md) it needs. HTTP and MCP accept the same fields. Over HTTP, send the idempotency key in the `Idempotency-Key` header; over MCP, send it as the `idempotency_key` argument.

Errors use one format for every endpoint. See [Errors](/docs/errors.md).

| Group | Endpoints |
|---|---|
| [Account](#account) | `GET /catalog`, `GET /identity`, `GET /address-references` |
| [Drafts](#drafts) | `GET /drafts`, `POST /drafts`, `GET /drafts/{id}`, `PATCH /drafts/{id}`, attachments, artwork, archive |
| [Proofs](#proofs) | `POST /drafts/{id}/proofs`, `GET /proofs/{id}` |
| [Credits](#credits) | `GET /wallet`, `GET /wallet/transactions`, `POST /wallet/topups`, `GET /wallet/topups/{id}` |
| [Sending](#sending) | `GET /agent-grants`, `POST /send-quotes`, `POST /orders` |
| [Orders](#orders) | `GET /orders`, `GET /orders/{id}`, `POST /orders/{id}/cancel` |
| [Templates](#templates) | `GET /templates`, `POST /templates`, `GET /templates/{id}`, `DELETE /templates/{id}`, `POST /templates/{id}/drafts` |
| [Batches](#batches) | `POST /batches`, `GET /batches/{id}` |
| [Webhooks](#webhooks) | `GET /webhooks`, `POST /webhooks`, `DELETE /webhooks/{id}`, deliveries, test |
| [Requests](#requests) | `GET /requests/{operation}/{key}` |
| [Test mode](#test-mode) | `POST /test/credits`, `POST /test/orders/{id}/advance` |

---

## Account

### GET /catalog

`catalog` · No authentication

Returns the options and limits you can use: `papers`, `envelopes`, `styles`, `pricing`, `limits` and `prepaid`. Use the `id` values it returns when creating drafts.

`prepaid` includes `minimum_topup_minor`, `maximum_topup_minor`, `quote_lifetime_seconds` and `environment`.

### GET /identity

`get_connection_identity` · `letters:read`

Returns the credential's own `client_id`, `credential_type` (`oauth` or `api_key`), `scopes` and `environment` (`live` or `test`). An assistant's `client_id` is what you enter in **Let my assistant send letters**.

### GET /address-references

`list_address_references` · `addresses:read` or `letters:read`

Lists addresses you've confirmed on the website in the last 30 days.

Each item has `id`, `role` (`sender` or `recipient`), `masked` (city, state and country), `draft_id` and `expires_at`. Use `id` as a draft's `sender_id` or `recipient_id`.

---

## Drafts

A draft response includes `draft_id`, `version`, `title`, `content_type`, `style`, `paper`, `envelope`, masked `addresses`, `address_validation`, `attachments` and `edit_url`.

### GET /drafts

`list_drafts` · `letters:read`

Lists your saved drafts as `items`.

### POST /drafts

`create_draft` · `letters:write` · Idempotency key required

Creates a draft. Nothing is ordered or charged.

| Field | Type | Notes |
|---|---|---|
| `content_policy_acknowledged` | boolean, required | Must be `true`: confirms the letter is ordinary correspondence |
| `title` | string | Up to 150 characters |
| `content_type` | string | `text` (default), `markdown` or `pdf` |
| `text` | string | Up to 30,000 characters |
| `style` | string | A `styles` ID from the catalog. Default `serif`. |
| `paper` | string | A `papers` ID. Default `classic`. |
| `envelope` | string | An `envelopes` ID. Default `white`. |
| `recipient`, `sender` | object | A US address: `name`, `line1`, `line2`, `city`, `state`, `postal_code`, `country` |
| `recipient_id`, `sender_id` | string | A confirmed [address reference](#get-address-references), instead of a full address |

For a PDF letter, create the draft with `content_type: "pdf"`, then [upload the file](#post-draftsdraft_idattachments).

### GET /drafts/{draft_id}

`get_draft` · `letters:read`

Returns one draft, including its `text` and `pages`.

### PATCH /drafts/{draft_id}

`update_draft` · `letters:write` · Idempotency key required

Edits a draft. Send the current `version` and only the fields you're changing; it accepts the same fields as `POST /drafts`. Returns the draft with a new `version`. If the draft changed since you read it, you get `VERSION_CONFLICT`.

Any edit makes earlier proofs and quotes stale.

### POST /drafts/{draft_id}/attachments

`upload_attachment` · `letters:write` · Idempotency key required

Uploads a print-ready PDF, a photo or PNG artwork. Returns the draft with a new `version`.

| Field | Type | Notes |
|---|---|---|
| `version` | integer, required | The draft's current version |
| `kind` | string, required | `pdf`, `photo` or `decoration` |
| `name` | string, required | The file name |
| `content_base64` | string, required | The file, base64-encoded. Up to the catalog's `max_upload_bytes` (10 MB). |
| `content_policy_acknowledged` | boolean, required | Must be `true` |

PDFs print exactly as uploaded. They must be US Letter, 1 to 10 pages and flattened: no forms, scripts, attachments or links. Upload the original file rather than extracting its text.

### DELETE /drafts/{draft_id}/attachments/{file_id}

`remove_attachment` · `letters:write` · Idempotency key required

Removes an attachment and any artwork placed from it. Body: `{"version": 3}`.

### POST /drafts/{draft_id}/artwork

`add_library_artwork` · `letters:write` · Idempotency key required

Adds decorative artwork from Pony's library. Body: `version` and `artwork_id`.

### POST /drafts/{draft_id}/archive

`archive_draft` · `letters:write` · Idempotency key required

Archives a draft. You can restore it on the website. Body: `{"version": 3}`.

---

## Proofs

### POST /drafts/{draft_id}/proofs

`preview` · `letters:read` · Idempotency key required

Renders the exact pages and envelope for a draft version. Body: `{"version": 1}`.

Returns `proof_id`, `status` (`ready`), `version`, `artifact_hash`, `errors` and `review_url`, a link to view the proof on the website. Rendering finishes before the response returns. If `errors` isn't empty (for example, a missing ZIP code), fix the draft and render again.

### GET /proofs/{proof_id}

`get_proof` · `letters:read`

Returns a proof you've rendered. Proofs never change.

---

## Credits

### GET /wallet

`get_credit_balance` · `wallet:read`

Returns `balance_minor`, `available_minor`, `currency`, `status`, `as_of` and `environment`. Spend only what `available_minor` shows.

### GET /wallet/transactions

`list_credit_transactions` · `wallet:read`

Lists credit history, newest first.

| Query | Notes |
|---|---|
| `limit` | 1 to 100. Default 50. |
| `cursor` | The `next_cursor` from the previous page |

Each item has `id`, `amount_minor` (negative for spending), `currency`, `type`, `created_at` and related order or top-up IDs. `next_cursor` is `null` on the last page.

### POST /wallet/topups

`create_credit_topup` · `wallet:topup` · Idempotency key required

Creates a Stripe Checkout link to add credits. It never charges a card on its own; a person has to pay at the link. Not available to test keys.

```json
{"amount_minor":2500,"currency":"USD"}
```

Returns `topup_id`, `status`, `checkout_url`, `expires_at`, `amount_minor` and `expected_credit_minor`.

### GET /wallet/topups/{topup_id}

`get_credit_topup` · `wallet:read`

Returns a top-up's `status`: `pending`, `credited`, `failed` or `expired`. If you lost the response to `POST /wallet/topups`, this finds the original Checkout session rather than creating a new one.

---

## Sending

### GET /agent-grants

`list_agent_grants` · `grants:read`

Lists the [sending permissions](/docs/authentication.md) that apply to this credential. Each includes its limits, `active`, `remaining_daily_minor`, `remaining_lifetime_minor`, `remaining_orders` and `next_reset_at`.

### POST /send-quotes

`quote_for_send` · `orders:send` · Idempotency key required

Gets the final price for one proof.

| Field | Type | Notes |
|---|---|---|
| `draft_id` | string, required | |
| `version` | integer, required | The proof's draft version |
| `proof_id` | string, required | |
| `client_reference` | string, required | Your identifier for this mailing |
| `send_intent_id` | string | Include when re-quoting a mailing you haven't sent |

Returns `quote_id`, `send_intent_id`, `proof_id`, `proof_hash`, `subtotal_minor`, `tax_minor`, `total_minor`, `currency` and `expires_at`. The quote expires after `catalog.prepaid.quote_lifetime_seconds`.

### POST /orders

`send_with_credits` · `orders:send` and an active sending permission · Idempotency key required

Sends a letter. Pony checks the permission and your balance, deducts the total and queues the letter, all in one step.

| Field | Type | Notes |
|---|---|---|
| `send_intent_id` | string, required | From the quote |
| `quote_id` | string, required | From the quote |
| `proof_id` | string, required | From the quote |
| `expected_total_minor` | integer, required | Must equal the quote's `total_minor` |
| `currency` | string, required | `USD` |
| `grant_id` | string | The sending permission to use. Leave it out to use your key's or assistant's newest active permission. |

Returns `order_id`, `send_intent_id`, `status` (`queued`), `payment_method` (`credits`), `debited_minor`, `currency`, `ledger_transaction_id` and `status_url`.

Save the request body and idempotency key before sending. If the response is lost, [look it up](#get-requestsoperationkey) instead of sending again.

---

## Orders

An order includes `order_id`, `status`, `client_reference`, `send_intent_id`, `created_at`, `mailed_at`, the masked `draft` and a `history` of status changes. See [order statuses](/docs/concepts.md).

### GET /orders

`list_orders` · `orders:read`

Lists your orders as `items`. Filter with `?client_reference=…`.

### GET /orders/{order_id}

`get_order` · `orders:read`

Returns one order and its current status.

### POST /orders/{order_id}/cancel

`cancel_order` · `orders:cancel` · Idempotency key required

Cancels an order before preparation starts. Body: `{"approved":true}` after explicit cancellation approval. The order's sending permission must allow cancellation.

Returns `order_id`, `status` (`cancelled`), `refund_status`, `credited_minor` and `currency`. The credits return to your balance once, even if you repeat the request. After preparation starts, you get `CANCELLATION_TOO_LATE`.

---

## Templates

A template is a reusable letter with `{{placeholders}}`. See [Templates](/docs/templates.md) for the placeholder rules.

| Request | MCP | Scope | Notes |
|---|---|---|---|
| `POST /templates` | `create_template` | `letters:write` | Idempotency key required. Takes `name`, `title`, `content_type` (`text` or `markdown`), `text` or `pages`, `style`, `paper`, `envelope`, an optional default `sender` and `content_policy_acknowledged`. Returns `template_id` and the `variables` it uses. |
| `GET /templates` | `list_templates` | `letters:read` | |
| `GET /templates/{template_id}` | `get_template` | `letters:read` | |
| `DELETE /templates/{template_id}` | `delete_template` | `letters:write` | Idempotency key required. Drafts and orders made from it don't change. |
| `POST /templates/{template_id}/drafts` | `create_template_draft` | `letters:write` | Idempotency key required. Takes `variables`, a `recipient` or `recipient_id`, and optionally `sender`, `sender_id` or `title`. Returns an ordinary draft. |

---

## Batches

### POST /batches

`create_batch` · `letters:write` and `orders:send` · Idempotency key required

Sends up to 100 letters from your balance in one request. Each item has a unique `client_reference` and either a `template_id` with `variables`, or a `draft_id`. Each item is drafted, proofed, quoted and sent on its own, and one item's failure doesn't stop the others.

Returns `batch_id`, `status`, a `summary` (`requested`, `queued`, `failed`, `pending`, `debited_minor`) and a result for each item. Allow about a second per item. See [Batch sends](/docs/batches.md) for the full request and retry rules.

### GET /batches/{batch_id}

`get_batch` · `orders:read`

Returns a batch's stored result.

---

## Webhooks

HTTP only. Scope `webhooks:write`. These routes don't take an idempotency key. See [Webhooks](/docs/webhooks.md).

| Request | Notes |
|---|---|
| `POST /webhooks` | Takes `url`, and optionally `events` and `description`. Returns the endpoint with its `secret`, shown once. |
| `GET /webhooks` | Lists endpoints, without secrets |
| `DELETE /webhooks/{endpoint_id}` | Removes an endpoint and cancels its pending deliveries |
| `GET /webhooks/{endpoint_id}/deliveries` | The 50 most recent deliveries |
| `POST /webhooks/{endpoint_id}/test` | Sends a signed `ping` |

---

## Requests

### GET /requests/{operation}/{key}

`request_status` · The scope of the original request

Looks up the result of an earlier write. `operation` is the original request's MCP tool name, such as `send_with_credits`, and `key` is its idempotency key.

Returns `state` (`committed`, `rejected` or `processing`) with the original `result` or `error`. Returns `404 REQUEST_NOT_FOUND` if Pony never recorded the request. See [Recovering from a lost response](/docs/concepts.md).

---

## Test mode

These endpoints work only with a `pony_test_` key. See [Test mode](/docs/test-mode.md).

| Request | Scope | Notes |
|---|---|---|
| `POST /test/credits` | `wallet:read` | Idempotency key required. Adds up to $1,000 of test credit per call: `{"amount_minor": 50000, "currency": "USD"}` |
| `POST /test/orders/{order_id}/advance` | `orders:read` | Moves a test order forward one step, or up to the `status` you send |
