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

6.7 KiB
Raw Blame History

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:
$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

  • -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
  • jobunique 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).