- 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).
6.7 KiB
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/.envasLETTERSTREAM_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=jsonreturns JSON instead of XML.
Auth (VERIFIED 2026-08-25)
Three form fields on every request:
a= api_idt= unique id — numeric, 10–18 digits, accepted only once (duplicate →-957 DUP). Usetime()-style value.h= hash, computed as:
$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:
import hashlib, base64
s = t[-6:] + api_key + t[:6]
h = hashlib.md5(base64.b64encode(s.encode())).hexdigest()
Auth response codes
-199AUTHOK— account good, connection successful-958IDOK— api_id found but hash lookup failed (wrong hash)-957DUP— unique id duplicate-950Unable to authenticateBAD— api_id not valid-998Improper submission format— auth valid but args don't form a valid request-999unknown 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— authjob— 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
-100success → includes<batch>,<quantity>,<cost>,<doc><id><job><cost>-200preauth success / preauth authorization success-911insufficient 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" endorsementcertified— Certified w/ Electronic Return Receipt (#10 3-window, tracking #)certnoerr— Certified WITHOUT e-Return Receipt (no signature collected)postcard— 5.5"x4.25" 100# cardstockflat— 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 (orgetinfo=trackxXML;responseformat=jsonfor 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 statusaccountstatus=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_KEYin.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(mirrorsdocuseal.pystyle). - Auth formula verified live 2026-08-25 (AUTHOK + balance returned).