API documentation
Upload PDFs, quote a mailing, then submit only after human review. REST and MCP use the same account, documents and prepaid credits.
Contents
Quickstart
You need a developer account, a scoped irk_ key, mailing credits, and a PDF. These examples use Node.js 22+ and a POSIX shell. All IDs, addresses and response values shown below are illustrative; replace placeholders with your own reviewed data.
- Create an account, create a key with the required scopes, and buy credits.
- Save your letter as
letter.pdf. Createmailing-input.jsonusing the payload below, replacing every address placeholder. A sender name or company, recipient name or company, and both complete mailing addresses are required. - Save the following script as
prepare.mjs. It checks your balance, uploads and registers the PDF, quotes the mailing, and writesreviewed-mailing.json. It does not send mail. - Have a person review the PDF, addresses, options, price and authority to send. Only then follow Submit & status.
// prepare.mjs — Node.js 22+, no dependencies. Does NOT submit mail.
import { readFile, writeFile } from 'node:fs/promises';
const base = process.env.IRIS_API_URL;
const key = process.env.IRIS_API_KEY;
if (!base || !key || key.includes('REPLACE')) {
throw new Error('Set IRIS_API_URL and a real server-side IRIS_API_KEY.');
}
async function api(path, body) {
const response = await fetch(new URL(path, base), {
method: body === undefined ? 'GET' : 'POST',
headers: {
Authorization: 'Bearer ' + key,
'Content-Type': 'application/json'
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const result = await response.json();
if (!response.ok) {
throw new Error(response.status + ' ' + JSON.stringify(result.error));
}
return result.data;
}
const account = await api('/v1/account');
console.log('Account and available US cents:', account);
const pdf = await readFile('letter.pdf');
const upload = await api('/v1/uploads/presign', {
fileName: 'letter.pdf', sizeBytes: pdf.byteLength, kind: 'letter'
});
const put = await fetch(upload.uploadUrl, {
method: 'PUT', headers: upload.uploadHeaders, body: pdf
});
if (!put.ok) throw new Error('PDF upload failed: HTTP ' + put.status);
const document = await api('/v1/documents', {
key: upload.key, fileName: 'letter.pdf', kind: 'letter'
});
console.log('Registered document:', document);
const mailing = JSON.parse(await readFile('mailing-input.json', 'utf8'));
mailing.primaryDocumentId = document.id;
const quote = await api('/v1/mailings/quote', mailing);
await writeFile('reviewed-mailing.json', JSON.stringify(mailing, null, 2));
console.log('Quote in US cents:', JSON.stringify(quote, null, 2));
console.log('STOP: review the PDF, saved payload, price and authority to send.'); Set the API origin and key in your local server environment, then run the preparation script:
export IRIS_API_URL="https://api.irispostalforms.com"
export IRIS_API_KEY="irk_REPLACE_WITH_YOUR_KEY"
# Keep the key in your server environment, never in browser code.
node prepare.mjsAuth & scopes
Base URL: https://api.irispostalforms.com. Send Authorization: Bearer irk_… from trusted server-side software. Never put an API key in a public repository, browser bundle or shared prompt. Use the minimum scopes needed for each integration.
Successful REST responses wrap results in data; failures return error.code and error.message. Send JSON request bodies with Content-Type: application/json. All amounts are integer US cents.
| Method and path | Scope | Result |
|---|---|---|
GET /v1/account | account:read | accountId, creditBalanceCents |
POST /v1/uploads/presign | documents:write | Private PUT URL |
POST /v1/documents | documents:write | Immutable document |
POST /v1/mailings/quote | mailings:read | Price and breakdown |
POST /v1/mailings | mailings:write | Paid order acknowledgement |
GET /v1/mailings/:id | mailings:read | Current status and tracking |
The preparation example needs account:read, documents:write and mailings:read. Submission additionally needs mailings:write. Every MCP request also requires account:read.
Uploads & documents
Upload each PDF in three steps: presign, PUT bytes, register. The upload key is not a document ID. Keep uploads and presigned URLs private.
1. Create a private upload
POST /v1/uploads/presign accepts fileName, exact sizeBytes and kind. A PDF filename may contain up to 180 characters. Maximum file size is 26 MiB (27,262,976 bytes).
{
"data": {
"key": "uploads/api/ACCOUNT_UUID/UPLOAD_UUID.pdf",
"uploadUrl": "https://storage.example.invalid/presigned-upload",
"uploadHeaders": {
"Content-Type": "application/pdf", "If-None-Match": "*"
},
"expiresSeconds": 600
}
} 2. PUT the exact PDF bytes
Use uploadUrl within expiresSeconds (600 seconds) and pass every returned uploadHeaders value, including If-None-Match: *. Do not send your IRIS bearer key to object storage. Confirm a successful PUT before registering. Storage failures do not use the REST JSON envelope.
3. Register and save the document ID
POST /v1/documents accepts the returned key, fileName and the same kind. HTTP 201 returns the validated, immutable document:
{
"data": {
"id": "11111111-1111-4111-8111-111111111111",
"kind": "letter", "pages": 1, "sizeBytes": 48211,
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
} Use data.id as primaryDocumentId (or postcard secondaryDocumentId). Re-registering the same key and kind returns that document with HTTP 201 and an additional accountId. A new version of a PDF needs a new upload and document ID.
PDF dimensions and page limits
letter: US Letter, 8.5 × 11 inches (612 × 792 pt), portrait or landscape. Up to 120 pages can be registered; mailing allows 60 single-sided or 120 double-sided PDF pages.postcard_4x6: each face must be one landscape page, 6.25 × 4.25 inches (450 × 306 pt), including bleed.postcard_6x9: each face must be one landscape page, 9.25 × 6.25 inches (666 × 450 pt), including bleed.postcard_small(LetterStream staging): one 5.5 × 4.25 inch page (396 × 306 pt), with art and message inside a quarter-inch white border. OmitsecondaryDocumentId; the provider generates the address side.postcard_pro(LetterStream staging): two 8.5 × 5.5 inch files (612 × 396 pt). The primary file is the main artwork; the secondary file is the message/address side. Keep its rightmost 3.75 inches clear. Provider size availability is checked before charging.
Every page is checked, with a 1 pt tolerance per dimension. PDFs must open without a password. Lob and LetterStream Pro postcards require different document IDs for the front and back, both of the kind matching the selected size. LetterStream Small uses one document with its art and message on the same side. The API does not render postcard artwork for you.
Quotes
Save this as mailing-input.json and replace every address placeholder. The preparation script fills in primaryDocumentId from registration, then calls POST /v1/mailings/quote with the complete mailing payload.
{
"product": "letter",
"primaryDocumentId": "REPLACED_BY_PREPARE_SCRIPT",
"sender": {
"recipient": "REPLACE WITH SENDER NAME",
"primaryLine": "REPLACE WITH SENDER STREET",
"city": "REPLACE WITH CITY", "state": "MA", "zipCode": "02108"
},
"recipient": {
"recipient": "REPLACE WITH RECIPIENT NAME",
"primaryLine": "REPLACE WITH RECIPIENT STREET",
"city": "REPLACE WITH CITY", "state": "MA", "zipCode": "02109"
},
"options": {
"color": false, "double_sided": true,
"certified": false, "certified_return_receipt": false
}
} Both sender and recipient require a name or company of 1–40 characters after trimming; missing or blank names fail validation. Street lines allow 64 characters; primaryLine needs at least 3. City allows 1–200 characters, state needs two letters, and ZIP accepts 5 digits or ZIP+4 (with or without its hyphen). Optional secondaryLine and urbanization, joined with a comma and space, must fit 64 characters together.
All four letter options are required. Electronic return receipt requires certified: true. Double-sided printing affects sheet count and page limits, but pricing is per PDF page.
Read the server-calculated price
Example HTTP 200 response for one black-and-white letter page, without Certified Mail or a return receipt:
LetterStream staging also accepts letter options format (standard, homeowner, flat), paper (W, Y, B, G, O, R, I), and return_envelope (N, 9RWS, 9LWS, 634). Defaults are standard, white, and none. Certified Mail with or without a receipt requires standard format. Letters support 1–5 PDF pages; flats support 1–60. All remain held test jobs. Non-default options are unavailable on Lob. Return envelopes do not include return postage.
{
"data": {
"priceCents": 320,
"breakdown": [
{ "label": "Base service & First-Class postage", "amountCents": 300 },
{ "label": "Black & white printing × 1", "amountCents": 20 }
]
}
} breakdown contains label and amountCents for each line. A quote does not charge credits, reserve a price or submit mail. Submission recalculates the price; obtain a fresh quote if you change the mailing or delay submission.
For a postcard
Repeat presign, PUT and registration for the front and back using the same postcard kind. Replace both IDs and addresses below. Use size of 4x6 or 6x9; layout of banner, poster or minimal; and font of editorial, modern or handwritten.
The example below uses Lob sizes. LetterStream staging instead accepts 5.5x4.25 (Small, primary file only) and 8.5x5.5 (Pro, two files). All LetterStream submissions are Test Mode jobs held for review; they are not printed or mailed.
{
"product": "postcard",
"primaryDocumentId": "11111111-1111-4111-8111-111111111111",
"secondaryDocumentId": "44444444-4444-4444-8444-444444444444",
"sender": {
"recipient": "REPLACE WITH SENDER NAME",
"primaryLine": "REPLACE WITH SENDER STREET",
"city": "REPLACE WITH CITY", "state": "MA", "zipCode": "02108"
},
"recipient": {
"recipient": "REPLACE WITH RECIPIENT NAME",
"primaryLine": "REPLACE WITH RECIPIENT STREET",
"city": "REPLACE WITH CITY", "state": "MA", "zipCode": "02109"
},
"options": {
"size": "4x6", "layout": "minimal", "font": "modern",
"artwork": { "type": "upload" }
}
} These options describe the already-rendered PDFs. For catalog artwork, use artwork.type: "destination" with a supported destinationId from the OpenAPI schema instead of type: "upload".
Submit & status
Stop until a person has approved the actual PDFs, addresses, print options, quoted total and intent to send. Placeholder data is not a safe test mailing. The preparation script intentionally stops before this step.
After approval, use the exact saved reviewed-mailing.json. Keep its idempotency key with that payload. The command below refuses to overwrite an existing key file; use a separate working directory for each new intended mailing.
# Only AFTER a person approves this exact PDF, payload and quoted price.
# Create and save a key ONCE per intended mailing. Do not repeat on retry.
node --input-type=module -e "import { writeFileSync } from 'node:fs'; writeFileSync('idempotency-key.txt', crypto.randomUUID(), { flag: 'wx' });"
# Retry this command with the SAME saved key and unchanged payload.
curl --fail-with-body --silent --show-error \
"$IRIS_API_URL/v1/mailings" \
-H "Authorization: Bearer $IRIS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(cat idempotency-key.txt)" \
--data-binary @reviewed-mailing.json \
--output submitted.json Read submitted.json. HTTP 201 means a new order was created. HTTP 200 means the completed request was replayed without another charge. Example:
{
"data": {
"orderId": "22222222-2222-4222-8222-222222222222",
"priceCents": 320, "balanceCents": 4680, "replay": false,
"status": "paid",
"statusUrl": "https://irispostalforms.com/order/22222222-2222-4222-8222-222222222222"
}
} balanceCents is the current balance, including on replay. status: "paid" is a creation acknowledgement, not current fulfillment status—even on replay. statusUrl opens the human-readable order page on the consumer site; it is not the REST polling endpoint.
Read current status
# Use data.orderId from a successful submitted.json response, not document.id.
ORDER_ID=$(node --input-type=module -e "import { readFileSync } from 'node:fs'; const r = JSON.parse(readFileSync('submitted.json', 'utf8')); if (!r.data?.orderId) throw new Error('No successful order'); console.log(r.data.orderId);")
curl --fail-with-body --silent --show-error \
"$IRIS_API_URL/v1/mailings/$ORDER_ID" \
-H "Authorization: Bearer $IRIS_API_KEY" {
"data": {
"orderId": "22222222-2222-4222-8222-222222222222",
"product": "letter", "status": "paid", "priceCents": 320,
"trackingNumber": null, "expectedDeliveryDate": null,
"createdAt": "2026-09-08T12:00:00.000Z"
}
} API orders start at paid. Fulfillment can progress through submitted_to_lob, in_production, in_transit and delivered, or report returned, fulfillment_failed or canceled. Updates may skip stages. Shared order states draft, pending_payment and payment_review are not API creation states.
trackingNumber and expectedDeliveryDate can be null. An estimated date is not a delivery guarantee. Poll within the rate limits; do not submit again to check an existing order.
Errors & idempotency
Use one unique Idempotency-Key per intended mailing, up to 200 characters after trimming. Keys are account-wide, even across different API keys. Retrying the same key with the same validated payload returns the original order; changing details returns HTTP 409.
If a submission times out or its result is uncertain, retain the payload and key and retry them unchanged. Do not generate a fresh key to retry: that can create a second mailing and a second charge. Investigate conflicts before deciding to send again.
| HTTP | Code | Next step |
|---|---|---|
| 400 | invalid_request, idempotency_key_required, document_contract_invalid | Correct fields, header or PDF/options mismatch before retrying. |
| 400 | Document registration errors | Check PDF validity, size, dimensions, pages and upload ownership. See OpenAPI for codes. |
| 401 | api_key_invalid | Supply a valid key. The response includes WWW-Authenticate: Bearer. |
| 402 | insufficient_credits | Add credits before submitting. |
| 403 | scope_required | Use a key with the named permission. |
| 404 | document_not_found, mailing_not_found | Check the ID and account ownership. |
| 409 | idempotency_mismatch, mailing_request_failed, mailing_order_missing | Resolve the existing request; do not blindly create a new key. |
| 415 | json_required | Presign requests require Content-Type: application/json. |
| 429 | rate_limit_exceeded | Wait for Retry-After seconds. |
| 500 | internal_error | Check request JSON. For uncertain submissions, preserve the key and payload. |
Limits apply across endpoints: 60 requests per API key per minute and 120 per client IP per minute. Either limit can return 429. The API returns Retry-After on throttled requests; it does not publish remaining-quota headers.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{"error":{"code":"rate_limit_exceeded","message":"Too many API requests."}} Document registration can return parser or storage error text as error.code; do not treat its codes as a closed list. Malformed JSON currently returns 500 internal_error on other JSON endpoints, or 400 at document registration. Parse responses defensively.
MCP
Use a client that supports authenticated remote Streamable HTTP. Set the endpoint and headers below using your client’s documented configuration. Configuration syntax varies; this is connection information, not a universal client config.
Endpoint: https://api.irispostalforms.com/mcp
Transport: Streamable HTTP
Authorization: Bearer irk_REPLACE_WITH_YOUR_KEY
Accept: application/json, text/event-stream Initialize the MCP connection and discover tools using the protocol. Every request requires account:read; tools enforce the additional scopes below. Tool results contain JSON serialized into MCP text content, not the REST data envelope. Tool failures follow MCP error handling.
| Tool | Input | Additional scope |
|---|---|---|
get-account-balance | No fields | None |
create-document-upload | fileName, sizeBytes, kind | documents:write |
register-document | key, fileName, kind | documents:write |
quote-mailing | mailing: complete REST mailing payload | mailings:read |
create-mailing | idempotencyKey, mailing | mailings:write |
get-mailing-status | orderId | mailings:read |
The client still needs to PUT PDF bytes to the presigned URL with all returned headers before calling register-document. Quote and show the exact document, addresses and price to a person before allowing create-mailing. MCP does not remove the human-review or idempotency requirements.
Human review
An agent may prepare documents and payloads, but a person must make the mailing decision. Your application must place its approval step before the REST submission or MCP create-mailing call.
- Show the actual letter or both postcard faces, not only filenames or generated summaries.
- Confirm the complete sender and recipient addresses, print options and current quoted price.
- Confirm authority to contact the recipient, message intent, document and image rights, and applicable legal requirements.
- Confirm available credits and preserve the approved payload with its unique idempotency key.
The person or organization operating the integration remains responsible for what is sent. A successful quote validates document ownership and print compatibility; it is not consent, legal approval or a promise of delivery.
Current boundaries
The API creates individual US letter and postcard orders. It does not provide recipient-list uploads, campaigns, personalization batches, EDDM, Marketing Mail, scheduling, QR-code generation or international mailing.
Back to quickstart