Register an HTTPS endpoint and Pony sends it a signed POST each time one of your orders changes status. You don't need to poll.
Register an endpoint#
Use a key with the webhooks:write scope (keys from the simple form have it), or add an endpoint under Account → Credits → Status webhooks.
curl --fail-with-body -sS -X POST "$PONY_API_BASE/webhooks" \
-H @<(printf 'Authorization: Bearer %s\n' "$(cat "$PONY_API_KEY_FILE")") \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/pony-webhooks","events":["order.mailed","order.action_needed"]}'| Field | Notes |
|---|---|
url |
Required. A public https URL, with no credentials or fragment. Private, loopback and link-local addresses are refused. |
events |
Optional. The event types you want. Leave it out to receive all of them. |
description |
Optional. Up to 200 characters. |
The response includes id and secret (whsec_…). Save the secret now: it's shown only once, and you need it to verify signatures. You can have up to 10 active endpoints.
Webhook routes don't take an idempotency key, so creating the same endpoint twice registers it twice. List your endpoints and remove the extra one.
Events#
| Event | Sent when |
|---|---|
order.queued |
The order is accepted: credits deducted, or card payment confirmed |
order.preparing |
The order is accepted for printing |
order.printed |
Printing is confirmed |
order.mailed |
The letter is handed to the US Postal Service. This isn't delivery. |
order.cancelled |
The order is cancelled. Credits are returned once if it was cancelled before preparation. |
order.action_needed |
The order needs your attention. Read the order for details. |
order.returned |
The Postal Service returned the letter |
ping |
You sent a test with POST /webhooks/{id}/test. data is {"endpoint_id": …}. |
An endpoint receives only changes that happen after you register it.
Payload#
Payloads never include letter text, files or addresses:
{"id":"evt_…","type":"order.mailed","created_at":"2026-10-26T15:04:05+00:00",
"data":{"order_id":"order_…","status":"mailed","client_reference":"invoice-1042","draft_id":"draft_…","mailed_at":"2026-10-26T15:04:05+00:00"}}Each request has these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
Pony-Event-Id |
The event id |
Pony-Event-Type |
The event type |
Pony-Signature |
t=<unix seconds>,v1=<hex signature> |
Verify the signature#
v1 is an HMAC-SHA256 of the bytes <t>.<raw body>, keyed with your endpoint secret. Verify it against the raw request body, before parsing the JSON, and reject old timestamps:
import hashlib, hmac, json, time
def verify_pony_webhook(body: bytes, header: str, secret: str, tolerance: int = 300) -> dict:
"""Return the event if Pony-Signature is valid and fresh; raise ValueError otherwise."""
try:
fields = dict(part.split('=', 1) for part in header.split(','))
stamp, received = int(fields['t']), fields['v1']
except (KeyError, ValueError):
raise ValueError('Malformed Pony-Signature header')
if abs(time.time() - stamp) > tolerance:
raise ValueError('Stale webhook timestamp')
expected = hmac.new(secret.encode(), str(stamp).encode() + b'.' + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received):
raise ValueError('Invalid webhook signature')
return json.loads(body)In Flask:
event = verify_pony_webhook(request.get_data(), request.headers['Pony-Signature'], SECRET)Respond and retry#
- Respond with any
2xxwithin 5 seconds. Do slow work after responding. - Failed deliveries are retried. A timeout, an error status or a redirect (redirects are never followed) is retried with exponential backoff, from 30 seconds up to 6 hours apart, for 24 hours. After that the delivery is marked
failed. - Expect duplicates and reordering. Delivery is at least once, and events can arrive out of order. Deduplicate on the event
id, and treatGET /orders/{order_id}as the source of truth.
Manage endpoints#
| Request | What it does |
|---|---|
GET /webhooks |
Lists your endpoints, without their secrets |
POST /webhooks/{id}/test |
Sends a signed ping event |
GET /webhooks/{id}/deliveries |
Lists the 50 most recent deliveries |
DELETE /webhooks/{id} |
Stops deliveries and cancels pending ones |
Each delivery shows its state (pending, delivered, failed or cancelled), attempts, next_attempt_at and an attempt_log. The log records each attempt's status code, duration and error kind: http_status, timeout, connection_error, redirect_not_followed or destination_not_allowed. Pony doesn't store your response bodies.