# 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 | Field | Type | Required | Description | |---|---|---|---| | `type` | string | Yes | Mail type. One of: `letter`, `postcard`. | | `color` | boolean | Yes | `true` for color printing, `false` for black & white. | | `document_url` | string | Conditional | URL of a PDF document to print. Must be publicly accessible. Required if `document_base64` and `text` are not provided. | | `document_base64` | string | Conditional | Base64-encoded PDF document. Required if `document_url` and `text` are not provided. | | `text` | string | Conditional | Plain text content to print. Will be rendered as a formatted letter. Required if `document_url` and `document_base64` are not provided. | | `to` | object | Yes | Recipient mailing address. US addresses only. | | `to.name` | string | Yes | Recipient full name. | | `to.company` | string | No | Recipient company or organization name. | | `to.address_line1` | string | Yes | Street address line 1. | | `to.address_line2` | string | No | Street address line 2 (apt, suite, unit, etc.). | | `to.city` | string | Yes | City name. | | `to.state` | string | Yes | Two-letter US state code (e.g., `CA`, `NY`). | | `to.zip` | string | Yes | 5-digit or ZIP+4 postal code (e.g., `97201` or `97201-1234`). | | `from` | object | No | Return address. If omitted, mail is sent without a return address. | | `from.name` | string | Yes | Sender full name. Required if `from` is provided. | | `from.company` | string | No | Sender company or organization name. | | `from.address_line1` | string | Yes | Sender street address line 1. Required if `from` is provided. | | `from.address_line2` | string | No | Sender street address line 2. | | `from.city` | string | Yes | Sender city. Required if `from` is provided. | | `from.state` | string | Yes | Sender two-letter US state code. Required if `from` is provided. | | `from.zip` | string | Yes | Sender ZIP code. Required if `from` is provided. | | `webhook_url` | string | Yes | Publicly reachable HTTPS URL to receive status update webhooks. | | `idempotency_key` | string | No | Client-generated unique key to prevent duplicate submissions. Max 255 characters. | | `metadata` | object | No | Arbitrary key-value pairs (string values only). Max 10 keys, 500 chars per value. Returned in webhook payloads. | #### Example Request ```json { "type": "letter", "color": false, "document_url": "https://example.com/invoice.pdf", "to": { "name": "Jane Smith", "address_line1": "123 Main St", "address_line2": "Apt 4B", "city": "Portland", "state": "OR", "zip": "97201" }, "from": { "name": "Acme Corp", "address_line1": "456 Oak Ave", "city": "San Francisco", "state": "CA", "zip": "94102" }, "webhook_url": "https://your-app.com/webhooks/mail", "metadata": { "invoice_id": "INV-2026-001", "customer_id": "cust_abc123" } } ``` ### Responses #### 201 Created — Success ```json { "id": "job_abc123def456", "status": "pending_payment", "type": "letter", "color": false, "payment_url": "https://checkout.stripe.com/c/pay/cs_live_...", "amount_cents": 249, "currency": "usd", "webhook_url": "https://your-app.com/webhooks/mail", "webhook_secret": "whsec_abc123...", "expires_at": "2026-03-15T13:00:00Z", "created_at": "2026-03-15T12:00:00Z", "metadata": { "invoice_id": "INV-2026-001", "customer_id": "cust_abc123" } } ``` | Field | Type | Description | |---|---|---| | `id` | string | Unique job identifier. Prefixed with `job_`. | | `status` | string | Current job status. Initially `pending_payment`. | | `type` | string | Mail type: `letter` or `postcard`. | | `color` | boolean | Whether the job is color or B&W. | | `payment_url` | string | Stripe Checkout URL. Redirect the user or return this URL. Expires in 1 hour. | | `amount_cents` | integer | Price in US cents. | | `currency` | string | Always `usd`. | | `webhook_url` | string | The webhook URL that will receive status updates. | | `webhook_secret` | string | HMAC secret for verifying webhook signatures. Store this securely. | | `expires_at` | string | ISO 8601 timestamp when the payment link expires. Job is cancelled if unpaid by this time. | | `created_at` | string | ISO 8601 timestamp when the job was created. | | `metadata` | object | The metadata provided in the request, if any. | #### 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. | --- ## 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 { "id": "job_abc123def456", "status": "cancelled", "cancelled_at": "2026-03-15T12:15:00Z" } ``` #### 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 - 10 requests per IP address per hour - Rate limit applies to POST /v1/print only - DELETE /v1/jobs/:id is not rate limited - Rate-limited responses return HTTP 429 with a `retry_after` field (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 required.** You must provide a publicly reachable HTTPS endpoint. There is no polling endpoint — all status updates are delivered exclusively via webhook. 2. **Store the webhook_secret.** The POST /v1/print response includes a `webhook_secret` field. Store it securely and use it to verify incoming webhook signatures via HMAC-SHA256. 3. **No polling endpoint exists.** Do not attempt to GET a job status. All updates arrive via the webhook you specified. 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