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.

Header format
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 response
{
  "error": "error_code",
  "message": "Human-readable description of what went wrong.",
  "suggestion": "How to fix the issue."
}
StatusError CodeDescription
400validation_errorInvalid request parameters
401unauthorizedInvalid or missing API key
402insufficient_creditsNot enough credit balance
403forbiddenAPI key lacks required permission
404not_foundResource not found
409not_cancellableOrder is not in a cancellable state
429rate_limit_exceededToo many requests
500internal_errorServer error

Balance

GET/balanceCheck your credit balance

Returns your current balance, pricing info, and estimated letters remaining.

Permission required: balance.read

Example response
{
  "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

GET/fontsList available handwriting fonts

Returns all fonts available to your tenant, including custom uploaded fonts.

Permission required: fonts.read

Example response
{
  "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

GET/ordersList orders with pagination

Query parameters:

  • page — Page number (default: 1)
  • limit — Results per page, max 100 (default: 25)
  • status — Filter by status: pending, processing, completed, cancelled
  • sort — Sort by: created_at, updated_at, total_cents, letter_count
  • order — Sort direction: asc, desc (default: desc)
POST/ordersCreate a new order

Permission required: orders.create

Creates an order, inserts recipients, deducts credits, and triggers the order.confirmed webhook. Returns 402 if your balance is insufficient.

Request body
{
  "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" }
    }
  ]
}
Success response (201)
{
  "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-..."
}
Insufficient credits response (402)
{
  "error": "insufficient_credits",
  "message": "Insufficient credit balance for this order.",
  "balance_cents": 100,
  "cost_cents": 298,
  "shortfall_cents": 198
}

Order Detail

GET/orders/:idGet a single order with recipients

Permission required: orders.read

Example response
{
  "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

POST/orders/:id/cancelCancel a pending order

Cancels an order and refunds credits. Only orders with pending status can be cancelled. Returns 409 if the order has already been processed.

Success response
{
  "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.

GET/webhooksList all webhooks
POST/webhooksCreate a new webhook
DELETE/webhooks/:idDeactivate a webhook
Create webhook request
{
  "url": "https://yourapp.com/webhooks/premier",
  "events": ["order.confirmed", "order.completed"]
}
Create webhook response (201)
{
  "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

EventDescription
order.confirmedOrder created and credits deducted
order.completedAll letters in the order have been printed
order.cancelledOrder was cancelled and credits refunded
order.shippedOrder has been shipped / mailed
letter.mailedIndividual 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

Verifying the signature (Node.js)
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:

AttemptDelay
1st retry30 seconds
2nd retry5 minutes
3rd retry1 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.