API reference
PS 00 · For your coding agent
Coding-agent brief
Copy the brief below into your coding agent. It points to the machine-readable API reference at /llms.txt and lists the constraints the integration must preserve.
You are helping me integrate POST Request (https://postrequest.dev), a mailbox-access verification API. I send an address; a card carrying a single-use QR code gets printed and mailed; when the card is completed, a signed webhook fires. Every attempt needs an address, postage, and time.
Fetch the full machine-readable reference before writing any code: https://postrequest.dev/llms.txt
Facts you must respect:
- Base URL https://postrequest.dev/api/v1. JSON. Bearer auth. Test keys are pr_test_…, live keys pr_live_… — the key chooses the mode; there is NO mode parameter.
- Create: POST /verifications with { address: { line1, line2?, city, state, zip }, class: "standard"|"sealed", binding: "none"|"email_otp", email?, external_id?, return_url?, metadata? }. ("certified" is still accepted as a deprecated alias for "sealed".) There is NO recipient-name field — mail is addressed to CURRENT RESIDENT.
- Validation runs before the debit: a bad address returns 422 and charges nothing. Insufficient credits returns 402 and mails nothing — treat it as queueable and retry after top-up. A global cap of 3 mailpieces per address per 7 days returns 429.
- Track by polling GET /verifications/:id or by handling webhooks: verification.delivered / .verified / .expired / .undeliverable.
- Verify webhook signatures: header X-Post-Request-Signature = "t=<unix>,v1=<hex>", where v1 = HMAC-SHA256 of `${t}.${rawBody}` under my signing secret. Compute over the RAW body and reject if now - t > 300s.
- On verification.delivered, prompt my user to check the mail while the card is current.
- The delivered status/event reflects provider postal progress (Lob processed_for_delivery), not confirmed receipt by the recipient. delivered_at can also be filled when a credential is completed. Only verification.verified records completion; test-mode events are simulated.
- Someone completed the mailed QR credential. With binding: none (the default), anyone with the link or a copy can complete it; no account sign-in or email code is required. Someone completed both the mailed QR credential and a code sent to the bound email. This checks control of both credentials, not that the same person originally received the mail. Neither mode attests legal identity or one-human uniqueness. Credentials can be shared or forwarded; completion does not prove residency, exclusive mailbox access, or that a user is not automated.
- Single-use means one successful completion per credential, not that its URL disappears. A completed page and return link may remain available until purge. Resending revokes the old credential; expiry prevents new completion; purge revokes access to the page.
- Never use test-mode completion to unlock a production account. Test mode simulates mail; it does not establish real mailbox access or exercise live provider acceptance, charging, deliverability, or the live address cap. Address identifiers and reuse counts are separate from live mode. The account request budget is shared across both modes.
Now help me wire this into my app. Start in test mode. My stack and what I'm gating: <describe here>.PS 01
Overview
POST Request verifies access to a deliverable mailbox by sending it a single-use QR code. When the card is completed, your signed webhook fires. Standard delivery is scheduled for 5–7 business days.
All endpoints live under /api/v1, speak JSON, and return errors that say what to do next. Nothing here apologizes for the latency: the latency is the credential.
PS 02
Authentication
Pass your API key as a Bearer token. Keys come in two modes — pr_test_… and pr_live_… — and the key you use decides which mode the verification is created in. There is no mode parameter; there is only the key.
Keys are issued in the dashboard under API keys. Sign in with your email — a new email opens an account, and test keys are ready at once.
curl https://postrequest.dev/api/v1/verifications \ -H "Authorization: Bearer pr_live_..."
PS 03
Test mode
Test mode simulates the mail flow with the clock compressed: mail “prints” in ~10 seconds, “delivers” in ~60, costs nothing, and never touches your credit balance. The QR is shown in the dashboard so you can scan it with your own phone and be your own end user — same verification page, same webhook, same green stamp. With binding: email_otp, the test code is shown on the verification’s dashboard page; it is emailed only when the bound email is your account’s own sign-in email.
Test mode simulates mail; it does not establish real mailbox access or exercise live provider acceptance, charging, deliverability, or the live address cap. Address identifiers and reuse counts are separate from live mode. The account request budget is shared across both modes.
PS 04
Create a verification
POST /api/v1/verifications
Validates the address, debits your balance, prints a card, and mails it. Validation runs before the debit — a bad address is a 422, not a $1.50 lesson. No recipient name is accepted anywhere: mailpieces are addressed to CURRENT RESIDENT, by design.
Live create and resend requests require an Idempotency-Key header: a unique opaque value per logical operation, reused unchanged on retries. The same key with changed parameters returns 409; external_id is not a deduplication key. Test mode supports the header too.
Create and resend share a limit of 60 request evaluations per fixed minute per account, across test/live and the dashboard; retries count. A 429 request_rate_limited includes Retry-After: wait, then retry with the same key. Polling does not consume this budget. The separate live address limit remains three mailpieces per seven days across all customers.
JSON bodies are limited to 16 KiB with a five-second read deadline (413 or 408). Address lines and city are each limited to 64 characters, state to two and ZIP to ten. Address fields must be strings without control characters or pipe separators. Email is limited to 254 characters, external_id to 200 and return_url to 2,048. Metadata must be an object of at most 4,096 JSON bytes, six levels, 128 values and 64 characters per key.
A 202 response means dispatch is pending, not that the provider accepted the card. Poll the Location URL and inspect mail_status. Unknown outcomes keep their debit and QR; automatic retries use the saved payload and provider key for up to 23 hours. A needs_review result requires support reconciliation. Do not create a replacement or change the key.
| Field | Type | Notes |
|---|---|---|
| address | object | line1, line2?, city, state, zip. Required. |
| class | string | standard ($1.50, default) · sealed ($4.00). “certified” is still accepted as a deprecated alias for “sealed.” |
| binding | string | none (default) or email_otp |
| string | Required iff binding=email_otp. Stored encrypted, purged on schedule. | |
| external_id | string | Your reference. The dashboard manifest is searchable by it. |
| return_url | string | https only. Shown to the user after the green stamp. |
| metadata | object | Yours, returned verbatim. |
// 201 Created
{
"id": "01JZ3NVJ4M8Q...",
"class": "standard",
"status": "created",
"price_cents": 150,
"balance_cents": 9850,
"expires_at": "2026-08-18T00:00:00Z",
"signals": {
"address_reuse_count": 0,
"cluster_count": 0,
"po_box": false,
"cmra": null
}
}
// 400 — the request body was not JSON. Nothing debited.
{ "error": "invalid_json", "detail": "request body must be JSON" }
// 402 — insufficient credits. Nothing printed. See "Recommended patterns".
{ "error": "insufficient_credits", "balance_cents": 50, "required_cents": 150 }
// 422 — the address failed validation. Nothing debited.
{ "error": "invalid_address", "field": "address.zip", "detail": "..." }
// 429 — this address hit the global limit: 3 mailpieces per 7 days,
// across ALL senders. Harassment protection; not negotiable.
{ "error": "address_rate_limited" }
// 202 — dispatch pending: poll Location; do not create a replacement.
// 409 — the idempotency key was used with different parameters.
// 503 — preparation failed before dispatch; nothing charged or sent.
// Retry later with the same Idempotency-Key.
{ "error": "mail_unavailable", "detail": "..." }PS 05
Retrieve & list
Address displays contain only state and ZIP prefix, or “Address withheld.” Use an opaque external_id to identify your record. The address_fingerprint is an opaque afp_v1_ identifier scoped to your account and test/live mode, not a global matching key or a person ID. Signal counts cover your other verification records with mailing observations in the same mode over 90 days; they exclude this record and do not count unique people or prove bot behavior. Sandbox records never query live activity. The internal live mail-abuse limit remains cross-customer.
Common unit spellings and address-line placements share an abuse key. This is basic formatting normalization, not certified address validation or a guarantee of household uniqueness. Normalizer upgrades can change address IDs on new records; existing IDs are not rewritten. Use external_id, not an address ID, as your durable integration reference.
Address identifiers and signal fields become null after erasure or 90 days from the verification’s first creation. Null means unavailable, not zero. Resending does not renew that deadline.
GET /api/v1/verifications/:id
Returns the full record, the signals block, and the events timeline — CREATED → PRINTED → IN TRANSIT → DELIVERED → VERIFIED, with timestamps. Events may be missing or arrive out of order. While the card is at the printer, status reads printing; the event it leaves behind is printed.
The delivered status/event reflects provider postal progress (Lob processed_for_delivery), not confirmed receipt by the recipient. delivered_at can also be filled when a credential is completed. Only verification.verified records completion; test-mode events are simulated.
GET /api/v1/verifications?status=&external_id=&limit=
The manifest listing. Filter by status or your external_id.
PS 06
Resend
POST /api/v1/verifications/:id/resend
Voids the old QR token, prints a new card, mails it, and debits full price — new mailpiece, new postage. Returns 200 with the updated verification; 402 if the balance cannot cover the postage (nothing printed); 404 if the ID is unknown or belongs to the other mode; 409 if the verification already succeeded, its address has been purged, a key was reused for another verification, or another mail attempt is pending. A pending dispatch returns 202: poll its Location or replay the original request with its original key.
Historical Bulk records remain readable, but a new resend returns 409 class_withdrawn with no new charge or mailpiece. Existing paid attempts retain their original retry and recovery rules.
PS 07
Erase application PII
Erasure returns 409 while a mail outcome is unresolved or a webhook is in flight. Reconcile mail or wait for delivery to finish; a pending request is not a completed deletion.
DELETE /api/v1/verifications/:id/pii
Removes address/email fields, external_id, metadata, return_url, lookup signals, credentials and saved event, mail and webhook payloads. The card is revoked. There is no research vault. The response says pii=application_copies_removed and erasure_complete=false: minimal abuse observations remain until their original 90-day expiry, as do financial and operation receipts. Backups and processors may hold copies. This endpoint cannot recall mail or webhooks already submitted.
PS 08
Credits
GET /api/v1/credits
Balance and the append-only ledger. Credits are dollars, held in cents. They never expire.
POST /api/v1/credits/checkout
Returns a Stripe Checkout URL for a top-up. amount_cents minimum is 2500 — we sell books of stamps, not single stamps. Your card is saved in the same card-only Checkout for auto-recharge. Choose its default in the billing portal, then enable auto-recharge in the dashboard. Uncertain payments stay pending while we check the original payment; we do not start a replacement charge just because a response or webhook is late. Turning auto-recharge off prevents new payments but does not undo one already submitted. Manual top-ups are separate purchases, not retries of a pending payment.
PS 09
Webhooks
Four events: verification.delivered / .verified / .expired / .undeliverable. Delivered is your cue to nudge your user — “your card arrived, go check the mail” — and it is the cheapest conversion lever in the whole loop. Five delivery attempts with backoff (30 seconds, 2 minutes, 10 minutes, then 1 hour). Events and retry progress survive restarts. Failed deliveries can be redelivered from the dashboard; test pings get one attempt.
{
"event_id": "evt_67b8c2735ab5de033a",
"occurred_at": "2026-07-17T15:02:11Z",
"type": "verification.verified",
"id": "01JZ3NVJ4M8Q...",
"external_id": "user_8123",
"class": "standard",
"mode": "live",
"status": "verified",
"delivered_at": "2026-07-16T19:41:00Z",
"verified_at": "2026-07-17T15:02:11Z",
"binding": "email_otp",
"address_fingerprint": "afp_v1_9c44..."
}Payloads carry your account-and-mode-scoped address identifier, never the address. Delivery may be duplicated or out of order. After verifying the signature, atomically deduplicate by event_id with your own application update, and acknowledge duplicates with 2xx. The id field remains the verification ID. Legacy events containing global address identifiers are blocked from replay; retrieve the current verification instead. Other retries and manual redelivery preserve the original payload and event ID, but use a fresh signature timestamp. Poll the verification for its current state.
Saving a different URL pauses unfinished events. Explicit redelivery sends the saved snapshot to your current URL; it does not reconstruct an old event from today's verification state. Events recorded before durable delivery was introduced cannot be reconstructed this way.
PS 10
Verifying signatures
Every delivery is signed in X-Post-Request-Signature using the Stripe scheme: t=timestamp,v1=hex, where v1 is HMAC-SHA256 of `${timestamp}.${body}` under your signing secret.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(header, body, secret) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`)
.digest("hex");
const expectedBytes = Buffer.from(expected, "hex");
const receivedBytes = Buffer.from(v1, "hex");
return expectedBytes.length === receivedBytes.length &&
timingSafeEqual(expectedBytes, receivedBytes);
}PS 11 · Verbatim
What verification attests
- binding: none
- Someone completed the mailed QR credential. With binding: none (the default), anyone with the link or a copy can complete it; no account sign-in or email code is required.
- binding: email_otp
- Someone completed both the mailed QR credential and a code sent to the bound email. This checks control of both credentials, not that the same person originally received the mail.
Neither mode attests legal identity or one-human uniqueness. Credentials can be shared or forwarded; completion does not prove residency, exclusive mailbox access, or that a user is not automated. Abuse resistance comes from address economics, reuse counts, building-level clusters, PO-box detection, and customer policy.
Single-use means one successful completion per credential, not that its URL disappears. A completed page and return link may remain available until purge. Resending revokes the old credential; expiry prevents new completion; purge revokes access to the page.
PS 12 · Form PS-110
What we store
The encrypted mailing address remains on the verification through terminal state + 90 days, or until an earlier successful DELETE. Unresolved mail must first be reconciled. Separately, minimal keyed abuse observations expire 90 days after each mailing event; new activity does not extend old observations. These records are not anonymous. Use opaque external_id values and avoid personal data in metadata.
| Mailing address ciphertext | Removed at terminal + 90 days or earlier successful DELETE; unresolved mail must first be reconciled. |
| Routine abuse observations | Keyed address/building identifiers, source/account/mode and mailing timing/count evidence. Expire 90 days after each event. No research vault. |
| Address display + lookup signals | Display withheld on purge. Keyed lookup signals expire 90 days after first creation or earlier DELETE; resends do not renew them. |
| Recipient name | Never collected. |
| Email ciphertext, if bound | Ciphertext and mask removed on purge. Keyed email hash follows the lookup-signal deadline. |
| External ID + metadata + return URL | Customer supplied. Removed on purge. |
| QR token | Hash on the verification, revoked on purge. Live dispatch artwork is encrypted until acknowledgment or scheduled deletion after 24 hours. Sandbox tokens are encrypted until purge. |
| Scan signals | Keyed IP hash. Removed at the lookup-signal deadline or earlier DELETE. |
Saved webhook payloads and request-body digests expire after 90 days or an earlier purge. Erased/expired webhooks cannot be replayed. Operation-key tombstones remain: a retired key returns 409 and must not be automatically replaced with a new paid request. Cleanup runs at startup and in bounded batches every ten minutes; API signal reads and replay enforce expiry even before cleanup. Backups and processor copies require separate retention and deletion procedures.
PS 13
Recommended patterns
The 402. Treat verification creation as queueable: on 402, enqueue, alert yourself, retry after top-up. Your user was going to wait five days anyway; thirty minutes of queue delay is invisible. Our latency makes insufficient-balance errors uniquely survivable, and we are telling you so in writing.
User-pays. You pay for each mailpiece. You may pass the fee through to your users, but a paid card is not proof of a unique person. Addresses can be shared and reused within the address cap; enforce your own account policy.
The delivered nudge. On verification.delivered, email your user. The delivered event exists so you can prompt them to check the mail while the card is current.
PS 14 · Effective July 2026
Rates & conversion math
| Standard · postcard · mailed next day · 5–7 business days | $1.50 |
| Sealed · security-tinted letter · mailed next day · 5–7 business days | $4.00 |
- Standard is the default: a postcard dispatched the next business day. The everyday checkpoint for most integrations.
- Sealed keeps the code inside a security-tinted envelope. Mailing and return-address information remain visible; the service may still be identifiable. It is not Certified Mail, a compliance certification, or protection against someone opening the envelope. Pass
class: "sealed"(the legacy value"certified"still resolves here).
Pricing is per mailpiece, not per success. Effective cost per verified user equals the mailpiece rate divided by your observed completion rate. We do not yet publish a production completion benchmark; measure your own cohort. Postage is non-refundable, with two exceptions: a print rejection before mailing is credited in full, and an undeliverable Sealed piece is credited at 50%. Expired cards refund nothing; expiry alone does not establish delivery or explain non-completion. Credits never expire. The Terms govern.