# LetterStream API Contract (verified live) Date: 2026-08-25 Source: `api_fulfillment.pdf` (LetterStream "Mail Fulfillment by LetterStream — Integration API", Feb 3 2023) + live verification against the account. ## Credentials - `API_ID` (8 chars), `API_KEY` (18 chars) — in `/opt/dre-portal/.env` as `LETTERSTREAM_API_ID` / `LETTERSTREAM_API_KEY`. - Account funded: balance `$100.00`, `testmode=disabled` (LIVE/production mode) as of 2026-08-25. ## Endpoint - Base: `https://www.letterstream.com/apis/` (or `/apis/index.php`). **POST only** (form-encoded or multipart). - Response: XML `...`. - `responseformat=json` returns JSON instead of XML. ## Auth (VERIFIED 2026-08-25) Three form fields on every request: - `a` = api_id - `t` = unique id — numeric, **10–18 digits**, accepted only once (duplicate → `-957 DUP`). Use `time()`-style value. - `h` = hash, computed as: ```php $unique_id = time(); // 10-18 digit numeric, unique per request $string_to_hash = substr($unique_id,-6) . $api_key . substr($unique_id,0,6); $hash = md5(base64_encode($string_to_hash)); ``` Python equivalent: ```python import hashlib, base64 s = t[-6:] + api_key + t[:6] h = hashlib.md5(base64.b64encode(s.encode())).hexdigest() ``` ### Auth response codes - `-199` `AUTHOK` — account good, connection successful - `-958` `IDOK` — api_id found but hash lookup failed (wrong hash) - `-957` `DUP` — unique id duplicate - `-950` `Unable to authenticate` - `BAD` — api_id not valid - `-998` `Improper submission format` — auth valid but args don't form a valid request - `-999` `unknown submission error` ## Send method 1 — Batch (ZIP) [preferred for volume] `POST` with `multi_file` = a `.zip` archive containing one PDF per recipient + one CSV data file. CSV filename becomes the batch id (must be unique). 50MB cap. CSV columns (Table 4.1.1): | # | Column | Required | Notes | |---|---|---|---| | 1 | UniqueDocId | yes | alphanumeric, max 20 chars, unique to any active/mailed job | | 2 | PDFFileName | yes | filename of the PDF inside the zip | | 3 | RecipientName1 | yes | | | 4 | RecipientName2 | optional | | | 5 | RecipientAddr1 | yes | | | 6 | RecipientAddr2 | optional | suite # | | 7 | RecipientCity | yes | | | 8 | RecipientState | yes | 2-char alpha | | 9 | RecipientZip | yes | 5–10 numeric + "-" | | 10 | SenderName1 | yes | | | 11 | SenderName2 | optional | | | 12 | SenderAddr1 | yes | | | 13 | SenderAddr2 | optional | | | 14 | SenderCity | yes | | | 15 | SenderState | yes | 2-char alpha | | 16 | SenderZip | yes | | | 17 | PageCount | yes | numeric | | 18 | MailType | no | `firstclass` \| `firstclass_hse` \| `certified` \| `certnoerr` \| `postcard` \| `flat` \| `propostcard` (default `firstclass`) | | 19 | CoverSheet | no | `Y`\|`N` (default `Y`) | | 20 | Duplex | no | `Y`\|`N` (default `N`) | | 21 | Ink | no | `B`\|`C` (default `B`) | | 22 | Paper | no | see options (default `W`) | | 23 | ReturnEnvelope | no | `Y`\|`9RWS`\|`9LWS`\|`634`\|`634_12PK`\|`N` (default `N`) | | 24 | Affidavit | no | `A`\|`N` (default `N`) | ## Send method 2 — HTTP POST (single file) [≤50/day, low volume] `POST` (multipart or form-encoded). Required fields: - `a`, `h`, `t` — auth - `job` — **unique** job name (unique across all active/mailed jobs) - `to[]` — array of recipient address strings (repeat the field per recipient) - `from` — single sender/return address (max 1) - `single_file` — the PDF to mail (multipart file OR base64 blob) - `pages` — number of pages in the PDF Optional: `mailtype` (default `firstclass`), `coversheet` (default true), `duplex`, `ink`, `paper`, `returnenv`, `preauth`. ### Address string format (`to[]` and `from`) Colon or pipe delimited (don't mix): ``` # recipient (doc_id included): doc_id:name_1:name_2:address_1:address_2:city:state:zip # sender (no doc_id): name_1:name_2:address_1:address_2:city:state:zip ``` `doc_id` must be unique per recipient (same spec as UniqueDocId). Only domestic addresses eligible for certified mail. ## Preauth (price-before-release) - Submit with `preauth=1` → processed but NOT released to production; returns `-200` + `authcode` + pricing. - Authorize/release by resubmitting `doauth=`. ## Submission response codes - `-100` success → includes ``, ``, ``, `` - `-200` preauth success / preauth authorization success - `-911` insufficient funding (items held until funds added) ## Mail types (cost/features) - `firstclass` — First Class Letter (#10 2-window) - `firstclass_hse` — First Class Letter "Homeowner Statement Enclosed" endorsement - `certified` — Certified w/ Electronic Return Receipt (#10 3-window, tracking #) - `certnoerr` — Certified WITHOUT e-Return Receipt (no signature collected) - `postcard` — 5.5"x4.25" 100# cardstock - `flat` — 10x13 windowed flat (up to 75 sheets, coversheet by default) - `propostcard` — pro postcard ## Tracking / status queries (POST, all with a/h/t) - `cert=&getinfo=track` → HTML tracking (or `getinfo=trackx` XML; `responseformat=json` for JSON) - `doc_id=&getinfo=track` → job status (non-certified) - `cert=...&getinfo=sig` → signature file (streamed PDF) - `doc_id=...&getinfo=proof` → document proof (base64 streamed PDF) - `batchstatus=` / `jobstatus=` / `docstatus=` → stage-of-production status - `accountstatus=1` → account balance (``, ``) USPS tracking numbers: 22 digits since March 2018 (older 20-digit still valid). ## Document preflight `POST` with `preflight=visual` (or `auto` coming soon) + `preflight_file` (PDF) + optional `display=true`. Returns marked-up PDF showing window placement. Used for template verification, not every submission. ## Callback / webhook (tracking push) — receive side See "API PUSH" section below (contract from account "API Callback Settings" page): - LetterStream PUSHES tracking data to our endpoint (HTTP POST) every 4 hours (and heartbeat when idle). - POST fields: `key`, `api_version`, `timestamp`, `json`. - `json` = JSON string of tracking line items: `batch_id`, `job_id`, `doc_id`, `tracking_id`, `scan_date`, `scan_zip`, `scan_facility`, `scan_code`, `scan_status`. - scan_codes reference: https://postalpro.usps.com/product-tracking-and-reporting/scan-events-descriptions - Required response: HTTP 200 + `{"success":true,"reason":"Received data"}`. - `key` = our callback auth string (`LETTERSTREAM_CALLBACK_KEY` in `.env`, 48 hex chars, generated 2026-08-25). - Enable must stay OFF until our receiver is live. ## Implementation - Python module: `/opt/dre-portal/app/letterstream.py` (mirrors `docuseal.py` style). - Auth formula verified live 2026-08-25 (AUTHOK + balance returned).