API reference
Reconciled November 9, 2026. The integration functionality review is complete. Read the guides for preparation, asynchronous charging and safe retries. No live requests were run for this revision.
Reconciled November 9, 2026. The integration functionality review is complete. Read the guides for preparation, asynchronous charging and safe retries. No live requests were run for this revision.
Returns batches belonging to the API key's account, newest first.
Pass paged=1 to receive a pagination object alongside the results;
without it, all matching batches are returned in one array.
Request body: None required.
{
"description": "Returns batches belonging to the API key's account, newest first.\n\nPass `paged=1` to receive a `pagination` object alongside the results;\nwithout it, all matching batches are returned in one array.\n\n**Request body: None required.**\n",
"operationId": "listBatches",
"parameters": [
{
"description": "Only batches created on or after this date, `YYYY-MM-DD`. Independent of `to` \u2014 either bound may be given on its own.",
"in": "query",
"name": "from",
"schema": {
"example": "2026-01-01",
"format": "date",
"type": "string"
}
},
{
"description": "Only batches created on or before this date, `YYYY-MM-DD`. Independent of `from`.",
"in": "query",
"name": "to",
"schema": {
"example": "2026-12-31",
"format": "date",
"type": "string"
}
},
{
"description": "Case-insensitive match on the batch `title` and `sftp_filename`. A digits-only term additionally matches the batch's `order_id` exactly.",
"in": "query",
"name": "search",
"schema": {
"example": "batch-07-10-2026",
"type": "string"
}
},
{
"description": "Column to sort by. An unrecognized value falls back to `created_at`.",
"in": "query",
"name": "sortBy",
"schema": {
"default": "created_at",
"enum": [
"created_at",
"updated_at",
"id",
"order_id",
"title",
"status"
],
"example": "title",
"type": "string"
}
},
{
"$ref": "#/components/parameters/SortType"
},
{
"description": "Filter by batch status. This filter takes the integer, while the\nresponse reports the name.\n\n| Value | Status |\n|---|---|\n| `0` | draft |\n| `1` | processing |\n| `2` | printed |\n| `3` | mailed |\n| `4` | in_transit |\n| `5` | delivered |\n| `6` | canceled |\n| `10` | error |\n\n11 = pre-processing.",
"in": "query",
"name": "status",
"schema": {
"enum": [
0,
1,
2,
3,
4,
5,
6,
10,
11
],
"example": 3,
"type": "integer"
}
},
{
"description": "Filter by how the batch was created. Note this filter takes the integer,\nwhile the response reports the name.\n\n| Value | Type |\n|---|---|\n| `0` | manual |\n| `1` | import |\n| `2` | ocr |\n| `3` | json_api |\n| `4` | sftp |\n| `5` | import_simple |\n",
"in": "query",
"name": "type",
"schema": {
"enum": [
0,
1,
2,
3,
4,
5
],
"example": 3,
"type": "integer"
}
},
{
"description": "Set to `1` to paginate the response.",
"in": "query",
"name": "paged",
"schema": {
"enum": [
1
],
"example": 1,
"type": "integer"
}
},
{
"description": "1-based page number. Only meaningful with `paged`.",
"in": "query",
"name": "page",
"schema": {
"default": 1,
"example": 1,
"minimum": 1,
"type": "integer"
}
},
{
"description": "Records per page, capped at 100. Only meaningful with `paged`.",
"in": "query",
"name": "perPage",
"schema": {
"default": 100,
"example": 25,
"maximum": 100,
"minimum": 1,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"paged": {
"summary": "With paged=1",
"value": {
"batches": [
{
"async_fullfill_requested": false,
"created_at": "2026-07-10T09:36:07Z",
"id": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"in_loading": false,
"order_id": 15511,
"sftp_filename": "",
"status": "processing",
"title": "batch-07-10-2026",
"type": "json_api",
"updated_at": "2026-07-10T09:36:08Z"
}
],
"is_successful": true,
"pagination": {
"current_page": 1,
"per_page": 2,
"total": 323,
"total_pages": 162
}
}
},
"unpaged": {
"summary": "Without paged \u2014 every matching batch, no pagination object",
"value": {
"batches": [
{
"async_fullfill_requested": false,
"created_at": "2026-07-10T09:36:07Z",
"id": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"in_loading": false,
"order_id": 15511,
"sftp_filename": "",
"status": "processing",
"title": "batch-07-10-2026",
"type": "json_api",
"updated_at": "2026-07-10T09:36:08Z"
}
],
"is_successful": true
}
}
},
"schema": {
"properties": {
"batches": {
"items": {
"$ref": "#/components/schemas/Batch"
},
"type": "array"
},
"is_successful": {
"example": true,
"type": "boolean"
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
},
"required": [
"is_successful",
"batches"
],
"type": "object"
}
}
},
"description": "Matching batches. The `pagination` object is present only when `paged` was set."
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "A query parameter could not be parsed."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
},
"summary": "List batches",
"tags": [
"Batches"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl \"https://api.uploadletters.com/batches?paged=1&perPage=25\" \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Create a real draft batch. Start with about 100 recipients per POST or recipient PATCH as an operational recommendation. The API enforces no fixed recipient-count limit on either request or on the batch. Append additional groups to the same draft before fulfillment. Invalid recipients are returned in invalid_letters while valid recipients are saved. Repeating POST creates a new batch; no idempotency key. Fulfill separately after reconciling recipients and PDFs.
{
"description": "Create a real draft batch. Start with about 100 recipients per POST or recipient PATCH as an operational recommendation. The API enforces no fixed recipient-count limit on either request or on the batch. Append additional groups to the same draft before fulfillment. Invalid recipients are returned in invalid_letters while valid recipients are saved. Repeating POST creates a new batch; no idempotency key. Fulfill separately after reconciling recipients and PDFs.",
"operationId": "createBatch",
"requestBody": {
"content": {
"application/json": {
"example": {
"address_verification_type": "NONE",
"batch_type": "letter",
"recipients": [
{
"unique_id": "example-001",
"letter_id": "example-letter-001",
"address": {
"first_name": "Example",
"last_name": "Recipient",
"address_line_1": "123 Example Street",
"city": "Frisco",
"state": "TX",
"zip": "75036"
},
"return_address": {
"company": "Example Business",
"address_line_1": "456 Example Avenue",
"city": "Frisco",
"state": "TX",
"zip": "75036"
},
"mailing_options": {
"service_type": "fc",
"envelope_type": "10",
"is_colored": false,
"is_double_sided": false
},
"pdf_file_config": {
"pdf_name": "sample-letter.pdf"
}
}
]
},
"schema": {
"$ref": "#/components/schemas/BatchWriteBody"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"allAccepted": {
"summary": "Every recipient valid",
"value": {
"batch_id": "bb00423d-d830-41be-b60b-dde443312432",
"is_successful": true
}
},
"partiallyAccepted": {
"summary": "Third recipient rejected; the other two were accepted",
"value": {
"batch_id": "bb00423d-d830-41be-b60b-dde443312432",
"invalid_letters": {
"2": {
"address.zip": [
"The zip code field has an invalid value."
]
}
},
"is_successful": true
}
}
},
"schema": {
"properties": {
"batch_id": {
"example": "bb00423d-d830-41be-b60b-dde443312432",
"format": "uuid",
"type": "string"
},
"invalid_letters": {
"$ref": "#/components/schemas/ValidationErrors"
},
"is_successful": {
"example": true,
"type": "boolean"
}
},
"required": [
"is_successful",
"batch_id"
],
"type": "object"
}
}
},
"description": "Batch created and accepted for processing. `invalid_letters` appears only when some recipients were rejected; the valid ones were still accepted."
},
"400": {
"content": {
"application/json": {
"example": {
"invalid_letters": {
"0": {
"address.city": [
"The city field is required."
],
"address.state": [
"The state field is required."
]
}
},
"is_successful": false,
"message": "Invalid data in request body."
},
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/Error"
},
{
"properties": {
"invalid_letters": {
"$ref": "#/components/schemas/ValidationErrors"
}
},
"type": "object"
}
]
}
}
},
"description": "Every recipient failed validation. No batch was created."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The account is not permitted to use a requested feature, such as check mailing or an affidavit."
},
"422": {
"content": {
"application/json": {
"example": {
"is_successful": false,
"message": "address_verification_type must be one of (NONE, CASS_NCOA_LOOKUP, CASS_NCOA_BEST, CASS_NCOA_BOTH)."
},
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The request body was unusable \u2014 malformed JSON, an unrecognized `address_verification_type` or `affidavit`, or a recipient limit exceeded."
}
},
"summary": "Create a batch",
"tags": [
"Batches"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl -X POST https://api.uploadletters.com/batches \\\n -H \"x-api-key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"batch_type\": \"letter\",\n \"address_verification_type\": \"NONE\",\n \"recipients\": [\n {\n \"unique_id\": \"1\",\n \"letter_id\": \"letter_1\",\n \"address\": {\n \"first_name\": \"Spencer\",\n \"last_name\": \"Tyler\",\n \"address_line_1\": \"3032 Arjun Views\",\n \"address_line_2\": \"Apt. 470\",\n \"city\": \"Reillyton\",\n \"state\": \"PA\",\n \"zip\": \"96504-0522\"\n },\n \"return_address\": {\n \"first_name\": \"Sterling\",\n \"last_name\": \"Law Group\",\n \"company\": \"Sterling Law Group, P.A.\",\n \"address_line_1\": \"456 Gulf Breeze Way\",\n \"address_line_2\": \"Suite 200\",\n \"city\": \"Naples\",\n \"state\": \"FL\",\n \"zip\": \"34102\"\n },\n \"pdf_file_config\": {\n \"pdf_name\": \"Sample Letter.pdf\",\n \"pdf_page_numbers\": \"1-3\"\n },\n \"mailing_options\": {\n \"envelope_type\": \"10\",\n \"service_type\": \"cm-rre\",\n \"is_colored\": false,\n \"is_double_sided\": false\n }\n }\n ]\n }'\n"
}
]
}
Returns the current wallet balance for the account owning the API key, together with its transaction history. Batches are charged against this balance when you call PUT /batches/{processId}/fulfill.
{
"description": "Returns the current wallet balance for the account owning the API key, together with its transaction history. Batches are charged against this balance when you call `PUT /batches/{processId}/fulfill`.",
"operationId": "listTransactions",
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"balance": 484168.83,
"is_successful": true,
"transactions": [
{
"amount": -864,
"confirmed": true,
"created_at": "2026-07-10T04:32:57Z",
"id": 7753,
"meta": {
"description": "Purchase of Batch",
"title": "batch-07-10-2026"
},
"type": "withdraw",
"updated_at": "2026-07-10T04:32:57Z",
"user_id": "74a54d21-e451-4f09-9090-4462ce7edf02",
"uuid": "8d631ca1-024e-47db-af13-f55c7def1e2b",
"wallet_id": 247
}
]
},
"schema": {
"properties": {
"balance": {
"description": "Current wallet balance, in dollars.",
"example": 484168.83,
"format": "double",
"type": "number"
},
"is_successful": {
"example": true,
"type": "boolean"
},
"transactions": {
"items": {
"$ref": "#/components/schemas/Transaction"
},
"type": "array"
}
},
"required": [
"is_successful",
"balance",
"transactions"
],
"type": "object"
}
}
},
"description": "Current `balance` and the account's full transaction history, newest\nfirst.\n\nMind the units: `balance` is a decimal number of **dollars**, while each\ntransaction's `amount` is a signed integer in **cents**.\n\nThis endpoint returns every transaction on the account and is not\npaginated, so the response can be large.\n"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The request could not be attributed to an account."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
},
"summary": "Get wallet balance and transactions",
"tags": [
"Transactions"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl https://api.uploadletters.com/batches/transactions \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Returns a single batch with its current status, plus counters describing how much background processing is still outstanding.
{
"description": "Returns a single batch with its current status, plus counters describing how much background processing is still outstanding.",
"operationId": "getBatch",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"active_bg_tasks_count": 0,
"batch": {
"async_fullfill_requested": false,
"created_at": "2026-07-10T09:36:07Z",
"id": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"in_loading": true,
"order_id": 15511,
"sftp_filename": "",
"status": "",
"title": "batch-07-10-2026",
"type": "json_api",
"updated_at": "2026-07-10T09:36:07Z"
},
"current_letters_count": 0,
"is_successful": true,
"minimum_letters_count": 2
},
"schema": {
"properties": {
"active_bg_tasks_count": {
"description": "Background tasks still running for this batch. `0` means processing has settled.",
"example": 0,
"type": "integer"
},
"batch": {
"$ref": "#/components/schemas/Batch"
},
"current_letters_count": {
"description": "Letters produced so far.",
"example": 0,
"type": "integer"
},
"is_successful": {
"example": true,
"type": "boolean"
},
"minimum_letters_count": {
"description": "Letters expected, i.e. the number of accepted recipients.",
"example": 2,
"type": "integer"
}
},
"required": [
"is_successful",
"batch"
],
"type": "object"
}
}
},
"description": "The batch, with counters describing outstanding background work."
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The batch id is malformed."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
}
},
"summary": "Get a batch",
"tags": [
"Batches"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl https://api.uploadletters.com/batches/$BATCH_ID \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Returns 200 when queued; charging and fulfillment happen asynchronously. Insufficient funds is reported by notification and leaves the order in Pre-Processing, not a synchronous 422. With manual review enabled, returns 422 "Batch should be reviewed at web portal in incomplete orders.", moves the draft to Incomplete Orders and does not charge. Invalid/missing PDFs are checked before review. Same-batch fulfillment retries do not charge paid orders twice. Poll existing state; no webhooks.
{
"description": "Returns 200 when queued; charging and fulfillment happen asynchronously. Insufficient funds is reported by notification and leaves the order in Pre-Processing, not a synchronous 422. With manual review enabled, returns 422 \"Batch should be reviewed at web portal in incomplete orders.\", moves the draft to Incomplete Orders and does not charge. Invalid/missing PDFs are checked before review. Same-batch fulfillment retries do not charge paid orders twice. Poll existing state; no webhooks.",
"operationId": "fulfillBatch",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"is_successful": true,
"message": "Batch put to queue and will be charged and fulfilled."
},
"schema": {
"properties": {
"is_successful": {
"example": true,
"type": "boolean"
},
"message": {
"type": "string"
}
},
"required": [
"is_successful",
"message"
],
"type": "object"
}
}
},
"description": "Batch queued for charging and printing."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
},
"422": {
"content": {
"application/json": {
"examples": {
"alreadyFulfilled": {
"summary": "Fulfil already requested",
"value": {
"is_successful": false,
"message": "Batch fulfilling already requested."
}
},
"missingPdfs": {
"summary": "A recipient references a PDF that was never uploaded",
"value": {
"extra_pdf_files": [],
"is_successful": false,
"message": "Batch contains invalid letters. Some letters have processing errors, missing PDF files, or a PDF name mismatch between batch configuration and uploaded files.",
"missed_pdf_files": [
"Sample Letter.pdf"
],
"not_mapped_recipients": [
{
"error_message": "",
"letter_id": "letter_1",
"pdf_file_config": {
"pdf_name": "Sample Letter.pdf",
"pdf_page_numbers": "1-3"
},
"unique_id": "1"
}
]
}
},
"manualReview": {
"summary": "Manual review required",
"value": {
"is_successful": false,
"message": "Batch should be reviewed at web portal in incomplete orders."
}
}
},
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/Error"
},
{
"$ref": "#/components/schemas/LetterMappingResult"
}
]
}
}
},
"description": "Validation/missing PDFs, already-requested/already-fulfilled, or company-enabled manual review. Insufficient funds is asynchronous on this endpoint."
}
},
"summary": "Queue charging and fulfillment",
"tags": [
"Batches"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "# Charges the wallet and sends the batch to production. Irreversible.\n# No request body.\ncurl -X PUT https://api.uploadletters.com/batches/$BATCH_ID/fulfill \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Returns the letters in a batch once it has been processed, including USPS tracking data, links to the rendered letter images, and Return Receipt Electronic (RRE) images where applicable. Image links are time-limited signed URLs — fetch them promptly rather than storing them.
Request body: None required.
{
"description": "Returns the letters in a batch once it has been processed, including USPS\ntracking data, links to the rendered letter images, and Return Receipt\nElectronic (RRE) images where applicable. Image links are time-limited\nsigned URLs \u2014 fetch them promptly rather than storing them.\n\n**Request body: None required.**\n",
"operationId": "listLetters",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
},
{
"$ref": "#/components/parameters/LetterType"
},
{
"$ref": "#/components/parameters/LetterServiceType"
},
{
"$ref": "#/components/parameters/LetterStatus"
},
{
"$ref": "#/components/parameters/LetterSearchTerm"
},
{
"$ref": "#/components/parameters/LetterSortBy"
},
{
"$ref": "#/components/parameters/SortType"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"beforePrinting": {
"summary": "Batch not yet printed \u2014 both arrays are null",
"value": {
"affidavit_images": null,
"is_successful": true,
"letters": null
}
},
"delivered": {
"summary": "A delivered Certified Mail with RRE letter",
"value": {
"affidavit_images": [],
"is_successful": true,
"letters": [
{
"code": "9214890321486300120500",
"created_at": "2026-08-04T09:36:07Z",
"id": 72272,
"is_colored": false,
"is_double_sided": false,
"is_rre": true,
"letter_image": "https://example-bucket.s3.amazonaws.com/letter.pdf?X-Amz-Signature=REDACTED",
"price": "8.64",
"recipient": {
"address_line_1": "3032 Arjun Views",
"address_line_2": "Apt. 470",
"city": "Reillyton",
"first_name": "Spencer",
"last_name": "Tyler",
"state": "PA",
"zip": "96504-0522"
},
"return_mail_image": [],
"rre_image": "https://example-bucket.s3.amazonaws.com/rre.pdf?X-Amz-Signature=REDACTED",
"serial_number": 1010,
"status": 5,
"tracking_info": {
"done": true,
"error": null,
"logs": [
{
"date": "Dec 27, 2024",
"description": "Delivered",
"location": "Reillyton, PA"
},
{
"date": "Dec 23, 2024",
"description": "Accepted",
"location": "Frisco, TX"
}
]
},
"type": "cm-10-rre",
"updated_at": "2026-08-04T09:36:40Z"
}
]
}
}
},
"schema": {
"properties": {
"affidavit_images": {
"description": "Signed URLs to affidavit PDFs. `null` when the batch has no affidavit.",
"items": {
"type": "string"
},
"nullable": true,
"type": "array"
},
"is_successful": {
"example": true,
"type": "boolean"
},
"letters": {
"description": "`null` until the batch has been printed. Each letter carries its rendered image, USPS tracking, and the resolved sender/recipient.",
"items": {
"$ref": "#/components/schemas/Letter"
},
"nullable": true,
"type": "array"
}
},
"required": [
"is_successful"
],
"type": "object"
}
}
},
"description": "Letters in the batch, plus `affidavit_images` when the batch carries an affidavit. Both are `null` \u2014 not `[]` \u2014 until the print pipeline has produced them.",
"x-example-note": "Trimmed for readability. Signed-URL query strings are truncated, the tracking history is cut to two events, and the always-present `sender` and `check` keys are omitted \u2014 on a standard letter mailing both are empty objects. `tracking_info` is shown in its normalized form; most letters return the carrier's raw payload there instead."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The batch id is unusable, or the batch could not be read."
}
},
"summary": "List letters in a batch",
"tags": [
"Letters"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl https://api.uploadletters.com/batches/$BATCH_ID/letters \\\n -H \"x-api-key: $API_KEY\"\n\n# Filtered\ncurl \"https://api.uploadletters.com/batches/$BATCH_ID/letters?serviceType=cm-rre&status=5\" \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Uploads the PDF files for letters already in the batch.
This does not create letters. A letter exists because a recipient
exists. This call supplies the artwork for those letters, binding each PDF
to the recipients whose pdf_file_config.pdf_name matches the uploaded
filename exactly.
curl --request PATCH \
--url "https://api.uploadletters.com/batches/{batchId}/letters" \
--header "x-api-key: YOUR_API_KEY" \
--form "letters[]=@Sample Letter.pdf"
curl --request PATCH \
--url "https://api.uploadletters.com/batches/{batchId}/letters" \
--header "x-api-key: YOUR_API_KEY" \
--form "letters[]=@Spencer_Tyler.pdf" \
--form "letters[]=@Amelia_Reyes.pdf"
multipart/form-data.letters[].pdf_name values
included in the recipient data, and must be unique within the batch.
Build around your company’s Single PDF Max Size setting, which defaults to 15 MB per PDF. A setting of 0 means the company-level per-file limit is disabled. The API does not enforce the previously documented 20 MB total request limit. No fixed cumulative API upload or total-page cap is documented. Per-letter envelope capacities still apply. Coordinate unusually large uploads with UploadLetters.Callable repeatedly, and freely interleaved with PATCH .../recipients,
until the batch is fulfilled.
The response tells you what did and did not line up: missed_pdf_files are
filenames recipients expect but you have not uploaded; extra_pdf_files are
files you uploaded that no recipient references.
{
"description": "Uploads the PDF files for letters already in the batch.\n\nThis does **not** create letters. A letter exists because a recipient\nexists. This call supplies the artwork for those letters, binding each PDF\nto the recipients whose `pdf_file_config.pdf_name` matches the uploaded\nfilename exactly.\n\n### One file\n\n```bash\ncurl --request PATCH \\\n --url \"https://api.uploadletters.com/batches/{batchId}/letters\" \\\n --header \"x-api-key: YOUR_API_KEY\" \\\n --form \"letters[]=@Sample Letter.pdf\"\n```\n\n### Multiple files\n\n```bash\ncurl --request PATCH \\\n --url \"https://api.uploadletters.com/batches/{batchId}/letters\" \\\n --header \"x-api-key: YOUR_API_KEY\" \\\n --form \"letters[]=@Spencer_Tyler.pdf\" \\\n --form \"letters[]=@Amelia_Reyes.pdf\"\n```\n\n- The request uses `multipart/form-data`.\n- The file upload field is `letters[]`.\n- One or multiple PDF files can be uploaded.\n- The uploaded filenames need to exactly match the `pdf_name` values\n included in the recipient data, and must be unique within the batch.\nBuild around your company’s Single PDF Max Size setting, which defaults to 15 MB per PDF. A setting of 0 means the company-level per-file limit is disabled. The API does not enforce the previously documented 20 MB total request limit. No fixed cumulative API upload or total-page cap is documented. Per-letter envelope capacities still apply. Coordinate unusually large uploads with UploadLetters.\n- PDF files are uploaded as files. They are not inserted into the JSON\n request as Base64.\n\nCallable repeatedly, and freely interleaved with `PATCH .../recipients`,\nuntil the batch is fulfilled.\n\nThe response tells you what did and did not line up: `missed_pdf_files` are\nfilenames recipients expect but you have not uploaded; `extra_pdf_files` are\nfiles you uploaded that no recipient references.\n",
"operationId": "uploadLetterFiles",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"encoding": {
"letters[]": {
"contentType": "application/pdf"
}
},
"schema": {
"properties": {
"letters[]": {
"description": "One or more PDF files. Repeat the field for each file.",
"items": {
"format": "binary",
"type": "string"
},
"type": "array"
}
},
"required": [
"letters[]"
],
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"extra_pdf_files": [
"nobody_wants_me.pdf"
],
"is_successful": true,
"missed_pdf_files": [],
"not_mapped_recipients": []
},
"schema": {
"$ref": "#/components/schemas/LetterMappingResult"
}
}
},
"description": "Files accepted. The three arrays describe what still does not line up \u2014 all empty means the batch is ready to fulfill."
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The multipart body could not be read."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"422": {
"content": {
"application/json": {
"example": {
"is_successful": false,
"message": "Batch fulfil was requested. You cannot add more letters after that."
},
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The files could not be accepted. Common causes:\n\n- `Batch fulfil was requested. You cannot add more letters after that.`\n- `Invalid batch id`\n- A file is not a PDF, or exceeds the account's per-file size limit.\n"
}
},
"summary": "Upload letter PDFs",
"tags": [
"Letters"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "# One file\ncurl -X PATCH https://api.uploadletters.com/batches/$BATCH_ID/letters \\\n -H \"x-api-key: $API_KEY\" \\\n -F \"letters[]=@Sample Letter.pdf\"\n\n# Several files - repeat letters[] once per file\ncurl -X PATCH https://api.uploadletters.com/batches/$BATCH_ID/letters \\\n -H \"x-api-key: $API_KEY\" \\\n -F \"letters[]=@Spencer_Tyler.pdf\" \\\n -F \"letters[]=@Amelia_Reyes.pdf\"\n"
}
]
}
A paginated view of GET /batches/{processId}/letters. Prefer this for large batches — the unpaginated call returns every letter, with tracking history, in one response.
Takes the same filters and sorting as the unpaginated call. pagination describes the filtered result set, not the whole batch.
{
"description": "A paginated view of `GET /batches/{processId}/letters`. Prefer this for large batches \u2014 the unpaginated call returns every letter, with tracking history, in one response.\nTakes the same filters and sorting as the unpaginated call. `pagination` describes the filtered result set, not the whole batch.",
"operationId": "listLettersPaged",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
},
{
"$ref": "#/components/parameters/LetterType"
},
{
"$ref": "#/components/parameters/LetterServiceType"
},
{
"$ref": "#/components/parameters/LetterStatus"
},
{
"$ref": "#/components/parameters/LetterSearchTerm"
},
{
"$ref": "#/components/parameters/LetterSortBy"
},
{
"$ref": "#/components/parameters/SortType"
},
{
"$ref": "#/components/parameters/Page"
},
{
"$ref": "#/components/parameters/PerPage"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"affidavit_images": null,
"is_successful": true,
"letters": null,
"pagination": {
"current_page": 1,
"per_page": 2,
"total": 0,
"total_pages": 0
}
},
"schema": {
"properties": {
"affidavit_images": {
"items": {
"type": "string"
},
"nullable": true,
"type": "array"
},
"is_successful": {
"example": true,
"type": "boolean"
},
"letters": {
"items": {
"$ref": "#/components/schemas/Letter"
},
"nullable": true,
"type": "array"
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
},
"required": [
"is_successful",
"pagination"
],
"type": "object"
}
}
},
"description": "A page of letters, with a `pagination` object."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The batch id is unusable, or the batch could not be read."
}
},
"summary": "List letters, paginated",
"tags": [
"Letters"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl \"https://api.uploadletters.com/batches/$BATCH_ID/letters/paged?page=1&perPage=25\" \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Returns the recipients in a batch and, crucially, whether each one has been matched to an uploaded PDF.
Call this before PUT .../fulfill. The batch is ready when:
total_not_mapped_recipients is 0, andmissed_images is empty.mapped_recipients are matched to a PDF. not_mapped_recipients are not,
each carrying an error_message explaining why. missed_images lists
pdf_name values that recipients referenced but which were never uploaded.
ncoa_lookup reports address changes found during address verification.
Note: this endpoint returns recipient data only for batches created through this API. Batches created by other means (e.g. manually) will not return recipient data here.
{
"description": "Returns the recipients in a batch and, crucially, **whether each one has\nbeen matched to an uploaded PDF**.\n\nCall this before `PUT .../fulfill`. The batch is ready when:\n\n- `total_not_mapped_recipients` is `0`, and\n- `missed_images` is empty.\n\n`mapped_recipients` are matched to a PDF. `not_mapped_recipients` are not,\neach carrying an `error_message` explaining why. `missed_images` lists\n`pdf_name` values that recipients referenced but which were never uploaded.\n\n`ncoa_lookup` reports address changes found during address verification.\n\n**Note:** this endpoint returns recipient data only for batches created\nthrough this API. Batches created by other means (e.g. manually) will not\nreturn recipient data here.\n",
"operationId": "listRecipients",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
},
{
"description": "When set, `total_not_mapped_recipients` and `not_mapped_recipients` are omitted from the response entirely. Useful once the batch is known good; do not use it while verifying.",
"in": "query",
"name": "onlyValid",
"schema": {
"example": true,
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"beforeUpload": {
"summary": "PDFs not yet uploaded \u2014 the batch is not ready to fulfill",
"value": {
"is_successful": true,
"mapped_recipients": [],
"missed_images": [
"Sample Letter.pdf"
],
"ncoa_lookup": null,
"not_mapped_recipients": [
{
"error_message": "",
"letter_id": "letter_1",
"pdf_file_config": {
"pdf_name": "Sample Letter.pdf",
"pdf_page_numbers": "1-3",
"pdf_process_type": ""
},
"shadow": false,
"unique_id": "1"
}
],
"total_not_mapped_recipients": 2,
"total_recipients": 2
}
},
"onlyValid": {
"summary": "With onlyValid=1 \u2014 the not-mapped keys are absent",
"value": {
"is_successful": true,
"mapped_recipients": [
{
"error_message": "",
"letter_id": "letter_1",
"shadow": false,
"unique_id": "1"
}
],
"missed_images": [],
"ncoa_lookup": null,
"total_recipients": 3
}
}
},
"schema": {
"properties": {
"is_successful": {
"example": true,
"type": "boolean"
},
"mapped_recipients": {
"description": "Recipients matched to an uploaded PDF.",
"items": {
"$ref": "#/components/schemas/Recipient"
},
"type": "array"
},
"missed_images": {
"description": "`pdf_name` values referenced by recipients but never uploaded.",
"example": [],
"items": {
"type": "string"
},
"type": "array"
},
"ncoa_lookup": {
"description": "Address changes found during NCOALink verification. `null` when `address_verification_type` was `NONE`.",
"items": {
"additionalProperties": true,
"type": "object"
},
"nullable": true,
"type": "array"
},
"not_mapped_recipients": {
"description": "Recipients with no matching PDF. Omitted entirely when `onlyValid` is set.",
"items": {
"$ref": "#/components/schemas/Recipient"
},
"type": "array"
},
"total_not_mapped_recipients": {
"description": "Omitted entirely when `onlyValid` is set.",
"example": 0,
"type": "integer"
},
"total_recipients": {
"example": 3,
"type": "integer"
}
},
"required": [
"is_successful",
"total_recipients",
"mapped_recipients",
"missed_images"
],
"type": "object"
}
}
},
"description": "Recipients with mapping state. `not_mapped_recipients` and `total_not_mapped_recipients` are omitted when `onlyValid` is set."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The batch id is unusable, or the batch could not be read."
}
},
"summary": "List recipients and check letter mapping",
"tags": [
"Recipients"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "# Verify the batch before fulfilling: ready when\n# total_not_mapped_recipients is 0 and missed_images is empty.\ncurl https://api.uploadletters.com/batches/$BATCH_ID/recipients \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
Append recipients before fulfillment. Start with about 100 recipients per POST or recipient PATCH as an operational recommendation. The API enforces no fixed recipient-count limit on either request or on the batch. Append additional groups to the same draft before fulfillment. Resend only corrected rejected rows. Repeating accepted rows duplicates them. Explicitly send address_verification_type on every call: omission uses the company default rather than preserving the original batch mode.
{
"description": "Append recipients before fulfillment. Start with about 100 recipients per POST or recipient PATCH as an operational recommendation. The API enforces no fixed recipient-count limit on either request or on the batch. Append additional groups to the same draft before fulfillment. Resend only corrected rejected rows. Repeating accepted rows duplicates them. Explicitly send address_verification_type on every call: omission uses the company default rather than preserving the original batch mode.",
"operationId": "addRecipients",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
}
],
"requestBody": {
"content": {
"application/json": {
"example": {
"recipients": [
{
"address": {
"address_line_1": "32704 Sven Spurs",
"city": "New Estaton",
"first_name": "June",
"last_name": "Kim",
"state": "IL",
"zip": "44989"
},
"letter_id": "letter_4",
"mailing_options": {
"envelope_type": "10",
"is_colored": false,
"is_double_sided": false,
"service_type": "cm-rre"
},
"pdf_file_config": {
"pdf_name": "Sample Letter.pdf",
"pdf_page_numbers": "10-12"
},
"return_address": {
"address_line_1": "456 Gulf Breeze Way",
"address_line_2": "Suite 200",
"city": "Naples",
"company": "Sterling Law Group, P.A.",
"first_name": "Sterling",
"last_name": "Law Group",
"state": "FL",
"zip": "34102"
},
"unique_id": "4"
}
]
},
"schema": {
"$ref": "#/components/schemas/BatchWriteBody"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"batch_id": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"is_successful": true
},
"schema": {
"properties": {
"batch_id": {
"example": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"format": "uuid",
"type": "string"
},
"invalid_letters": {
"$ref": "#/components/schemas/ValidationErrors"
},
"is_successful": {
"example": true,
"type": "boolean"
}
},
"required": [
"is_successful",
"batch_id"
],
"type": "object"
}
}
},
"description": "Recipients accepted. Carries `batch_id`, plus `invalid_letters` if some were rejected."
},
"400": {
"content": {
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/Error"
},
{
"properties": {
"invalid_letters": {
"$ref": "#/components/schemas/ValidationErrors"
}
},
"type": "object"
}
]
}
}
},
"description": "Every recipient failed validation, or the batch has already been fulfilled."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The account is not permitted to use a requested feature."
},
"422": {
"content": {
"application/json": {
"example": {
"is_successful": false,
"message": "Batch fulfil was requested. You cannot add more letters after that."
},
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The recipients could not be added. Common causes:\n\n- `Batch fulfil was requested. You cannot add more letters after that.`\n- `Invalid batch id`\n- An unrecognized `address_verification_type` or `affidavit`.\n"
}
},
"summary": "Add recipients to a batch",
"tags": [
"Recipients"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl -X PATCH https://api.uploadletters.com/batches/$BATCH_ID/recipients \\\n -H \"x-api-key: $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"recipients\": [\n {\n \"unique_id\": \"4\",\n \"letter_id\": \"letter_4\",\n \"address\": {\n \"first_name\": \"June\",\n \"last_name\": \"Kim\",\n \"address_line_1\": \"32704 Sven Spurs\",\n \"city\": \"New Estaton\",\n \"state\": \"IL\",\n \"zip\": \"44989\"\n },\n \"return_address\": {\n \"first_name\": \"Sterling\",\n \"last_name\": \"Law Group\",\n \"company\": \"Sterling Law Group, P.A.\",\n \"address_line_1\": \"456 Gulf Breeze Way\",\n \"address_line_2\": \"Suite 200\",\n \"city\": \"Naples\",\n \"state\": \"FL\",\n \"zip\": \"34102\"\n },\n \"pdf_file_config\": {\n \"pdf_name\": \"Sample Letter.pdf\",\n \"pdf_page_numbers\": \"10-12\"\n },\n \"mailing_options\": {\n \"envelope_type\": \"10\",\n \"service_type\": \"cm-rre\",\n \"is_colored\": false,\n \"is_double_sided\": false\n }\n }\n ]\n }'\n"
}
]
}
A paginated view of the batch's recipients. Unlike GET .../recipients,
this returns a flat recipients array with a pagination object, and does
not report letter-mapping state. Use GET .../recipients to verify a batch
before fulfilling it.
Searching and filtering here read the recipient as you submitted it, so
they work from the moment a recipient is added — including on a draft batch
whose letters do not exist yet. If address verification later rewrites an
address, search still matches what you sent, not what was mailed.
pagination describes the filtered result set, not the whole batch.
Note: this endpoint returns recipient data only for batches created through this API. Batches created by other means (e.g. manually) will not return recipient data here.
{
"description": "A paginated view of the batch's recipients. Unlike `GET .../recipients`,\nthis returns a flat `recipients` array with a `pagination` object, and does\nnot report letter-mapping state. Use `GET .../recipients` to verify a batch\nbefore fulfilling it.\n\nSearching and filtering here read the recipient **as you submitted it**, so\nthey work from the moment a recipient is added \u2014 including on a draft batch\nwhose letters do not exist yet. If address verification later rewrites an\naddress, `search` still matches what you sent, not what was mailed.\n\n`pagination` describes the filtered result set, not the whole batch.\n\n**Note:** this endpoint returns recipient data only for batches created\nthrough this API. Batches created by other means (e.g. manually) will not\nreturn recipient data here.\n",
"operationId": "listRecipientsPaged",
"parameters": [
{
"$ref": "#/components/parameters/ProcessId"
},
{
"description": "Case-insensitive search across the recipient's first name, last name, full name, company, address lines, city, state and zip.",
"in": "query",
"name": "search",
"schema": {
"example": "Tyler",
"type": "string"
}
},
{
"description": "Filter by the `service_type` submitted in `mailing_options`.",
"in": "query",
"name": "serviceType",
"schema": {
"enum": [
"fc",
"cm",
"cm-rre",
"pm",
"pm-box",
"intl-fc",
"rim"
],
"example": "cm-rre",
"type": "string"
}
},
{
"description": "Exact match on the recipient's `unique_id`.",
"in": "query",
"name": "uniqueId",
"schema": {
"example": "1",
"type": "string"
}
},
{
"description": "Column to sort by. An unrecognized value falls back to `id`, which is the order the recipients were added.",
"in": "query",
"name": "sortBy",
"schema": {
"default": "id",
"enum": [
"id",
"import_order",
"created_at"
],
"example": "import_order",
"type": "string"
}
},
{
"$ref": "#/components/parameters/SortType"
},
{
"$ref": "#/components/parameters/Page"
},
{
"$ref": "#/components/parameters/PerPage"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"example": {
"is_successful": true,
"pagination": {
"current_page": 1,
"per_page": 2,
"total": 3,
"total_pages": 2
},
"recipients": [
{
"error_message": "",
"letter_id": "letter_1",
"pdf_file_config": {
"pdf_name": "Sample Letter.pdf",
"pdf_page_numbers": "1-3",
"pdf_process_type": ""
},
"shadow": false,
"unique_id": "1"
}
]
},
"schema": {
"properties": {
"is_successful": {
"example": true,
"type": "boolean"
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
},
"recipients": {
"items": {
"$ref": "#/components/schemas/Recipient"
},
"type": "array"
}
},
"required": [
"is_successful",
"recipients",
"pagination"
],
"type": "object"
}
}
},
"description": "A page of recipients, with a `pagination` object. Note the key is `recipients` here, not `mapped_recipients`."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/BatchNotFound"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
},
"description": "The batch id is unusable, or the batch could not be read."
}
},
"summary": "List recipients, paginated",
"tags": [
"Recipients"
],
"x-codeSamples": [
{
"lang": "cURL",
"source": "curl \"https://api.uploadletters.com/batches/$BATCH_ID/recipients/paged?page=1&perPage=25\" \\\n -H \"x-api-key: $API_KEY\"\n"
}
]
}
A postal address.
first_name and last_name are not individually required, but supply at
least one of them or company — otherwise the envelope has nobody to address.
For international mail (service_type of intl-fc or rim), country is
required and state is not validated.
{
"description": "A postal address.\n\n`first_name` and `last_name` are not individually required, but supply at\nleast one of them or `company` \u2014 otherwise the envelope has nobody to address.\n\nFor international mail (`service_type` of `intl-fc` or `rim`), `country` is\nrequired and `state` is not validated.\n",
"properties": {
"address_line_1": {
"example": "3032 Arjun Views",
"maxLength": 128,
"type": "string"
},
"address_line_2": {
"example": "Apt. 470",
"maxLength": 128,
"type": "string"
},
"city": {
"example": "Reillyton",
"type": "string"
},
"company": {
"description": "Company name, printed above the street address.",
"example": "Sterling Law Group, P.A.",
"type": "string"
},
"country": {
"description": "Required for international mail only.",
"example": "CA",
"type": "string"
},
"first_name": {
"example": "Spencer",
"type": "string"
},
"full_name": {
"description": "Read-only; populated on responses. Ignored on input.",
"example": "Spencer Tyler",
"type": "string"
},
"id": {
"description": "Read-only; populated on responses. Ignored on input.",
"example": 254961,
"type": "integer"
},
"last_name": {
"example": "Tyler",
"type": "string"
},
"phone": {
"description": "API recipient phone on address and sender phone on return_address; required for FedEx, with a configured recipient default allowed. SFTP headers are Recipient Phone Number and Return Phone. FedEx SFTP accepts formatting characters and optional leading 1/+1, then requires exactly 10 normalized U.S. digits; plain 10-digit input is recommended.",
"title": "Phone Number",
"type": "string"
},
"state": {
"description": "Domestic state required: two-letter USPS code. International state is not validated as a U.S. state.",
"example": "PA",
"type": "string"
},
"zip": {
"description": "Domestic ZIP/ZIP+4; international postal data must not be constrained to the U.S. ZIP pattern.",
"example": "96504-0522",
"type": "string"
}
},
"required": [
"address_line_1",
"city"
],
"type": "object"
}
A batch as returned by the API.
{
"description": "A batch as returned by the API.",
"properties": {
"async_fullfill_requested": {
"description": "Fulfilment has been requested. Once `true`, both `PATCH` endpoints reject further changes. Note the spelling.",
"example": false,
"type": "boolean"
},
"created_at": {
"example": "2026-07-10T09:36:07Z",
"format": "date-time",
"type": "string"
},
"firm_mailing_book_url": {
"description": "Present only when the account has firm mailing books enabled.",
"type": "string"
},
"id": {
"description": "The batch id \u2014 the same value returned as `batch_id` from `POST /batches`, and the `{processId}` in every other path.",
"example": "6f6708b0-8fd4-4e3f-80bf-28401a657c9d",
"format": "uuid",
"type": "string"
},
"in_loading": {
"description": "The batch is still being prepared in the background. `PUT .../fulfill-v1` rejects the batch while this is `true`; `PUT .../fulfill` waits instead.",
"example": false,
"type": "boolean"
},
"order_id": {
"description": "Human-facing sequential order number. Not usable as a path parameter.",
"example": 15511,
"type": "integer"
},
"sftp_filename": {
"description": "Source filename, for batches created over SFTP. Empty otherwise.",
"example": "",
"type": "string"
},
"status": {
"description": "Status name. May be an empty string for a batch whose numeric status has no name assigned. Observed values below.",
"enum": [
"",
"draft",
"processing",
"printed",
"mailed",
"in_transit",
"delivered",
"canceled",
"error"
],
"example": "processing",
"type": "string"
},
"title": {
"example": "batch-07-10-2026",
"type": "string"
},
"type": {
"description": "How the batch was created.",
"enum": [
"manual",
"import",
"ocr",
"json_api",
"sftp",
"import_simple"
],
"example": "json_api",
"type": "string"
},
"updated_at": {
"example": "2026-07-10T09:36:08Z",
"format": "date-time",
"type": "string"
}
},
"type": "object"
}
The body accepted by both POST /batches and
PATCH /batches/{processId}/recipients — the two share one handler, so they
take an identical body.
Fields other than recipients are batch-level settings, and most are honoured
only on POST. On PATCH, same_day_mailing, certificate_of_mailing,
restricted_delivery and batch_type are inherited from the existing batch —
sending them has no effect. Set them when you create the batch.
Unknown fields are ignored without error.
{
"description": "The body accepted by **both** `POST /batches` and\n`PATCH /batches/{processId}/recipients` \u2014 the two share one handler, so they\ntake an identical body.\n\nFields other than `recipients` are batch-level settings, and most are honoured\n**only on `POST`**. On `PATCH`, `same_day_mailing`, `certificate_of_mailing`,\n`restricted_delivery` and `batch_type` are inherited from the existing batch \u2014\nsending them has no effect. Set them when you create the batch.\n\nUnknown fields are ignored without error.\n",
"properties": {
"address_verification_type": {
"description": "Set `address_verification_type` explicitly on POST /batches and every PATCH recipient-addition request. Omitting it uses the company default, including on PATCH, rather than preserving the batch's original selection. Explicit NONE overrides that default. Invalid or incorrectly cased enum values return 422 without creating the batch. An explicit API address_verification_type works independently of company-level CASS/NCOA enablement. The company setting is used only when the API request omits the field. SFTP verification is controlled entirely by the company setting; there is no file field. Verification cannot be combined with international mail.\n\nRetrieve available LOOKUP results through GET /batches/{processId}/recipients and its `ncoa_lookup` information.\nModes and charges: NONE original; LOOKUP standardized and report forwarding; BEST forwarding/best available; BOTH standardized plus forwarding copy. Failure falls back to original. Configured verification price defaults to $0.05, including failed lookups and shadows; extra BOTH copy has mailing charge without another verification fee. An explicit API address_verification_type works independently of company-level CASS/NCOA enablement. The company setting is used only when the API request omits the field. SFTP verification is controlled entirely by the company setting; there is no file field. Verification cannot be combined with international mail.",
"enum": [
"NONE",
"CASS_NCOA_LOOKUP",
"CASS_NCOA_BEST",
"CASS_NCOA_BOTH"
],
"example": "NONE",
"type": "string"
},
"batch_type": {
"default": "letter",
"description": "What the batch carries. Optional; defaults to `letter`.\n\n| Value | Contents |\n|---|---|\n| `letter` | Letters only. |\n| `letter_and_check` | Letters and checks submitted together. |\n| `check` | Checks only. |\n\nIgnored on `PATCH`, where it is inherited from the existing batch.\nCheck batches require check mailing to be enabled on the account.\n",
"enum": [
"letter",
"letter_and_check",
"check"
],
"example": "letter",
"type": "string"
},
"certificate_of_mailing": {
"default": false,
"description": "Attach a USPS Certificate of Mailing. Optional; defaults to `false`. Requires the feature to be enabled on the account. **Every recipient in the batch must use `service_type: fc`**, and it cannot be combined with `restricted_delivery`. USPS only. `POST` only \u2014 inherited and ignored on `PATCH`.",
"example": false,
"type": "boolean"
},
"recipients": {
"description": "Start with about 100 recipients per POST or recipient PATCH as an operational recommendation. The API enforces no fixed recipient-count limit on either request or on the batch. Append additional groups to the same draft before fulfillment.",
"items": {
"$ref": "#/components/schemas/Recipient"
},
"type": "array"
},
"restricted_delivery": {
"default": false,
"description": "USPS Restricted Delivery. Optional; defaults to `false`. Requires the feature to be enabled on the account. **Every recipient in the batch must use `service_type` of `cm` or `cm-rre`**, and it cannot be combined with `certificate_of_mailing`. USPS only. `POST` only \u2014 inherited and ignored on `PATCH`.",
"example": false,
"type": "boolean"
},
"same_day_mailing": {
"default": false,
"description": "Enabled accounts only; default cutoff 11 AM Central, account configurable. Evaluated at charging/fulfillment processing or manual approval. After cutoff or non-mailing days, dropped without surcharge and mailed next business mailing day. When applied, $1 per piece including shadow copies. Not international. Set at POST.",
"example": false,
"type": "boolean"
}
},
"required": [
"recipients"
],
"type": "object"
}
Check fields for check/letter_and_check batches. Verified company bank account required. Bank UID, amount and MM-DD-YYYY date required; memo and notes optional. Printing is black and white and single-sided; shadow ignored.
{
"description": "Check fields for check/letter_and_check batches. Verified company bank account required. Bank UID, amount and MM-DD-YYYY date required; memo and notes optional. Printing is black and white and single-sided; shadow ignored.",
"properties": {
"amount": {
"description": "The check amount, as a decimal string.",
"example": "125.00",
"type": "string"
},
"bank_account_uid": {
"description": "UID of a bank account registered on the account. The account must be verified and belong to your company, otherwise the recipient is rejected with `Bank Account UID is invalid`.",
"example": "01H8XGJWBWBAQ4H1SVX0JT2M7Z",
"type": "string"
},
"date": {
"description": "Check date in MM-DD-YYYY; not in the past.",
"example": "12-15-2026",
"type": "string"
},
"memo": {
"description": "Optional check memo.",
"example": "Invoice 4471",
"type": "string"
},
"note_1": {
"description": "Free-text note printed on the check stub. Optional.",
"example": "Thank you for your business",
"type": "string"
},
"note_2": {
"description": "Second free-text note printed on the check stub. Optional.",
"example": "",
"type": "string"
}
},
"type": "object"
}
Standard error envelope. Every non-2xx response uses this shape. Two
operations add a field to it: createBatch (400) adds invalid_letters,
and fulfillBatch (422) adds missed_pdf_files.
{
"description": "Standard error envelope. Every non-2xx response uses this shape. Two\noperations add a field to it: `createBatch` (400) adds `invalid_letters`,\nand `fulfillBatch` (422) adds `missed_pdf_files`.\n",
"properties": {
"is_successful": {
"enum": [
false
],
"example": false,
"type": "boolean"
},
"message": {
"description": "Human-readable explanation. Not a stable machine-readable code.",
"example": "Batch was not found",
"type": "string"
}
},
"required": [
"is_successful",
"message"
],
"type": "object"
}
A rendered letter, as produced by the print pipeline.
Only present once the batch has been printed — until then letters is null.
Fields marked optional are omitted while empty, so an unprinted or in-flight
letter carries far fewer keys than a delivered one.
{
"description": "A rendered letter, as produced by the print pipeline.\n\nOnly present once the batch has been printed \u2014 until then `letters` is `null`.\nFields marked optional are omitted while empty, so an unprinted or in-flight\nletter carries far fewer keys than a delivered one.\n",
"properties": {
"affidavit": {
"description": "The affidavit slug applied to this letter, if any.",
"enum": [
"sb",
"sl",
"nb",
"nl"
],
"type": "string"
},
"check": {
"allOf": [
{
"$ref": "#/components/schemas/CheckOptions"
}
],
"description": "Check details. Always present \u2014 an empty object on a standard letter mailing."
},
"code": {
"description": "USPS tracking number. Present once the letter has been posted.",
"example": "9214890321486300010108",
"type": "string"
},
"created_at": {
"example": "2026-07-10T09:36:07Z",
"format": "date-time",
"type": "string"
},
"id": {
"example": 217947,
"type": "integer"
},
"is_colored": {
"example": true,
"type": "boolean"
},
"is_double_sided": {
"example": false,
"type": "boolean"
},
"is_rre": {
"description": "This letter carries Return Receipt Electronic.",
"example": true,
"type": "boolean"
},
"letter_image": {
"description": "Time-limited signed URL to the rendered letter PDF. Fetch it promptly; do not store it.",
"example": "https://example-bucket.s3.amazonaws.com/letter.pdf?X-Amz-Signature=REDACTED",
"format": "uri",
"type": "string"
},
"price": {
"description": "Amount charged for this letter, as a decimal string.",
"example": "8.64",
"type": "string"
},
"recipient": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"description": "The resolved delivery address."
},
"recipient_id": {
"example": 254961,
"type": "integer"
},
"return_envelope": {
"description": "The return envelope applied to this letter, if any. Always `re-`-prefixed.",
"example": "re-9npna",
"type": "string"
},
"return_mail_image": {
"description": "Signed URLs to scanned return-mail images, if the letter came back.",
"items": {
"format": "uri",
"type": "string"
},
"type": "array"
},
"rre_enabled": {
"example": false,
"type": "boolean"
},
"rre_image": {
"description": "Time-limited signed URL to the Return Receipt Electronic PDF.",
"format": "uri",
"type": "string"
},
"sender": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"description": "The resolved return address. Always present \u2014 an empty object until the letter has been produced."
},
"serial_number": {
"example": 1010,
"type": "integer"
},
"status": {
"description": "Letter status.\n\n| Value | Status |\n|---|---|\n| `0` | draft |\n| `1` | processing |\n| `2` | printed |\n| `3` | mailed |\n| `4` | in_transit |\n| `5` | delivered |\n\nThe developer-confirmed status list includes 11 (pre-processing). Production status is not carrier-confirmed delivery.\nThe developer-confirmed status list includes 11 (pre-processing). Production status is not carrier-confirmed delivery.",
"enum": [
0,
1,
2,
3,
4,
5,
11
],
"example": 1,
"type": "integer"
},
"tracking_info": {
"additionalProperties": true,
"description": "The carrier's **raw** tracking payload, passed through unmodified. Its\nshape is set by USPS, not by this API, and it varies by mail class \u2014 treat\nit as opaque.\n\nWhen no raw payload has been stored for a letter, a normalized form is\nreturned instead:\n\n```json\n{\n \"error\": null,\n \"done\": true,\n \"logs\": [\n { \"date\": \"Dec 27, 2024\", \"description\": \"Delivered\", \"location\": \"Reillyton, PA\" },\n { \"date\": \"Dec 23, 2024\", \"description\": \"Accepted\", \"location\": \"Frisco, TX\" }\n ]\n}\n```\n\n`error` carries a human-readable explanation when USPS has nothing to\nreport yet. Code defensively for both shapes.\n\nOmitted entirely until the letter has been posted and a first scan\narrives. The earliest log entry is the acceptance scan \u2014 this is where the\nmailing date is read from, as letters carry no separate mailing-date\nfield.\n",
"type": "object"
},
"type": {
"description": "Resolved service-and-envelope slug \u2014 the combination of the recipient's `service_type` and `envelope_type`, not either one alone.",
"example": "cm-9x12-rre",
"type": "string"
},
"updated_at": {
"example": "2026-07-10T09:36:40Z",
"format": "date-time",
"type": "string"
}
},
"type": "object"
}
Reports how uploaded PDFs line up with the batch's recipients. Returned by PATCH .../letters, and by PUT .../fulfill when the batch is not ready.
{
"description": "Reports how uploaded PDFs line up with the batch's recipients. Returned by `PATCH .../letters`, and by `PUT .../fulfill` when the batch is not ready.",
"properties": {
"extra_pdf_files": {
"description": "Uploaded filenames that no recipient references.",
"example": [
"nobody_wants_me.pdf"
],
"items": {
"type": "string"
},
"type": "array"
},
"is_successful": {
"type": "boolean"
},
"missed_pdf_files": {
"description": "Filenames recipients reference via `pdf_name` but which were never uploaded.",
"example": [],
"items": {
"type": "string"
},
"type": "array"
},
"not_mapped_recipients": {
"description": "Recipients still without a matching PDF, each with an `error_message`.",
"example": [],
"items": {
"$ref": "#/components/schemas/Recipient"
},
"type": "array"
}
},
"type": "object"
}
How this letter is printed and mailed.
{
"description": "How this letter is printed and mailed.",
"properties": {
"affidavit": {
"description": "Affidavit of Mailing (AOM). Omit for none.\n\n| Value | Meaning | Billing |\n|---|---|---|\n| `sb` | Signature Only, **Batch Level** \u2014 one signed affidavit covers the entire batch | flat, once per batch |\n| `sl` | Signature Only, **Letter Level** \u2014 a separate signed affidavit per recipient | **per letter** |\n| `nb` | Notarized, **Batch Level** \u2014 one notarized affidavit covers the entire batch | flat, once per batch |\n| `nl` | Notarized, **Letter Level** \u2014 a separate notarized affidavit per recipient | **per letter** |\n\nThe first letter is the kind of affidavit \u2014 `s` signed, `n` notarized. The\nsecond letter is the scope: **`b` is batch level**, **`l` is letter level**.\n\nNotarized affidavits are reviewed manually and are not published\nautomatically.\n\n**Every recipient in a batch must carry the same value.** If one recipient\nrequests an affidavit, they all must request the same one; a batch with\nmixed values is rejected with 422.\n\nOnly available if the account has affidavit mailing enabled; otherwise\nthe value is ignored. An unrecognized value returns 422.\n",
"enum": [
"sb",
"sl",
"nb",
"nl"
],
"example": "sb",
"type": "string"
},
"envelope_type": {
"description": "Domestic fc/cm/cm-rre: omitted selects by page count; explicit 10, 6x9 or 9x12 accepted. Physical-sheet capacities including cover: 5, 16 and 70. International intl-fc/rim: explicit 10 or 9x12 required. FedEx: explicit overnight or 2day required. pm/pm-box: ignored; leave blank. ps is checks-only First-Class packaging. Inserts reduce document allowance.",
"enum": [
"10",
"6x9",
"9x12",
"ps",
"overnight",
"2day"
],
"example": "10",
"type": "string"
},
"is_colored": {
"default": false,
"description": "Print in color. Defaults to `false`. Forced to `false` for check batches.",
"example": true,
"type": "boolean"
},
"is_double_sided": {
"default": false,
"description": "Print on both sides. Defaults to `false`. Forced to `false` for check batches.",
"example": false,
"type": "boolean"
},
"return_envelope": {
"description": "Include a return envelope. Optional; omit it, or send `re-empty`, for\nnone. The selection may vary from one recipient to the next.\n\n| Value | Envelope |\n|---|---|\n| `re-9npna` | #9, no postage, no pre-addressed return destination |\n| `re-9pna` | #9, prepaid postage, no pre-addressed return destination |\n| `re-9pa` | #9, prepaid postage and a pre-addressed return destination |\n| `re-9pam` | #9 prepaid, pre-addressed, metered |\n| `re-empty` | No return envelope. Same as omitting the field. |\n\nThe `re-` prefix is optional on input \u2014 `9npna` is accepted and stored as\n`re-9npna` \u2014 but responses always return the prefixed form, so prefer it.\nAny other value returns a validation error.\n\nConditions:\n\n- Cannot be used with international mail (`intl-fc`, `rim`) \u2014 doing so\n returns a validation error.\n- `re-9pam` is accepted only with a `9x12` envelope or a Priority Mail\n service (`pm`, `pm-box`).\n- Silently cleared if return envelopes are not enabled on the account.\n",
"enum": [
"re-empty",
"re-9npna",
"re-9pna",
"re-9pa",
"re-9pam"
],
"example": "re-9npna",
"type": "string"
},
"service_type": {
"description": "Supported service codes: fc, cm, cm-rre, pm, pm-box, intl-fc, rim and case-sensitive FedEx. For API-key requests on FedEx-enabled accounts, FedEx requires envelope_type overnight or 2day. For API-key requests on FedEx-enabled accounts, FedEx requires envelope_type overnight or 2day. Separate international/domestic and USPS/FedEx batches. International requires country and excludes checks, return envelopes, same-day and verification.",
"enum": [
"fc",
"cm",
"cm-rre",
"pm",
"pm-box",
"intl-fc",
"rim",
"FedEx"
],
"example": "fc",
"type": "string"
}
},
"required": [
"service_type"
],
"type": "object"
}
Present only when the request asked for a paged response.
{
"description": "Present only when the request asked for a paged response.",
"properties": {
"current_page": {
"example": 1,
"type": "integer"
},
"per_page": {
"example": 25,
"type": "integer"
},
"total": {
"description": "Total matching records across all pages.",
"example": 137,
"type": "integer"
},
"total_pages": {
"example": 6,
"type": "integer"
}
},
"required": [
"current_page",
"per_page",
"total",
"total_pages"
],
"type": "object"
}
Binds this recipient's letter to a PDF you upload via PATCH /batches/{processId}/letters.
{
"description": "Binds this recipient's letter to a PDF you upload via `PATCH /batches/{processId}/letters`.",
"properties": {
"pdf_name": {
"description": "The exact filename of the PDF uploaded through `PATCH /batches/{processId}/letters`, matched character for character. Required whenever the batch carries letters. **Filenames must be unique within a batch** \u2014 this is the only key linking a recipient to its letter image.",
"example": "Sample Letter.pdf",
"type": "string"
},
"pdf_page_numbers": {
"description": "The pages within `pdf_name` assigned to this recipient. Either a single\npage (`\"3\"`) or an inclusive range (`\"1-3\"`). The start must be less than\nthe end. Spaces are ignored. `\"0\"` is rejected.\n\n**Optional \u2014 omit it when this recipient has a PDF to itself**, and the\nwhole uploaded file becomes that recipient's letter. Supply it when one\nuploaded PDF carries letters for several recipients, each taking its own\npage range.\n",
"example": "1-3",
"pattern": "^\\s*\\d+\\s*(-\\s*\\d+\\s*)?$",
"type": "string"
},
"pdf_process_type": {
"description": "Internal. Leave it unset \u2014 page selection is driven entirely by `pdf_page_numbers`. It is echoed back on responses, usually as an empty string.",
"example": "",
"type": "string"
}
},
"type": "object"
}
One recipient — which is to say, one letter. A recipient carries its own mailing options and its own binding to a PDF, so adding a recipient adds a letter to be mailed.
{
"description": "One recipient \u2014 which is to say, **one letter**. A recipient carries its own\nmailing options and its own binding to a PDF, so adding a recipient adds a\nletter to be mailed.\n",
"properties": {
"address": {
"allOf": [
{
"$ref": "#/components/schemas/RecipientAddress"
}
],
"description": "The recipient's mailing address. Required."
},
"check_options": {
"allOf": [
{
"$ref": "#/components/schemas/CheckOptions"
}
],
"description": "Check details. Present only on `check` and `letter_and_check` batches."
},
"error_message": {
"description": "Read-only; populated on responses when this recipient failed validation.",
"example": "",
"type": "string"
},
"letter_id": {
"description": "A customer-provided identifier for the individual letter. Optional; echoed back on responses.",
"example": "letter_1",
"type": "string"
},
"mailing_options": {
"allOf": [
{
"$ref": "#/components/schemas/MailingOptions"
}
],
"description": "How this letter is printed and mailed. Required."
},
"pdf_file_config": {
"allOf": [
{
"$ref": "#/components/schemas/PdfFileConfig"
}
],
"description": "Binds this recipient's letter to an uploaded PDF. Required whenever the batch carries letters."
},
"return_address": {
"allOf": [
{
"$ref": "#/components/schemas/Address"
}
],
"description": "The return address printed on the mailing. Required. If you leave the phone off, the phone on your account's default return address is used."
},
"shadow": {
"default": false,
"description": "Send a duplicate First-Class copy of this letter.\n\nWhen enabled for an eligible Certified Mail (`cm`) or Certified Mail with\nRRE (`cm-rre`) piece, UploadLetters automatically sends an identical\nadditional copy by First-Class Mail. You submit the recipient one time and\nUploadLetters automatically generates the additional First-Class piece,\nwhich is printed and billed like any other letter.\n\nOptional; defaults to `false`. Conditions:\n\n- Has no effect unless `mailing_options.service_type` is `cm` or `cm-rre`.\n- Ignored on check batches.\n",
"example": false,
"type": "boolean"
},
"unique_id": {
"description": "A customer-provided unique identifier used to match the mailing record to your own system. Optional; echoed back on responses so you can reconcile. Send it as a string.",
"example": "1",
"type": "string"
}
},
"required": [
"address",
"return_address",
"mailing_options"
],
"type": "object"
}
The delivery address. Same as Address, but zip is also required and format-checked.
{
"allOf": [
{
"$ref": "#/components/schemas/Address"
},
{
"properties": {
"phone": {
"title": "Recipient Phone Number"
}
},
"required": [
"zip"
],
"type": "object"
}
],
"description": "The delivery address. Same as `Address`, but `zip` is also required and format-checked."
}
A single wallet movement.
{
"description": "A single wallet movement.",
"properties": {
"amount": {
"description": "Signed integer, in cents. Negative for a `withdraw`. Note this differs from `balance`, which is a decimal number of dollars.",
"example": -864,
"type": "integer"
},
"confirmed": {
"example": true,
"type": "boolean"
},
"created_at": {
"example": "2026-07-10T04:32:57Z",
"format": "date-time",
"type": "string"
},
"id": {
"example": 7753,
"type": "integer"
},
"meta": {
"additionalProperties": true,
"description": "Free-form context. For a batch purchase, the batch title and a description.",
"example": {
"description": "Purchase of Batch",
"title": "batch-07-10-2026"
},
"type": "object"
},
"type": {
"description": "`withdraw` debits the wallet, `deposit` credits it.",
"enum": [
"withdraw",
"deposit"
],
"example": "withdraw",
"type": "string"
},
"updated_at": {
"example": "2026-07-10T04:32:57Z",
"format": "date-time",
"type": "string"
},
"user_id": {
"example": "74a54d21-e451-4f09-9090-4462ce7edf02",
"format": "uuid",
"type": "string"
},
"uuid": {
"example": "8d631ca1-024e-47db-af13-f55c7def1e2b",
"format": "uuid",
"type": "string"
},
"wallet_id": {
"example": 247,
"type": "integer"
}
},
"type": "object"
}
Per-recipient validation failures, keyed by the recipient's index in the
recipients array you submitted (as a string). The inner object is keyed by
dotted field path, each mapping to a list of messages.
{
"additionalProperties": {
"additionalProperties": {
"items": {
"type": "string"
},
"type": "array"
},
"type": "object"
},
"description": "Per-recipient validation failures, keyed by the recipient's **index in the\n`recipients` array you submitted** (as a string). The inner object is keyed by\ndotted field path, each mapping to a list of messages.\n",
"example": {
"0": {
"address.city": [
"The city field is required."
],
"address.zip": [
"The zip code field has an invalid value."
],
"mailing_options.service_type": [
"The service type field is required."
]
}
},
"type": "object"
}