# PieterPost API docs

Source: https://pieterpost.com/api/docs/

Create compose links, hosted checkout links, wallet-funded direct orders, temporary uploads, and order lookups from your backend. These docs cover the API surface only.

Base URL: `https://pieterpost.com`

Create requests: send JSON for orders and multipart form-data for uploads.

## Quick start

The fastest end-to-end test is a test key, a test wallet top-up, one direct-send request, and a status lookup.

- **Create a test key:** Sign in to `/api/dashboard`, create a `pp_test_...` key, and keep it server-side.
- **Add test wallet credits:** Use the dashboard button or call `POST /v1/credits/topups`. Test top-ups mark `paid` immediately.
- **Send a direct test order:** Call `POST /v1/orders` with `requestType: "letter"` or `requestType: "postcard"`.
- **Read the result:** Store the returned `orderId`, then inspect `GET /v1/orders/:orderId` for the latest status.

### Add test credits

Add credits with a test key. Test mode applies the top-up immediately.

**Request**

```bash
curl -X POST https://pieterpost.com/v1/credits/topups \
  -H "Authorization: Bearer pp_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: first-five-minutes-topup" \
  -d '{
  "amountCents": 2500,
  "currency": "eur",
  "metadata": {
    "source": "billing-settings"
  },
  "returnUrl": "https://example.com/topups/return"
}'
```

**Response**

```json
{
  "amountCents": 2500,
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "paidAt": "2026-04-21T10:00:01.000Z",
  "status": "paid",
  "testMode": true,
  "topupId": "topup_123",
  "updatedAt": "2026-04-21T10:00:01.000Z",
  "wallet": {
    "balanceCents": 2500,
    "currency": "eur",
    "mode": "test"
  }
}
```

### Send a test letter

Use those credits to create a direct-send letter with `POST /v1/orders`.

**Request**

```bash
curl -X POST https://pieterpost.com/v1/orders \
  -H "Authorization: Bearer pp_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: first-five-minutes-direct-send" \
  -d '{
  "letters": [
    {
      "message": "This direct-send letter is paid from wallet credits.",
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street"
      }
    }
  ],
  "locale": "en",
  "metadata": {
    "campaign": "spring"
  },
  "requestType": "letter",
  "senderEmail": "sender@example.com"
}'
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "credits",
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": null,
  "externalId": null,
  "fulfilledAt": "2026-04-21T10:00:05.000Z",
  "metadata": {
    "campaign": "spring"
  },
  "orderId": "ord_789",
  "orderRef": "7d5118b8-7a91-4e7a-8b92-c72bd46c8432",
  "paidAt": "2026-04-21T10:00:02.000Z",
  "paymentReference": "wallet:ledger_123",
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "fulfilled",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:05.000Z",
  "wallet": {
    "balanceCents": 2300,
    "currency": "eur",
    "mode": "test"
  }
}
```

### Check the order

Store the returned `orderId` and read it back when your application needs the latest status.

**Request**

```bash
curl https://pieterpost.com/v1/orders/ord_789 \
  -H "Authorization: Bearer pp_test_your_key_here"
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "credits",
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": null,
  "externalId": null,
  "fulfilledAt": "2026-04-21T10:00:05.000Z",
  "metadata": {
    "campaign": "spring"
  },
  "orderId": "ord_789",
  "orderRef": "7d5118b8-7a91-4e7a-8b92-c72bd46c8432",
  "paidAt": "2026-04-21T10:00:02.000Z",
  "paymentReference": "wallet:ledger_123",
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "fulfilled",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:05.000Z",
  "wallet": {
    "balanceCents": 2300,
    "currency": "eur",
    "mode": "test"
  }
}
```

## Authentication

All v1 endpoints use bearer-token authentication. Keep API keys on your server and use idempotency keys for JSON create requests.

```
Authorization: Bearer pp_test_your_key_here
Content-Type: application/json
Idempotency-Key: order-demo-001
```

- **Test keys:** Keys that start with `pp_test_` use sandbox checkout, simulated sending, and test wallet credits.
- **Live keys:** Keys that start with `pp_live_` can create live hosted checkout links, add live credits, and send live mail.
- **Idempotency:** Use a stable `Idempotency-Key` for each create operation. Retry with the same key after a timeout.

## MCP

PieterPost also exposes a remote MCP server for model clients that need API docs, account context, Mailbook contacts, uploads, checkout links, compose links, wallet credit top-ups, direct-send tools, and API key management. The server uses OAuth authorization code with PKCE, dynamic client registration, short-lived bearer tokens, and scoped `api:read api:send api:keys mailbook:read mailbook:write` access.

Docs page: `https://pieterpost.com/api/docs/mcp`

Server URL: `https://pieterpost.com/mcp/`

Markdown-only MCP docs: `https://pieterpost.com/api/docs/mcp.md`

OAuth issuer: `https://pieterpost.com/mcp/oauth`

Protected resource metadata: `https://pieterpost.com/.well-known/oauth-protected-resource/mcp/`

Authorization server metadata: `https://pieterpost.com/.well-known/oauth-authorization-server/mcp/oauth/`

Tools: `search`, `fetch`, `list_mailbook_contacts`, `get_mailbook_contact`, `create_mailbook_contact`, `update_mailbook_contact`, `delete_mailbook_contact`, `upload_asset`, `create_compose_link`, `create_checkout_link`, `create_direct_order`, `get_wallet`, `get_order`, `create_credit_topup`, `list_api_keys`, `create_api_key`, and `revoke_api_key`.

Use `upload_asset` before passing a square 1:1 custom postcard front image, letter attachments, or custom letter stamps into checkout or direct-order payloads. Live `create_direct_order` sends real mail from wallet credits and requires `confirmLiveSend: true` plus `maxAmountCents`.

`create_api_key` returns the secret once. Existing key secrets cannot be fetched later.

```json
{
  "type": "mcp",
  "server_label": "pieterpost",
  "server_url": "https://pieterpost.com/mcp/",
  "allowed_tools": [
    "search",
    "fetch",
    "upload_asset",
    "create_compose_link",
    "create_checkout_link",
    "create_direct_order",
    "get_wallet",
    "get_order",
    "create_credit_topup",
    "list_api_keys",
    "create_api_key",
    "revoke_api_key"
  ],
  "require_approval": {
    "never": {
      "read_only": true
    }
  }
}
```

## Modes

Pick the payment and fulfillment mode that matches the product flow you want to ship first.

- **Test keys:** `pp_test_...` keys use sandbox checkout, test credits, and simulated fulfillment. They do not send live mail.
- **Live hosted checkout:** `pp_live_...` keys create real hosted checkout links. PieterPost sends after payment succeeds. EUR checkout links offer both card and iDEAL by default. Use `paymentMethod: "card"` or `paymentMethod: "ideal"` only when you want to force one method.
- **Live direct send:** Direct orders use wallet credits. Live top-ups return a Stripe checkout URL, then `/v1/orders` sends live mail after the wallet has enough balance.

- **Test wallet:** A `pp_test_...` key uses a separate test wallet. Add credits in `/api/dashboard` or through `POST /v1/credits/topups`.
- **Live wallet:** A `pp_live_...` key can create hosted checkout links. Live top-ups return a Stripe checkout URL.
- **Direct send:** Live direct send uses the wallet balance from `GET /v1/wallet`; no separate approval step is required.

## Recipients and payloads

Letter and postcard requests share the same recipient shape, so you can reuse address data across flows.

- **Required fields:** `name`, `postalCode`, `city`, and `country` are expected. Provide either `streetAddress` or `street` plus `number`.
- **Optional fields:** `state`, `extra`, `addressLines`, and `customFields` are accepted. Some destinations require `state`.
- **Multiple recipients:** Letters use a `letters` array. Postcards use `postcard.recipient` for one recipient or `postcard.recipients` for a batch.
- **Postcard front image:** Postcards can use the default front image or an uploaded custom one. Custom postcard fronts must be square, with a 1:1 aspect ratio. To use a custom front, upload a `postcard-image` first and reuse that asset as `postcard.frontImageAssetId` or `postcard.frontImage`.
- **Template variables:** For bulk letters, send top-level `composeMode: "template"`, a shared `templateMessage`, optional `variableKeys`, and per-recipient `customFields`. For postcards, use `postcard.composeMode: "template"`, `postcard.message`, `postcard.variableKeys`, and recipient `customFields`. The API resolves placeholders for each recipient.
- **Hosted checkout payment:** Checkout links default to `paymentMethod: "auto"`. For EUR letter or postcard checkout links, auto offers both card and iDEAL in Stripe Checkout. Pass `paymentMethod: "card"` or `paymentMethod: "ideal"` only when you want to force one method.
- **Letter attachments:** Each letter can include uploaded `letter-attachment` assets. Supported files are PDF, PNG, and JPEG. Destination page limits apply to the combined message and attachment pages.
- **Letter stamps:** Upload `letter-stamp-image` assets and pass `stampImageAssetId` to use one custom stamp image for the letter order.
- **Letter markets:** US and non-US letter recipients cannot be combined in one letter order.
- **Limits:** Letter and postcard requests support up to 25 recipients. Letter messages are trimmed to 6,000 characters. Postcard messages are trimmed to 400 characters.

**Recipient object**

```json
{
  "city": "Springfield",
  "country": "United States",
  "name": "Pieter Post",
  "postalCode": "62704",
  "state": "IL",
  "streetAddress": "123 Main Street"
}
```

## Assets

Use temporary uploads when a postcard needs a square 1:1 custom front image, a letter needs image/PDF attachments, or a letter order needs a custom stamp image.

### Upload a postcard front image

Upload a square 1:1 `postcard-image` as PNG or JPEG, then pass the returned `assetId` as `postcard.frontImageAssetId` when you want a custom front.

**Request**

```bash
curl -X POST https://pieterpost.com/v1/uploads \
  -H "Authorization: Bearer pp_test_your_key_here" \
  -F "kind=postcard-image" \
  -F "file=@front.jpg"
```

**Response**

```json
{
  "assetId": "asset_123",
  "contentType": "image/jpeg",
  "kind": "postcard-image",
  "name": "asset_123.jpg",
  "originalName": "front.jpg",
  "publicUrl": "https://pieterpost.com/api/public-assets/asset_123",
  "size": 385421
}
```

### Upload a letter attachment

Upload a `letter-attachment` as PDF, PNG, or JPEG, then pass the returned `assetId` in a letter `attachments` array.

**Request**

```bash
curl -X POST https://pieterpost.com/v1/uploads \
  -H "Authorization: Bearer pp_test_your_key_here" \
  -F "kind=letter-attachment" \
  -F "file=@receipt.pdf"
```

**Response**

```json
{
  "assetId": "asset_letter_pdf_123",
  "contentType": "application/pdf",
  "kind": "letter-attachment",
  "name": "receipt.pdf",
  "originalName": "receipt.pdf",
  "publicUrl": null,
  "size": 148024
}
```

### Upload a letter stamp image

Upload a `letter-stamp-image` as PNG or JPEG, then pass the returned `assetId` as `stampImageAssetId`.

**Request**

```bash
curl -X POST https://pieterpost.com/v1/uploads \
  -H "Authorization: Bearer pp_test_your_key_here" \
  -F "kind=letter-stamp-image" \
  -F "file=@stamp.png"
```

**Response**

```json
{
  "assetId": "asset_stamp_123",
  "contentType": "image/png",
  "kind": "letter-stamp-image",
  "name": "stamp.png",
  "originalName": "stamp.png",
  "publicUrl": null,
  "size": 42192
}
```

### Use an attachment in a letter order

Letter attachments stay private and are validated server-side before pricing.

**Request**

```json
{
  "externalId": "invoice_attachments_123",
  "letters": [
    {
      "attachments": [
        "asset_letter_pdf_123"
      ],
      "message": "Thanks for your order. The receipt PDF is enclosed.",
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street"
      }
    }
  ],
  "locale": "en",
  "requestType": "letter",
  "returnUrl": "https://example.com/pieterpost-return",
  "senderEmail": "sender@example.com"
}
```

**Response**

```json
{
  "amountCents": 250,
  "billingMode": "hosted_checkout",
  "checkoutUrl": "https://pieterpost.com/v1/sandbox/checkout/ord_458",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": "2026-04-22T10:00:00.000Z",
  "externalId": "invoice_attachments_123",
  "fulfilledAt": null,
  "metadata": {},
  "orderId": "ord_458",
  "orderRef": "48c62242-5a2d-45a2-b073-7df1a8f64a36",
  "paidAt": null,
  "paymentReference": null,
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "checkout_open",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

The upload limit is 50 MB. Postcard images receive a temporary public URL because the print flow fetches the image later. Letter attachments and stamp images stay private.

## Endpoint reference

This is the current customer-facing API surface.

- `POST /v1/compose-links` - Create a secure composer URL with a prefilled recipient and message.
- `POST /v1/checkout-links` - Create a hosted payment link for a letter or postcard order.
- `POST /v1/orders` - Create a direct-send letter or postcard order paid from wallet credits.
- `GET /v1/orders/:orderId` - Fetch an order by id, including current status, payment reference, and errors.
- `GET /v1/wallet?currency=eur` - Read account capabilities and the wallet balance for a currency.
- `POST /v1/credits/topups` - Create a wallet top-up. Test mode credits the wallet immediately.
- `POST /v1/uploads` - Upload a temporary postcard image, letter attachment, or letter stamp image with an API key before creating an order.

### Create a compose link

`POST /v1/compose-links`

Create a PieterPost composer URL with a prepared recipient and optional message. The user can review before sending.

### POST /v1/compose-links

Send one recipient and an optional message. The response URL opens PieterPost with a secure draft token.

**Request**

```json
{
  "externalId": "lead_123",
  "locale": "en",
  "message": "Hi Pieter,\nWould you like to come by on Sunday?",
  "metadata": {
    "source": "crm"
  },
  "recipient": {
    "city": "Springfield",
    "country": "United States",
    "name": "Pieter Post",
    "postalCode": "62704",
    "state": "IL",
    "streetAddress": "123 Main Street"
  }
}
```

**Response**

```json
{
  "expiresAt": "2026-04-28T10:00:00.000Z",
  "id": "cl_123",
  "mode": "compose_link",
  "testMode": true,
  "url": "https://pieterpost.com/en/?draft=..."
}
```

### Create checkout links

`POST /v1/checkout-links`

Create a hosted checkout link. PieterPost collects payment and starts fulfillment after payment succeeds.

### Letter checkout

Send `requestType: "letter"` with a `letters` array.

**Request**

```json
{
  "externalId": "invoice_123",
  "letters": [
    {
      "message": "Thanks for your order. Your receipt is enclosed below.",
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street"
      }
    }
  ],
  "locale": "en",
  "metadata": {
    "customerId": "cus_123"
  },
  "requestType": "letter",
  "returnUrl": "https://example.com/pieterpost-return",
  "senderEmail": "sender@example.com"
}
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "hosted_checkout",
  "checkoutUrl": "https://pieterpost.com/v1/sandbox/checkout/ord_123",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": "2026-04-22T10:00:00.000Z",
  "externalId": "invoice_123",
  "fulfilledAt": null,
  "metadata": {
    "customerId": "cus_123"
  },
  "orderId": "ord_123",
  "orderRef": "5b5b8d3f-7b83-4a6f-9fb8-0c1c3f9b8d8c",
  "paidAt": null,
  "paymentReference": null,
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "checkout_open",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

### Bulk template letter checkout

For bulk template letters, send `composeMode: "template"` with a shared `templateMessage`, per-recipient `customFields`, and optional `stampImageAssetId`.

**Request**

```json
{
  "composeMode": "template",
  "externalId": "spring_campaign_123",
  "letters": [
    {
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street",
        "customFields": {
          "gift_code": "SPRING-10"
        }
      }
    },
    {
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Sam Example",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street",
        "customFields": {
          "gift_code": "SPRING-20"
        }
      }
    }
  ],
  "locale": "en",
  "requestType": "letter",
  "returnUrl": "https://example.com/pieterpost-return",
  "senderEmail": "sender@example.com",
  "stampImageAssetId": "asset_stamp_123",
  "templateMessage": "Hi {{first_name}},\nYour spring code is {{gift_code}}.",
  "variableKeys": [
    "gift_code"
  ]
}
```

**Response**

```json
{
  "amountCents": 460,
  "billingMode": "hosted_checkout",
  "checkoutUrl": "https://pieterpost.com/v1/sandbox/checkout/ord_457",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": "2026-04-22T10:00:00.000Z",
  "externalId": "spring_campaign_123",
  "fulfilledAt": null,
  "metadata": {},
  "orderId": "ord_457",
  "orderRef": "ac42e951-6323-4055-b82f-cb2e8b2847bc",
  "paidAt": null,
  "paymentReference": null,
  "recipientCount": 2,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "checkout_open",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

### Postcard checkout

Send `requestType: "postcard"` with a `postcard` object. This example uses `paymentMethod: "ideal"`.

**Request**

```json
{
  "locale": "en",
  "paymentMethod": "ideal",
  "postcard": {
    "frontImageAssetId": "asset_123",
    "message": "A small card from PieterPost.",
    "recipient": {
      "city": "Springfield",
      "country": "United States",
      "name": "Pieter Post",
      "postalCode": "62704",
      "state": "IL",
      "streetAddress": "123 Main Street"
    }
  },
  "requestType": "postcard",
  "returnUrl": "https://example.com/pieterpost-return",
  "senderEmail": "sender@example.com"
}
```

**Response**

```json
{
  "amountCents": 250,
  "billingMode": "hosted_checkout",
  "checkoutUrl": "https://pieterpost.com/v1/sandbox/checkout/ord_456",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": "2026-04-22T10:00:00.000Z",
  "externalId": null,
  "fulfilledAt": null,
  "metadata": {},
  "orderId": "ord_456",
  "orderRef": "8cfa9ca3-11ec-43b5-b16a-7c0c48b8d124",
  "paidAt": null,
  "paymentReference": null,
  "recipientCount": 1,
  "requestType": "postcard",
  "senderEmail": "sender@example.com",
  "status": "checkout_open",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

### Create direct orders

`POST /v1/orders`

Create a direct-send order paid from wallet credits. Test keys simulate the flow; live keys send real mail when the wallet has enough balance.

### Direct letter order

Use the same letter shape as hosted checkout, without `returnUrl`.

**Request**

```json
{
  "letters": [
    {
      "message": "This direct-send letter is paid from wallet credits.",
      "recipient": {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street"
      }
    }
  ],
  "locale": "en",
  "metadata": {
    "campaign": "spring"
  },
  "requestType": "letter",
  "senderEmail": "sender@example.com"
}
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "credits",
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": null,
  "externalId": null,
  "fulfilledAt": "2026-04-21T10:00:05.000Z",
  "metadata": {
    "campaign": "spring"
  },
  "orderId": "ord_789",
  "orderRef": "7d5118b8-7a91-4e7a-8b92-c72bd46c8432",
  "paidAt": "2026-04-21T10:00:02.000Z",
  "paymentReference": "wallet:ledger_123",
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "fulfilled",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:05.000Z",
  "wallet": {
    "balanceCents": 2300,
    "currency": "eur",
    "mode": "test"
  }
}
```

### Direct postcard order

Direct postcards use the same postcard object and are paid from credits.

**Request**

```json
{
  "locale": "en",
  "postcard": {
    "frontImageAssetId": "asset_postcard_front_123",
    "message": "A credits-funded postcard from PieterPost.",
    "recipients": [
      {
        "city": "Springfield",
        "country": "United States",
        "name": "Pieter Post",
        "postalCode": "62704",
        "state": "IL",
        "streetAddress": "123 Main Street"
      }
    ]
  },
  "requestType": "postcard",
  "senderEmail": "sender@example.com"
}
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "credits",
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": null,
  "externalId": null,
  "fulfilledAt": "2026-04-21T10:00:05.000Z",
  "metadata": {},
  "orderId": "ord_790",
  "orderRef": "8e25cad3-0f24-4a28-a55a-6ce879ec5d4a",
  "paidAt": "2026-04-21T10:00:02.000Z",
  "paymentReference": "wallet:ledger_124",
  "recipientCount": 1,
  "requestType": "postcard",
  "senderEmail": "sender@example.com",
  "status": "fulfilled",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:05.000Z",
  "wallet": {
    "balanceCents": 2300,
    "currency": "eur",
    "mode": "test"
  }
}
```

### Track orders

`GET /v1/orders/:orderId`

Fetch an API order by id with the same API account that created it.

### Get order status

Use order lookups for support, retries, and back-office reconciliation.

**Request**

```bash
curl https://pieterpost.com/v1/orders/ord_123 \
  -H "Authorization: Bearer pp_test_your_key_here"
```

**Response**

```json
{
  "amountCents": 200,
  "billingMode": "hosted_checkout",
  "checkoutUrl": "https://pieterpost.com/v1/sandbox/checkout/ord_123",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "error": null,
  "expiresAt": "2026-04-22T10:00:00.000Z",
  "externalId": "invoice_123",
  "fulfilledAt": null,
  "metadata": {
    "customerId": "cus_123"
  },
  "orderId": "ord_123",
  "orderRef": "5b5b8d3f-7b83-4a6f-9fb8-0c1c3f9b8d8c",
  "paidAt": null,
  "paymentReference": null,
  "recipientCount": 1,
  "requestType": "letter",
  "senderEmail": "sender@example.com",
  "status": "checkout_open",
  "testMode": true,
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

### Read wallet balance

`GET /v1/wallet?currency=eur`

Read API account capabilities and the current wallet balance.

### Wallet capabilities

Use `GET /v1/wallet?currency=eur` before direct-send flows to check the current credit balance.

**Request**

```bash
curl https://pieterpost.com/v1/wallet?currency=eur \
  -H "Authorization: Bearer pp_test_your_key_here"
```

**Response**

```json
{
  "accountId": "acct_123",
  "approvalStatus": "approved",
  "approvedForDirectSend": true,
  "creditsEnabled": true,
  "hostedApiEnabled": true,
  "liveEnabled": true,
  "wallet": {
    "balanceCents": 2500,
    "currency": "eur",
    "mode": "test",
    "updatedAt": "2026-04-21T10:00:00.000Z"
  }
}
```

### Create credit top-ups

`POST /v1/credits/topups`

Create a wallet top-up. Test mode applies credits immediately; live mode returns a Stripe Checkout URL that you send the payer to.

### Test top-up

Test mode creates and pays the top-up in one response.

**Request**

```json
{
  "amountCents": 2500,
  "currency": "eur",
  "metadata": {
    "source": "billing-settings"
  },
  "returnUrl": "https://example.com/topups/return"
}
```

**Response**

```json
{
  "amountCents": 2500,
  "checkoutUrl": null,
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "paidAt": "2026-04-21T10:00:01.000Z",
  "status": "paid",
  "testMode": true,
  "topupId": "topup_123",
  "updatedAt": "2026-04-21T10:00:01.000Z",
  "wallet": {
    "balanceCents": 2500,
    "currency": "eur",
    "mode": "test"
  }
}
```

### Live top-up

Live mode returns a pending top-up with `checkoutUrl`. Open or redirect to that Stripe URL; the wallet is credited after payment succeeds.

**Request**

```json
{
  "amountCents": 2500,
  "currency": "eur",
  "metadata": {
    "source": "billing-settings"
  },
  "returnUrl": "https://example.com/topups/return"
}
```

**Response**

```json
{
  "amountCents": 2500,
  "checkoutUrl": "https://checkout.stripe.com/c/pay/...",
  "createdAt": "2026-04-21T10:00:00.000Z",
  "currency": "eur",
  "paidAt": null,
  "status": "pending",
  "testMode": false,
  "topupId": "topup_456",
  "updatedAt": "2026-04-21T10:00:00.000Z"
}
```

- **Live response:** With a `pp_live_...` key, the response is `pending` and includes `checkoutUrl`. Redirect the payer to that Stripe Checkout URL, or show it as a payment link.
- **Completion:** Live payment completes asynchronously. After Stripe confirms payment, PieterPost marks the top-up `paid` and credits the live wallet.
- **Return URL:** If you pass `returnUrl`, Checkout sends the payer back with `topup=success` or `topup=cancelled`. Use `GET /v1/wallet` after payment to confirm the credited balance.

### Upload assets

`POST /v1/uploads`

Upload a temporary file with an API key before creating the order payload that references it.

- **Authentication:** Use the same bearer-key authentication as other `/v1` endpoints. Send multipart form-data instead of JSON.
- **Kinds:** Use `postcard-image` for optional square 1:1 PNG/JPEG postcard fronts, `letter-attachment` for PDF, PNG, or JPEG letter attachments, and `letter-stamp-image` for PNG/JPEG custom letter stamps.

## Orders and status

Order statuses are intentionally small so integrations can branch on a stable lifecycle.

- **Hosted checkout success:** `draft` -> `checkout_open` -> `paid` -> `queued` -> `fulfilled`. The `checkoutUrl` is present while checkout is open.
- **Hosted checkout not paid:** `checkout_open` can become `cancelled` when the customer cancels or `expired` after the checkout expiry window.
- **Direct send success:** `draft` -> `paid` -> `queued` -> `fulfilled`. Wallet debit happens before fulfillment starts.
- **Failures:** `failed` means validation, payment, wallet debit, provider handoff, or server configuration stopped the order.

- **draft:** The order exists, but checkout or fulfillment has not started yet.
- **checkout_open:** The hosted checkout URL is ready and waiting for payment.
- **paid:** Payment or wallet debit succeeded.
- **queued:** The order is queued for fulfillment.
- **fulfilled:** The order has been handed to the mail fulfillment flow.
- **failed:** The order could not be completed. Check the `error` field.
- **cancelled or expired:** The hosted checkout was cancelled or was not paid before expiry.

## Limits

Release limits keep the public API safe while leaving room for normal integration testing.

- **Minute request limit:** Each API account is limited to 60 requests per minute across all keys. Individual keys and source IPs are also checked.
- **Daily order limit:** Each API account can create up to 100 API orders per UTC day across test and live modes.
- **Batch size:** Each letter or postcard order can include up to 25 recipients.

## Errors

Errors return a short machine-readable code and a human-readable message.

```json
{
  "code": "invalid_request",
  "error": "Hosted checkout links require a valid senderEmail."
}
```

- **Authentication:** `missing_api_key`, `invalid_api_key`, `invalid_api_key_mode`, `api_key_inactive`, and `account_inactive`.
- **Validation:** `invalid_json` and `invalid_request` cover malformed JSON, invalid recipients, missing messages, missing sender emails, and bad return URLs.
- **Access and limits:** `rate_limited`, `daily_order_cap_reached`, and `live_order_cap_reached`.
- **Server configuration:** `misconfigured`, `db_error`, and `unknown_error` mean the request reached PieterPost but the server could not complete it.
