{"openapi":"3.1.0","info":{"title":"Premier Letters API","version":"1.0.0","description":"Create, quote, preview and track real, pen-written (or printed) letters, cards and postcards mailed via USPS. An AI assistant without a key can get one with POST /api/v1/agent/connect (the account owner approves by email). See also the remote MCP server at https://www.premierletters.com/api/mcp for AI agents.","contact":{"url":"https://www.premierletters.com/developers"}},"servers":[{"url":"https://www.premierletters.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pl_live_... | pl_test_...","description":"API key from Settings → API keys, or from the agent connect flow. Sent as `Authorization: Bearer pl_live_...`. Live keys (pl_live_) create and charge real orders. Test keys (pl_test_) run the same validation, pricing and return-address checks but never create, charge or send anything: POST /api/v1/orders answers `{ test: true }`, order lists are empty, and balance top-ups, new-card charges and Growth record writes answer 400 test_mode. Webhook endpoints can be managed with either key."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Client-supplied key, 1-255 characters. A repeat with the same key and the same body returns the original order (200, idempotent_replay: true) instead of creating a second one. The same key with a different body is a 422 idempotency_key_reused; a repeat while the first request is still running is a 409 idempotency_in_progress. The body's idempotency_key takes precedence over this header."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Stable machine-readable code, e.g. validation_failed, insufficient_balance, test_mode"},"message":{"type":"string"},"suggestion":{"type":"string","description":"What to do next"},"details":{}},"required":["error","message"]},"Recipient":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"address_line1":{"type":"string"},"address_line2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string","description":"Two-letter US state code"},"zip":{"type":"string"},"custom_fields":{"type":"object","additionalProperties":{"type":"string"}}}},"ReturnAddress":{"type":"object","description":"Return address written on the envelope. Omit to use the account default (Settings). Required for an envelope order unless the account has a default or no_return_address_ack is true.","required":["name","address_line1","city","state","zip"],"properties":{"name":{"type":"string","maxLength":100},"company":{"type":"string","maxLength":100},"address_line1":{"type":"string","maxLength":255},"address_line2":{"type":"string","maxLength":255},"city":{"type":"string","maxLength":100},"state":{"type":"string","description":"Two-letter US state code"},"zip":{"type":"string","description":"5-digit ZIP or ZIP+4"}}},"OrderOptions":{"type":"object","properties":{"handwriting_style":{"type":"string","description":"Font UUID from GET /api/v1/styles"},"paper_type":{"type":"string","enum":["standard","premium"]},"product_type":{"type":"string","description":"e.g. letter, card, postcard"},"authenticity_preset":{"type":"string"},"mailing_option":{"type":"string","enum":["we_mail","you_mail_stamped","you_mail_no_stamp"],"description":"Delivery option. Omit to use the account's default mailing option (else we_mail)."},"include_letter":{"type":"boolean"},"include_envelope":{"type":"boolean"},"copies":{"type":"integer","minimum":1,"maximum":500,"description":"Values above 1 only for ship-to-you mailing options"},"output_mode":{"type":"string","enum":["handwritten","printed_bw","printed_color"],"default":"handwritten","description":"How the letter body is produced: pen-written (default) or printed in black & white / color."},"envelope_output_mode":{"type":["string","null"],"enum":["printed_bw","printed_color",null],"default":null,"description":"How the envelope is produced: null (default) = hand-addressed, or printed. Independent of output_mode."}}},"OrderCreate":{"type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Letter body. Supports {{first_name}}/{{last_name}}/{{greeting}} merge fields."},"recipients":{"type":"array","items":{"$ref":"#/components/schemas/Recipient"}},"options":{"$ref":"#/components/schemas/OrderOptions"},"payment":{"type":"object","properties":{"method":{"type":"string","enum":["card_on_file","checkout_session","balance"],"default":"checkout_session","description":"balance pays from the prepaid balance and waives the small-order handling fee (orders under 5 pieces); 402 insufficient_balance when short."},"return_url":{"type":"string","format":"uri"}}},"skip_invalid":{"type":"boolean"},"return_address":{"$ref":"#/components/schemas/ReturnAddress"},"no_return_address_ack":{"type":"boolean","description":"Send with no return address at all. Only honoured when no return_address is given and the account has no default; undeliverable mail is then destroyed. Without it, such an order is a 400 no_return_address_unacknowledged."},"idempotency_key":{"type":"string","minLength":1,"maxLength":255,"description":"Client-supplied key, 1-255 characters. A repeat with the same key and the same body returns the original order (200, idempotent_replay: true) instead of creating a second one. The same key with a different body is a 422 idempotency_key_reused; a repeat while the first request is still running is a 409 idempotency_in_progress. Also accepted as the Idempotency-Key header; the body wins."}}},"Order":{"type":"object","properties":{"order_id":{"type":"string","description":"Order UUID; test_<uuid> for a test-mode key (nothing is created)"},"status":{"type":"string","description":"paid, pending_payment, … or test"},"letter_count":{"type":"integer"},"total_cents":{"type":"integer"},"tracking_url":{"type":["string","null"],"format":"uri"},"checkout_url":{"type":["string","null"],"format":"uri"},"return_address":{"type":"object","properties":{"source":{"type":"string","enum":["request","profile","none"],"description":"Which return address the envelope uses: the request, the account default, or none (acknowledged)."}}},"idempotent_replay":{"type":"boolean","description":"Present (true) when this is the original order returned for a repeated idempotency key"},"test":{"type":"boolean","description":"Present (true) for a test-mode key: validated and priced, nothing created or charged"},"skipped_recipients":{"type":"integer"}}},"PricingBreakdown":{"type":"object","properties":{"basePrice":{"type":"integer","description":"Cents"},"paperSurchargeCents":{"type":"integer"},"printedPageSurchargeCents":{"type":"integer"},"printedEnvelopeSurchargeCents":{"type":"integer"},"signatureSurchargeCents":{"type":"integer"},"meterSurchargeCents":{"type":"integer"},"postageCents":{"type":"integer","description":"Postage passed through at face value; 0 when the customer stamps it themselves"},"pieceCentsPerLetter":{"type":"integer","description":"Pen work per piece, excluding postage"},"totalCents":{"type":"integer"},"perLetterCents":{"type":"integer"},"pieceCount":{"type":"integer"}}},"Balance":{"type":"object","properties":{"balance_cents":{"type":"integer","description":"Prepaid balance; 0 for a test-mode key"},"min_cents":{"type":"integer","description":"Minimum top-up (1000)"},"max_cents":{"type":"integer","description":"Maximum top-up (6000000)"},"tiers":{"type":"array","items":{"type":"object","properties":{"min_cents":{"type":"integer"},"bonus_pct":{"type":"number"}}}},"presets_cents":{"type":"array","items":{"type":"integer"}},"next_tier":{"type":["object","null"],"properties":{"min_cents":{"type":"integer"},"bonus_pct":{"type":"number"}}},"handling_fee_waived_when_paying_from_balance":{"type":"boolean"},"test":{"type":"boolean"}}},"Topup":{"type":"object","properties":{"checkout_url":{"type":"string","format":"uri","description":"Stripe Checkout link for a human to pay; the credit lands when it is paid"},"amount_cents":{"type":"integer"},"bonus_cents":{"type":"integer"},"min_cents":{"type":"integer"},"max_cents":{"type":"integer"}}},"ConnectRequest":{"type":"object","required":["email","agent_name"],"properties":{"email":{"type":"string","format":"email","description":"The account owner's email; the approval link is sent here"},"agent_name":{"type":"string","minLength":1,"maxLength":80,"description":"Your name as the user will see it, e.g. \"Claude\". Letters, digits, spaces and . , ' & ( ) - _ only (no links)."},"mode":{"type":"string","enum":["live","test"],"default":"live"}}},"ConnectStarted":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"poll_url":{"type":"string","format":"uri"},"poll_secret":{"type":"string","description":"Pass as ?secret= when polling. Keep it private."},"user_code":{"type":"string","description":"Short code (ABC-DEF) to show the user; the approval page repeats it"},"expires_at":{"type":"string","format":"date-time"},"instructions":{"type":"string"}}},"ConnectStatus":{"type":"object","properties":{"status":{"type":"string","enum":["pending","approved","claimed","denied","expired"]},"api_key":{"type":"string","description":"Only with status approved, and only once. Save it."},"mode":{"type":"string","enum":["live","test"]}}},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","lead.created","lead.stage_changed","reward.released","reward.reversed"]}},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string"},"event_type":{"type":"string"},"response_status":{"type":["integer","null"]},"status":{"type":"string"},"attempts":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}}}},"paths":{"/api/v1/config":{"get":{"summary":"Public pricing + agent-discovery config","security":[],"responses":{"200":{"description":"Live pricing tiers, character meter, and service levels"}}}},"/api/v1/agent/connect":{"post":{"summary":"Ask the account owner for an API key (no key needed)","description":"Emails the owner an approval link (they can sign up from it). Show them user_code, then poll GET /api/v1/agent/connect/{id}?secret=<poll_secret> every 10-30 seconds until status is approved. Always 202 for a valid body, whether or not the email has an account.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectRequest"}}}},"responses":{"202":{"description":"Request created and approval email sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectStarted"}}}},"400":{"description":"invalid_json, or validation_error (bad email, agent_name or mode)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited: too many requests from this IP, or for this email, in the last hour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"connect_failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agent/connect/{id}":{"get":{"summary":"Poll a connect request; returns the API key exactly once","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"secret","in":"query","required":true,"schema":{"type":"string"},"description":"poll_secret from POST /api/v1/agent/connect"}],"responses":{"200":{"description":"pending | denied | expired | claimed, or approved with api_key (returned once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectStatus"}}}},"404":{"description":"No request matches this id and secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited: polling too fast","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/verify":{"get":{"summary":"Verify an API key","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Key is valid; returns account + payment method summary"},"401":{"description":"Missing or invalid key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/styles":{"get":{"summary":"List handwriting styles","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Array of available fonts"}}}},"/api/v1/validate":{"post":{"summary":"Dry-run validation + pricing for an order","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreate"}}}},"responses":{"200":{"description":"Order is valid; includes total_cents and a full PricingBreakdown","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"total_cents":{"type":"integer"},"breakdown":{"$ref":"#/components/schemas/PricingBreakdown"}}}}}},"400":{"description":"no_return_address_unacknowledged and other request errors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"One or more recipients/fields failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/preview":{"post":{"summary":"Render a letter to SVG and get its price, without creating an order","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"},"handwriting_style":{"type":"string"},"product_type":{"type":"string"},"paper_type":{"type":"string"}}}}}},"responses":{"200":{"description":"SVG data URL, char count, and price"}}},"get":{"summary":"Same as POST, via query params (message, handwriting_style, product_type, paper_type)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"SVG data URL, char count, and price"}}}},"/api/v1/orders":{"get":{"summary":"List the caller's orders (always empty for a test key)","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer"}},{"name":"status","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated order list"}}},"post":{"summary":"Create (and pay for) a new order","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreate"}}}},"responses":{"200":{"description":"Idempotent replay: the order already created with this key (idempotent_replay: true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"201":{"description":"Order created and paid, or (test key) validated and priced with test: true","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"202":{"description":"Order created, payment pending (checkout_url returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"no_return_address_unacknowledged (envelope order with no return_address, no account default and no no_return_address_ack), invalid_payment_method, payment_method_not_supported, invalid_delivery, content_not_allowed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Payment required or failed (no_payment_method, payment_failed, insufficient_balance)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"idempotency_in_progress: a request with this idempotency key is still being processed; retry shortly","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"validation_failed, or idempotency_key_reused (same key, different request body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"checkout_unavailable: nothing created, safe to retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/orders/{id}":{"get":{"summary":"Get order detail with recipients","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Order + recipients + summary"},"404":{"description":"Order not found (always, for a test key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/orders/{id}/tracking":{"get":{"summary":"Lightweight per-recipient tracking","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Tracking summary + per-recipient status"},"404":{"description":"Order not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/balance":{"get":{"summary":"Prepaid balance and top-up rules","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Balance summary (balance_cents 0 and test: true for a test key)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}}}}},"/api/v1/balance/topup":{"post":{"summary":"Create a Stripe Checkout link that adds prepaid balance once a human pays it","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_cents"],"properties":{"amount_cents":{"type":"integer","minimum":1000,"maximum":6000000}}}}}},"responses":{"200":{"description":"Checkout link created; the credit (plus any tier bonus) lands when it is paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Topup"}}}},"400":{"description":"invalid_amount, below_minimum, above_maximum, or test_mode (test keys cannot top up)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate_limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"checkout_failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/payment/charge-new-card":{"post":{"summary":"Create and pay for an order with a new card (Stripe payment method id)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"requires_action: true with payment_intent_client_secret; the card needs 3-D Secure authentication before it is charged"},"201":{"description":"Order created and charged"},"400":{"description":"missing_payment_method, validation errors, or test_mode (test keys cannot charge a card)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"card_declined","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks":{"get":{"summary":"List webhook endpoints","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Endpoints and the valid event names","content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}},"valid_events":{"type":"array","items":{"type":"string"}}}}}}}}},"post":{"summary":"Create a webhook endpoint (returns the signing secret once)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","lead.created","lead.stage_changed","reward.released","reward.reversed"]},"description":"Defaults to every event"}}}}}},"responses":{"201":{"description":"Created; includes secret (whsec_...), which is never shown again","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string"},"warning":{"type":"string"}}}]}}}},"400":{"description":"missing_url, invalid_url or invalid_events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{id}":{"get":{"summary":"Get a webhook endpoint","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Update a webhook endpoint","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","lead.created","lead.stage_changed","reward.released","reward.reversed"]}},"is_active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"invalid_url, invalid_events, or no fields to update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete a webhook endpoint","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"summary":"Recent delivery attempts for a webhook endpoint","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"pagination":{"type":"object"}}}}}},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhooks/stripe":{"post":{"summary":"Stripe webhook receiver (inbound, not called by API clients)","security":[],"responses":{"200":{"description":"Acknowledged"}}}}}}