# 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).