Files
dre/docs/letterstream-api-contract.md
T
root 7a62b0b340 Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate
- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake)
- packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog)
- letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified)
- docuseal.py: DocuSeal signing integration
- staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table
- frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard)
- landing-mockups/: 4 design-stance mockups + favicons
- legal/: aup/privacy/sms-terms/terms HTML
- docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates
- review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint
- compliance/DRE_Compliance_Manual.md: updated

Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
2026-08-26 02:26:33 -04:00

139 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<messages id="..."><message type="...">...</message></messages>`.
- `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, **1018 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 | 510 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=<authcode>`.
## Submission response codes
- `-100` success → includes `<batch>`, `<quantity>`, `<cost>`, `<doc><id><job><cost>`
- `-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=<tracking_number>&getinfo=track` → HTML tracking (or `getinfo=trackx` XML; `responseformat=json` for JSON)
- `doc_id=<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=<batch1,batch2>` / `jobstatus=<job1,job2>` / `docstatus=<doc1,doc2>` → stage-of-production status
- `accountstatus=1` → account balance (`<balance>`, `<testmode>`)
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).