Partner API Reference
Integrate written letters into your application. Create orders, check balances, and receive real-time webhook notifications.
Base URL: https://yourdomain.com/api/v1/partner
Authentication
All API requests require a Bearer token in the Authorization header. API keys are generated in the Partner Dashboard under API Management.
Authorization: Bearer pl_live_YOUR_API_KEY
Each key has scoped permissions (e.g., orders.create, balance.read). Requests to endpoints without the required permission return 403 Forbidden.
Rate Limits
The API enforces a sliding-window rate limit of 100 requests per minute per API key.
X-RateLimit-Limit — Maximum requests per window
X-RateLimit-Remaining — Remaining requests in the current window
X-RateLimit-Reset — Unix timestamp when the window resets
When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header.
Errors
All error responses follow a consistent format:
{
"error": "error_code",
"message": "Human-readable description of what went wrong.",
"suggestion": "How to fix the issue."
}| Status | Error Code | Description |
|---|---|---|
| 400 | validation_error | Invalid request parameters |
| 401 | unauthorized | Invalid or missing API key |
| 402 | insufficient_credits | Not enough credit balance |
| 403 | forbidden | API key lacks required permission |
| 404 | not_found | Resource not found |
| 409 | not_cancellable | Order is not in a cancellable state |
| 429 | rate_limit_exceeded | Too many requests |
| 500 | internal_error | Server error |
Balance
/balanceCheck your credit balanceReturns your current balance, pricing info, and estimated letters remaining.
Permission required: balance.read
{
"balance_cents": 250000,
"low_balance_alert_cents": 5000,
"pricing_model": "wholesale",
"per_letter_cost_cents": 199,
"per_envelope_cost_cents": 99,
"estimated_letters_remaining": 1256
}Fonts
/fontsList available handwriting fontsReturns all fonts available to your tenant, including custom uploaded fonts.
Permission required: fonts.read
{
"fonts": [
{
"id": "6abef6e4-2253-4767-a95f-6a718e2e5e5c",
"name": "Kyle1",
"description": "Clean, natural handwriting style",
"category": "script",
"is_default": true,
"is_custom": false
}
],
"default_font_id": "6abef6e4-2253-4767-a95f-6a718e2e5e5c",
"total": 1
}Orders
/ordersList orders with paginationQuery parameters:
page— Page number (default: 1)limit— Results per page, max 100 (default: 25)status— Filter by status: pending, processing, completed, cancelledsort— Sort by: created_at, updated_at, total_cents, letter_countorder— Sort direction: asc, desc (default: desc)
/ordersCreate a new orderPermission required: orders.create
Creates an order, inserts recipients, deducts credits, and triggers the order.confirmed webhook. Returns 402 if your balance is insufficient.
{
"message_body": "Dear {{first_name}},\n\nThank you for your business!\n\nBest regards",
"product_type": "letter", // letter | card | postcard
"paper_type": "standard", // standard | premium
"envelope_type": "standard", // optional
"handwriting_style": "", // font ID or legacy style name (optional)
"return_address": { // optional
"name": "My Company",
"line1": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001"
},
"recipients": [
{
"first_name": "John",
"last_name": "Doe",
"address_line1": "456 Oak Ave",
"address_line2": "Suite 100",
"city": "Los Angeles",
"state": "CA",
"zip": "90001",
"country": "US",
"custom_fields": { "company": "Acme Inc" }
}
]
}{
"order": {
"id": "a1b2c3d4-...",
"status": "pending",
"letter_count": 1,
"total_cents": 298,
"product_type": "letter",
"paper_type": "standard",
"envelope_type": "standard",
"recipient_count": 1,
"cost_breakdown": {
"letter_cost_cents": 199,
"envelope_cost_cents": 99,
"total_letters": 1,
"total_envelopes": 1,
"subtotal_cents": 298,
"description": "1 letter x $1.99 + 1 envelope x $0.99 = $2.98"
},
"created_at": "2026-04-07T12:00:00.000Z"
},
"balance_cents": 249702,
"transaction_id": "e5f6g7h8-..."
}{
"error": "insufficient_credits",
"message": "Insufficient credit balance for this order.",
"balance_cents": 100,
"cost_cents": 298,
"shortfall_cents": 198
}Order Detail
/orders/:idGet a single order with recipientsPermission required: orders.read
{
"order": {
"id": "a1b2c3d4-...",
"status": "pending",
"letter_count": 1,
"total_cents": 298,
"message_body": "Dear John,\n\nThank you!",
"handwriting_style": "6abef6e4-...",
"paper_type": "standard",
"product_type": "letter",
"envelope_type": "standard",
"source": "partner_api",
"return_address": { "name": "My Co", "line1": "123 Main St", ... },
"created_at": "2026-04-07T12:00:00.000Z",
"updated_at": "2026-04-07T12:00:00.000Z",
"cancelled_at": null,
"recipients": [
{
"id": "r1s2t3-...",
"first_name": "John",
"last_name": "Doe",
"address_line1": "456 Oak Ave",
"city": "Los Angeles",
"state": "CA",
"zip": "90001",
"country": "US",
"status": "pending",
"tracking_number": null,
"mailed_at": null
}
],
"recipient_count": 1
}
}Cancel Order
/orders/:id/cancelCancel a pending orderCancels an order and refunds credits. Only orders with pending status can be cancelled. Returns 409 if the order has already been processed.
{
"order": {
"id": "a1b2c3d4-...",
"status": "cancelled",
"cancelled_at": "2026-04-07T12:05:00.000Z"
},
"balance_cents": 250000,
"message": "Order cancelled successfully. Credits have been refunded."
}Webhooks
Register webhook endpoints to receive real-time notifications when events occur. Manage webhooks via the API or the Partner Dashboard.
/webhooksList all webhooks/webhooksCreate a new webhook/webhooks/:idDeactivate a webhook{
"url": "https://yourapp.com/webhooks/premier",
"events": ["order.confirmed", "order.completed"]
}{
"webhook": {
"id": "w1x2y3-...",
"url": "https://yourapp.com/webhooks/premier",
"events": ["order.confirmed", "order.completed"],
"is_active": true,
"secret": "whsec_abc123...",
"created_at": "2026-04-07T12:00:00.000Z"
},
"message": "Webhook created. Save the secret -- it will not be shown again."
}Webhook Events
| Event | Description |
|---|---|
| order.confirmed | Order created and credits deducted |
| order.completed | All letters in the order have been printed |
| order.cancelled | Order was cancelled and credits refunded |
| order.shipped | Order has been shipped / mailed |
| letter.mailed | Individual letter has been mailed (includes tracking) |
Webhook Signatures
Every webhook delivery includes an HMAC-SHA256 signature so you can verify it came from Premier Letters.
X-Premier-Signature — HMAC-SHA256 hex digest of the payload
X-Premier-Event — The event type (e.g., order.confirmed)
X-Premier-Delivery — Unique delivery ID
X-Premier-Timestamp — ISO 8601 timestamp
import crypto from 'crypto';
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/premier', (req, res) => {
const signature = req.headers['x-premier-signature'];
const rawBody = req.body; // must be the raw string, not parsed JSON
if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Received event:', event.event, event.data);
res.status(200).send('OK');
});Retry Policy
If your endpoint returns a non-2xx status code or fails to respond within 10 seconds, we retry the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 1 hour |
After 3 consecutive failed delivery attempts, the webhook is automatically deactivated. You can reactivate it from the Partner Dashboard or create a new one.