# AgentPrinter — Full API Reference > Account-free print-and-mail API. Send a physical letter or postcard via a single API call. No signup required. Base URL: https://api.agentprinter.app/v1 --- ## POST /v1/print Submit a print job. Returns a Stripe payment link and a job ID. ### Request ``` POST /v1/print Content-Type: application/json ``` #### Request Body The request body has four top-level fields: | Field | Type | Required | Description | |---|---|---|---| | `document` | object | Yes | The document to print. Must contain exactly one of: `pdf_url`, `pdf_base64`, or `text`. | | `document.pdf_url` | string | Conditional | URL of a PDF document to print. Must be publicly accessible HTTPS URL. | | `document.pdf_base64` | string | Conditional | Base64-encoded PDF document. | | `document.text` | string | Conditional | Plain text content to print. Rendered as a formatted letter using the font specified in options. | | `recipient` | object | Yes | Recipient mailing address. US addresses only. | | `recipient.name` | string | Yes | Recipient full name. | | `recipient.line1` | string | Yes | Street address line 1. | | `recipient.line2` | string | No | Street address line 2 (apt, suite, unit, etc.). | | `recipient.city` | string | Yes | City name. | | `recipient.state` | string | Yes | Two-letter US state code (e.g., `CA`, `NY`). | | `recipient.zip` | string | Yes | 5-digit or ZIP+4 postal code (e.g., `97201` or `97201-1234`). | | `options` | object | Yes | Print and mail options. | | `options.mail_type` | string | No | `letter` (default) or `postcard`. | | `options.color` | boolean | No | `true` for color, `false` (default) for black & white. | | `options.font` | string | No | `serif` (default) or `mono`. Only applies to text input; ignored for PDF. | | `webhook_url` | string | Yes | Publicly reachable HTTPS URL to receive status update webhooks. | #### Example Request ```json { "document": { "pdf_url": "https://pdfobject.com/pdf/sample.pdf" }, "recipient": { "name": "Jane Smith", "line1": "123 Main St", "line2": "Apt 4B", "city": "Portland", "state": "OR", "zip": "97201" }, "options": { "mail_type": "letter", "color": false }, "webhook_url": "https://your-app.com/webhooks/mail" } ``` #### Example Request (plain text, no webhook) ```json { "document": { "text": "Dear Mr. Thompson,\n\nThis letter confirms your appointment on March 22nd at 10:00 AM. Please bring a valid photo ID and your insurance card.\n\nThank you,\nRiverside Legal Group" }, "recipient": { "name": "Robert Thompson", "line1": "456 Oak Ave", "line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94102" }, "options": { "mail_type": "letter", "color": false, "font": "serif" } } ``` Note: `webhook_url` is omitted here. Use `GET /v1/jobs/:id` to poll for status instead. ### Responses #### 200 OK — Success ```json { "job_id": "job_abc123def456", "status_url": "https://agentprinter.app/jobs/job_abc123def456", "payment_url": "https://checkout.stripe.com/c/pay/cs_live_...", "price_usd": 2.49, "expires_at": "2026-03-15T16:00:00Z", "webhook_secret": "whsec_abc123..." } ``` | Field | Type | Description | |---|---|---| | `job_id` | string | Unique job identifier. Prefixed with `job_`. Use this for cancellation or status polling. | | `status_url` | string | Human-friendly status page URL. Share this with the end user so they can track their mail delivery in a browser. | | `payment_url` | string | Stripe Checkout URL. Redirect the user or return this URL. Expires in ~4 hours. | | `price_usd` | number | Price in US dollars. | | `expires_at` | string | ISO 8601 timestamp when the payment link expires. Job expires if unpaid by this time. | | `webhook_secret` | string | HMAC secret for verifying incoming webhook signatures. Only present if `webhook_url` was provided. Store this securely — it is only returned once. | #### 422 Unprocessable Entity — Address Invalid ```json { "error": "address_invalid", "message": "The recipient address could not be verified. Please check the address fields.", "details": { "field": "to.zip", "issue": "ZIP code does not match city/state" } } ``` #### 422 Unprocessable Entity — Document Too Long ```json { "error": "document_too_long", "message": "The document exceeds the maximum page count for this mail type.", "details": { "max_pages": 3, "actual_pages": 5, "type": "letter" } } ``` #### 429 Too Many Requests — Rate Limited ```json { "error": "rate_limited", "message": "Too many requests. Maximum 10 requests per IP per hour.", "retry_after": 324 } ``` | Field | Type | Description | |---|---|---| | `retry_after` | integer | Seconds until the rate limit resets. | --- ## GET /v1/jobs/:id Check the current status of a job. Available for all jobs regardless of whether a webhook URL was provided. ### Request ``` GET /v1/jobs/job_abc123def456 ``` No request body required. ### Response #### 200 OK ```json { "job_id": "job_abc123def456", "status": "in_production", "mail_type": "letter", "color": false, "price_usd": 2.49, "created_at": "2026-03-15T12:00:00Z", "paid_at": "2026-03-15T12:05:00Z", "lob_job_id": "ltr_abc123", "expected_delivery": "2026-03-19", "tracking_url": "https://..." } ``` | Field | Type | Description | |---|---|---| | `job_id` | string | The job identifier. | | `status` | string | Current status: `pending_payment`, `queued`, `in_production`, `mailed`, `failed`, `expired`, or `cancelled`. | | `mail_type` | string | `letter` or `postcard`. | | `color` | boolean | Whether the job is color or B&W. | | `price_usd` | number | Price in US dollars. | | `created_at` | string | ISO 8601 timestamp when the job was created. | | `paid_at` | string or null | ISO 8601 timestamp when payment was received. Null if unpaid. | | `lob_job_id` | string or null | Fulfillment partner job ID. Null until submitted. | | `expected_delivery` | string or null | Estimated delivery date (ISO 8601 date). Available after submission. | | `tracking_url` | string or null | Tracking URL if available. | #### 404 Not Found ```json { "error": "Job not found" } ``` ### Rate Limit GET requests are rate limited separately at 60 requests per IP per hour. --- ## DELETE /v1/jobs/:id Cancel a pending job. Only jobs that have not yet been fulfilled can be cancelled, and only within 30 minutes of creation. ### Request ``` DELETE /v1/jobs/job_abc123def456 ``` No request body required. ### Responses #### 200 OK — Cancelled ```json { "job_id": "job_abc123def456", "status": "cancelled", "refund_id": "re_abc123..." } ``` `refund_id` is present if payment had been received; `null` if the job was still pending payment. #### 404 Not Found ```json { "error": "not_found", "message": "No job found with the specified ID." } ``` #### 409 Conflict — Already Fulfilled or Expired ```json { "error": "conflict", "message": "This job cannot be cancelled. It has already been submitted for fulfillment.", "details": { "status": "submitted", "submitted_at": "2026-03-15T12:10:00Z" } } ``` #### 409 Conflict — Cancellation Window Expired ```json { "error": "conflict", "message": "The 30-minute cancellation window has passed.", "details": { "created_at": "2026-03-15T11:00:00Z", "cancellation_deadline": "2026-03-15T11:30:00Z" } } ``` --- ## Webhooks All status updates are delivered via webhook to the `webhook_url` provided in the print request. There is no polling endpoint. ### Webhook Event Types | Event | Description | |---|---| | `job.paid` | Payment received via Stripe. Job is queued for printing. | | `job.queued` | Job is in the print queue. | | `job.submitted` | Job has been submitted to the print/mail fulfillment partner. | | `job.mailed` | Physical mail has been handed off to USPS. | | `job.failed` | Job failed during processing. See `failure_reason` for details. | | `job.cancelled` | Job was cancelled via the DELETE endpoint or due to payment expiry. | | `job.expired` | Payment link expired before payment was received. | ### Webhook Payload ```json { "event": "job.mailed", "job_id": "job_abc123def456", "status": "mailed", "timestamp": "2026-03-17T09:30:00Z", "data": { "type": "letter", "color": false, "tracking_number": null, "carrier": "usps_first_class", "expected_delivery": "2026-03-22", "amount_cents": 249, "metadata": { "invoice_id": "INV-2026-001", "customer_id": "cust_abc123" } } } ``` | Field | Type | Description | |---|---|---| | `event` | string | The event type (see table above). | | `job_id` | string | The job ID from the original POST /v1/print response. | | `status` | string | Current job status matching the event. | | `timestamp` | string | ISO 8601 timestamp of the event. | | `data.type` | string | Mail type: `letter` or `postcard`. | | `data.color` | boolean | Whether the job is color or B&W. | | `data.tracking_number` | string or null | USPS tracking number, if available. Typically null for First Class letters. | | `data.carrier` | string | Carrier and service level. Always `usps_first_class`. | | `data.expected_delivery` | string or null | Estimated delivery date (ISO 8601 date). Available after `job.mailed`. | | `data.amount_cents` | integer | Amount charged in US cents. | | `data.failure_reason` | string | Present only for `job.failed` events. Human-readable failure description. | | `data.metadata` | object | The metadata from the original request, if any. | ### Webhook Signature Verification Every webhook request includes a signature header for verification: ``` X-AgentPrinter-Signature: t=1710504600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` To verify: 1. Extract the timestamp (`t`) and signature (`v1`) from the header. 2. Construct the signed payload: `{timestamp}.{raw_request_body}` (the timestamp, a literal dot, then the raw JSON body). 3. Compute HMAC-SHA256 of the signed payload using the `webhook_secret` from the original POST /v1/print response. 4. Compare the computed signature with the `v1` value using a constant-time comparison. 5. Optionally, reject webhooks with timestamps older than 5 minutes to prevent replay attacks. #### Example (Node.js) ```javascript const crypto = require('crypto'); function verifyWebhook(header, body, secret) { const parts = Object.fromEntries( header.split(',').map(p => p.split('=', 2)) ); const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.${body}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(parts.v1) ); } ``` --- ## Document Limits and Accepted Formats | Format | Max Size | Notes | |---|---|---| | PDF via URL (`document_url`) | 5 MB | Must be publicly accessible. HTTPS required. | | PDF via base64 (`document_base64`) | 5 MB (decoded) | Standard base64 encoding. | | Plain text (`text`) | 50 KB | Rendered as a formatted letter with standard margins. | | Mail Type | Max Pages | |---|---| | Letter | 3 pages | | Postcard | 1 page (front only; back is used for address) | Only one document source may be provided per request (`document_url`, `document_base64`, or `text`). --- ## Pricing | Type | Price | |---|---| | B&W letter | $2.49 | | Color letter | $2.99 | | B&W postcard | $2.19 | | Color postcard | $2.49 | All prices are in USD. US addresses only. Pricing includes printing, envelope/card stock, and USPS First Class postage. --- ## Rate Limits - **Write operations** (POST /v1/print, DELETE /v1/jobs/:id): 10 requests per IP per hour - **Read operations** (GET /v1/jobs/:id): 60 requests per IP per hour - Rate-limited responses return HTTP 429 with a `Retry-After` header (seconds) --- ## Use Cases AgentPrinter is designed for scenarios where physical mail needs to be triggered programmatically: - **AI agent follow-ups**: An AI assistant schedules a meeting, then automatically sends a physical confirmation letter to the client. - **Invoice and billing**: Generate and mail invoices, payment reminders, or receipts directly from a billing system without manual steps. - **Legal and compliance**: Send notices, disclosures, or compliance letters that require a physical paper trail. - **Real estate**: Mail offer letters, property disclosures, or tenant notices directly from a CRM or deal pipeline. - **Customer outreach**: Send thank-you postcards, appointment reminders, or welcome letters triggered by CRM events. - **One-off personal mail**: A user asks their AI assistant to mail a letter — no printer, no stamps, no trip to the post office. --- ## FAQ **Do I need to create an account?** No. AgentPrinter is fully account-free. Call the API, pay via the Stripe link, and your mail is sent. No signup, no API keys, no dashboard. **How does payment work?** When you submit a print job, you receive a Stripe Checkout URL in the response. The person who triggered the request pays via that link. Mail is printed and sent only after payment is confirmed. **What document formats are supported?** PDF (via URL or base64-encoded) and plain text. Plain text is rendered into a clean letter format. Max 3 pages for letters, 5 MB for PDFs, 50 KB for text. **Can I cancel a job?** Yes, within 30 minutes of creation and before the job has been submitted to the print partner. Use `DELETE /v1/jobs/:id`. If payment was received, a full refund is issued automatically. **How do I get status updates?** All status updates are delivered via webhook to the URL you provide. There is no polling endpoint. Events: paid, queued, submitted, mailed, failed, cancelled, expired. **How long does delivery take?** USPS First Class, typically 3-5 business days within the US after the job enters the mail stream. **Do you support international addresses?** Not yet. US addresses only. International support is planned. **What is your refund policy?** Full refund if cancelled within 30 minutes and before fulfillment begins. Once submitted to the print partner, no refunds — physical mail is already in production. **How do I integrate this with my AI agent?** Use the OpenAPI spec at /v1/openapi.json as a function/tool definition in your agent framework (LangChain, Claude, GPT, etc.). The agent calls POST /v1/print, receives a payment URL to present to the user, and gets status updates via webhook. --- ## Important Notes for AI Agents 1. **webhook_url is optional.** If provided, status updates are pushed to your endpoint. If omitted, use `GET /v1/jobs/:id` to poll for status. 2. **Store the webhook_secret if using webhooks.** The POST /v1/print response includes a `webhook_secret` field when a webhook_url is provided. Store it securely and use it to verify incoming webhook signatures via HMAC-SHA256. 3. **Polling is available via GET /v1/jobs/:id.** Returns current status, tracking info, and delivery estimates. Rate limited at 60 requests per IP per hour. Recommended polling interval: every 30-60 seconds. 4. **Payments are handled via Stripe Checkout.** The `payment_url` in the response is a Stripe Checkout session URL. Either redirect a human user to it, or return the URL so they can complete payment. Payment links expire in 1 hour. 5. **Jobs expire if unpaid.** If the Stripe payment link is not completed within 1 hour, the job status transitions to `expired` and a `job.expired` webhook is sent. 6. **Cancellation is time-limited.** Jobs can only be cancelled within 30 minutes of creation and only before fulfillment has begun. 7. **Address verification happens at submission time.** Invalid addresses are rejected immediately with a 422 response and specific field-level error details. 8. **Idempotency keys prevent duplicates.** If you retry a request with the same `idempotency_key`, you will receive the original response rather than creating a duplicate job. 9. **US addresses only.** International mail is not currently supported. 10. **The OpenAPI spec is the source of truth.** For schema details and field-level validation, refer to: https://agentprinter.app/v1/openapi.json