Verification your platform can call.
Programmatic IRS transcript verification: submit an entity, obtain Form 8821 authorization, and receive the pulled transcripts plus a structured compliance read — by polling or by webhook.
Overview
A typical automated integration — how a reseller such as a lending platform runs it end to end.
Submit
Send one or more entities to POST /api/intake/transcript.
Authorize
Each entity needs a signed IRS Form 8821 — upload a pre-signed PDF you collected yourself, or let ModernTax collect the signature.
Receive results
Poll GET /api/intake/transcript?token= with an optional schema= shape, or have us POST to your webhook.
Every endpoint in this reference authenticates with an x-api-key header. The portal also exposes cookie-authenticated admin, expert, and processor endpoints; those are internal and not part of this contract.
Authentication
All requests require an x-api-key header. Live keys have the form mt_live_txn_<slug>_<12hex>.
Manage keys — rotation, webhook config, default response schema, usage — self-serve in the developer portal at portal.moderntax.io/developer. To try the API first, grab a free sandbox key; for a live key, contact matt@moderntax.io.
x-api-key: mt_live_txn_yourco_xxxxxxxxxxxx
Content-Type: application/json # or multipart/form-data where noted
Keys are stored as SHA-256 hashes and constant-time verified on every request. A missing or invalid key returns 401 via the same code path, so errors never reveal whether a partial match exists.
Base URL
https://portal.moderntax.io
Sandbox
Self-serve at portal.moderntax.io/sandbox — enter a work email and company name and you get an instant {company}-sandbox API key against synthetic data.
Endpoints and response shapes are production-identical; the key is capped at 250 requests. No sales call required.
Submit a transcript request
Creates a request with one or more entities and queues them for IRS retrieval.
Each entity needs a signed 8821 before results can be delivered: attach one inline via entities[].signed_8821, upload one to /api/intake/8821-pdf, or let ModernTax collect the signature via /api/intake/8821.
Request
curl -X POST https://portal.moderntax.io/api/intake/transcript \
-H "x-api-key: mt_live_txn_yourco_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d @request.json
{
"request_token": "your-loan-12345", // unique per account; idempotency key
"loan_number": "your-loan-12345", // optional; used in invoice itemization
"entities": [
{
"entity_name": "ACME Holdings LLC",
"tid": "12-3456789",
"tid_kind": "EIN", // "EIN" or "SSN"
"form_type": "1120S", // optional; inferred from tid_kind if omitted
"years": ["2022","2023","2024"],
"address": "123 Main St", "city": "Austin", "state": "TX", "zip_code": "78701",
"loan_officer_name": "Jane Rep", // optional
"loan_officer_email": "jane@lender.com",
"fiscal_year_end_month": 12, // optional
"signed_8821": { // optional: attach a pre-signed 8821 inline
"pdf_base64": "<base64 PDF, max 10 MB decoded>",
"persona_inquiry_id": "inq_abc123", // optional signature evidence —
"persona_verification_token": "…", // all evidence fields optional
"signer_name": "Jane Founder",
"signer_email": "jane@acme.com",
"signed_at": "2026-08-12T18:04:00Z", // ISO 8601
"signer_ip": "203.0.113.9",
"signature_vendor": "persona"
}
}
]
}
| Field | Req | Notes |
|---|---|---|
| request_token | yes | Your unique id for the request. Reusing one returns 409. |
| loan_number | no | Falls back to request_token. Used on invoices. |
| entities[].entity_name | yes | Legal name on file with the IRS. |
| entities[].tid / tid_kind | yes | EIN (XX-XXXXXXX) or SSN (XXX-XX-XXXX); "EIN"/"SSN". |
| entities[].years | yes | 4-digit years, 1990–2028. |
| entities[].form_type | no | 1040, 1065, 1120, 1120S, 941. Inferred from tid_kind if omitted. |
| entities[].signed_8821 | no | Inline pre-signed 8821: pdf_base64 (≤10 MB decoded) plus optional evidence fields (persona_inquiry_id, persona_verification_token, signer_name, signer_email, signed_at, signer_ip, signature_vendor). |
Response 201
{
"success": true,
"request_id": "uuid",
"request_token": "your-loan-12345",
"loan_number": "your-loan-12345",
"status": "irs_queue",
"entities": [
{ "entity_id": "uuid", "entity_name": "ACME Holdings LLC",
"form_type": "1120S", "years": ["2022","2023","2024"], "status": "irs_queue",
"signed_8821": "attached" } // only when entities[].signed_8821 was sent:
// attached | unreadable_attached | rejected_designee_mismatch
],
"usage": { "used": 47, "remaining": 953, "limit": 1000 }
}
An inline 8821 whose designee CAF isn't a ModernTax designee never fails the request — the entity is created without the form (signed_8821: "rejected_designee_mismatch") and you can re-upload via /api/intake/8821-pdf.
Errors: 401 (auth), 402 (account not enrolled in billing — see Pricing), 409 (duplicate request_token).
Authorization — IRS Form 8821
The IRS requires a signed Form 8821 authorizing ModernTax as designee before transcripts can be pulled. There are two integration models.
A. Upload a pre-signed 8821 — you collect the signature
For partners who obtain the taxpayer's signature through their own workflow (DocuSign, wet-sign, Persona-verified e-sign). The entity advances straight to 8821_signed. This is the model a fully-automated reseller uses. Optional evidence fields record how the signature was collected; they're stored with the entity and returned as signature_evidence on poll responses.
curl -X POST https://portal.moderntax.io/api/intake/8821-pdf \
-H "x-api-key: mt_live_txn_yourco_xxxxxxxxxxxx" \
-F "file=@signed-8821.pdf" \
-F "request_token=your-loan-12345" \
-F "entity_id=uuid" \
-F "persona_inquiry_id=inq_abc123" \
-F "signer_name=Jane Founder" \
-F "signed_at=2026-08-12T18:04:00Z"
file: <signed PDF, application/pdf, max 10 MB> (required)
request_token: your-loan-12345 (required)
entity_id: uuid # or entity_name if the request has one entity
years: "2022,2023,2024" # optional override
form_type: "1120S" # optional override
# optional signature evidence
persona_inquiry_id: inq_abc123
persona_verification_token: <token>
signer_name: Jane Founder
signer_email: jane@acme.com
signed_at: 2026-08-12T18:04:00Z # ISO 8601
signer_ip: 203.0.113.9
signature_vendor: persona
{ "success": true,
"designee_check": "ok", // "ok" | "unreadable" (PDF accepted either way)
"signature_evidence_stored": true, // true when any evidence field was sent
"entity": { "id":"uuid","name":"ACME Holdings LLC","status":"8821_signed",
"signed_8821_url":"https://…1h-expiry…","years":["2022","2023","2024"],"form_type":"1120S" } }
If the uploaded 8821 names a designee CAF that is not a ModernTax designee, the endpoint returns 422 { "code": "designee_caf_mismatch", "designee_cafs": ["…"] } and the PDF is not attached — fix the form and re-upload.
B. Let ModernTax collect the signature
Submit entities with signer details (CSV/XLSX file or JSON { entities: [...] }) and ModernTax generates a pre-filled 8821, e-mails the signer for signature, then proceeds automatically.
CSV columns: entity_name, tid, tid_kind, email, signer_first_name, signer_last_name, years, address, city, state, zip, form_type.
{ "request_id":"uuid","entities_created":12,"signature_requests_sent":12,"status":"8821_sent" }
Poll for results
Returns every entity's current status, 1-hour signed transcript URLs, and a compliance read. Without a schema query parameter (and no portal-pinned default) the legacy shape below is returned unchanged.
{
"request_id": "your-loan-12345",
"status": "completed", // collapses to "completed" | "pending"
"request_status": "completed", // raw: submitted | 8821_sent | 8821_signed | irs_queue | processing | completed | failed
"completed_at": "2026-05-09T10:32:11Z",
"entities": [{
"entity_id":"uuid","entity_name":"ACME Holdings LLC","tid":"12-3456789","form_type":"1120S",
"years":["2022","2023","2024"],"status":"completed",
"signed_8821_url":"https://…1h-expiry…","signature_created_at":"2026-05-08T…",
"signature_evidence": { // present when evidence was supplied at upload
"persona_inquiry_id":"inq_abc123","signer_name":"Jane Founder","signer_email":"jane@acme.com",
"signed_at":"2026-08-12T18:04:00Z","signature_vendor":"persona",
"recorded_at":"2026-08-12T18:05:02Z","source":"api_8821_pdf"
}, // signer_ip is stored but never returned
"transcript_urls":["https://…/2022-record-of-account.pdf", "…"],
"transcript_html_urls":["https://…"],
"compliance": {
"severity":"WARNING", // CRITICAL | WARNING | CLEAN (max across forms)
"flags":[{"type":"BALANCE_DUE","severity":"WARNING","message":"Account balance: $4,287.21"}],
"financials":{"grossReceipts":1240000,"totalTax":42000,"accountBalance":4287.21,"accruedInterest":0,"accruedPenalty":0},
"recent_transactions":[{"code":"150","explanation":"Tax return filed","date":"2024-04-12","amount":"42000"}] // ≤10, newest first
}
}]
}
Response schemas
schema is a query parameter on the results poll — it is not a field on the create-request body. You can also pin an org-wide default ("default response schema") in the developer portal; an explicit query parameter overrides the pin. No parameter and no pin returns the legacy shape unchanged. An invalid value returns 400 listing the valid values.
Just tell me if it's clean — filed years, unfiled years, flags, and file URLs.
The lines a credit memo needs — financials, liabilities, repayment posture, headline.
You parse it yourself — artifact list with per-file sha256.
curl "https://portal.moderntax.io/api/intake/transcript?token=your-loan-12345&schema=underwriting" \
-H "x-api-key: mt_live_txn_yourco_xxxxxxxxxxxx"
All three shapes share one envelope; only the per-entity objects differ. PDF and HTML URLs are 1-hour signed.
{
"request_id": "uuid",
"reference": "your-loan-12345",
"schema": "minimal", // echoes the shape served
"status": "completed",
"request_status": "completed",
"created_at": "2026-08-10T14:02:00Z",
"completed_at": "2026-08-11T10:32:11Z",
"entities": [ … ] // shape per schema, below
}
schema=minimal — just tell me if it's clean
{ "id":"uuid", "name":"ACME Holdings LLC", "form":"1120S",
"years":["2022","2023","2024"], "status":"completed",
"years_filed":["2022","2023","2024"], "years_unfiled":[],
"flags":[{"type":"BALANCE_DUE","severity":"WARNING","message":"Account balance: $4,287.21"}],
"pdf":["https://…1h-expiry…"], "html":["https://…1h-expiry…"] }
schema=underwriting — the lines a credit memo needs
Everything in minimal, plus:
{ …minimal fields…,
"financials":[{ "year":"2023","form":"1120S","gross_receipts":1240000,"total_income":1262400,
"total_deductions":1108000,"total_tax":42000,"account_balance":4287.21,
"accrued_interest":112.04,"accrued_penalty":0,"account_balance_plus_accruals":4399.25 }],
"tax_liabilities":{ "total_assessed":42000,"total_paid":37712.79,"total_balance":4287.21,"total_accrued":112.04 },
"repayment_plan":{ "has_installment_agreement":false,"has_offer_in_compromise":false,
"has_currently_not_collectible":false,"details":null,
"recommendation":"Balance due with no active agreement — condition the loan on proof of payment or an installment agreement." },
"overall_severity":"WARNING",
"headline":"Filed all 3 requested years; $4,287.21 balance due on 2023." }
schema=raw — you parse it yourself
{ "id":"uuid", "name":"ACME Holdings LLC", "form":"1120S",
"years":["2022","2023","2024"], "status":"completed",
"retrieved_at":"2026-08-11T10:32:11Z",
"artifacts":[
{ "kind":"pdf", "file":"2023-record-of-account.pdf", "url":"https://…1h-expiry…", "sha256":"9f2c…a41b" },
{ "kind":"html", "file":"2023-record-of-account.html","url":"https://…1h-expiry…", "sha256":"3d81…7c02" },
{ "kind":"signed_8821", "file":"signed-8821.pdf", "url":"https://…1h-expiry…", "sha256":"77aa…09c3" }
] }
sha256 is computed at serve time; it may be null for legacy externally-stored artifacts.
Structured JSON
Machine-readable parse of a completed entity — filings, per-year compliance, and an income baseline — for resellers who consume data rather than files. Same x-api-key auth.
Companion endpoint: compares declared values against IRS-of-record values.
{ "entity": {…}, "filings": [{ "year":"2023","form":"1120S","filed":true,… }],
"compliance": {…}, "income_baseline": {…} }
Enroll in monitoring
{ "request_token":"your-loan-12345", "entity_name":"ACME Holdings LLC",
"frequency":"annual", // weekly | monthly | quarterly | annual | custom
"custom_interval_days":365, // required if frequency=="custom"
"skip_initial_pull":true, "expires_at":"2031-05-09", "next_pull_date":"2027-04-15" }
Returns the created subscription. If skip_initial_pull=false (default) an immediate_pull is queued. An entity with an active or paused subscription returns 409 with the existing one.
Webhooks — push result delivery
Configure your webhook self-serve in the developer portal: set the delivery URL and a signing secret, and send yourself signed test deliveries.
The signing secret is shown once — store it immediately. There is no per-request callback_url field.
Delivery fires when an entity's transcripts are completed. There is no separate event for intermediate steps like 8821 signed or IRS queued — poll for those.
Headers
Content-Type: application/json
User-Agent: ModernTax-Webhook/1.0
X-ModernTax-Signature: <hex HMAC-SHA256 of the raw body, keyed with your webhook secret>
Verifying the signature
Compute the HMAC over the raw request body (before any JSON parsing) and compare constant-time:
const crypto = require('crypto');
function verifyModernTax(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret)
.update(rawBody) // Buffer or exact raw string
.digest('hex');
const given = Buffer.from(signatureHeader || '', 'utf8');
const want = Buffer.from(expected, 'utf8');
return given.length === want.length && crypto.timingSafeEqual(given, want);
}
Payload
Two contracts are delivered; your account is configured for one.
{ "request_token":"your-loan-12345", "status":"complete", // partial | complete | error
"files":[{ "file_id":"uuid","type":"record_of_account","year":"2023",
"entity_name":"ACME Holdings LLC","html":"<…>","created_at":"…" }] }
{ "request_token":"your-loan-12345","status":"complete",
"record_id":"uuid", // deterministic — use for idempotency/dedupe
"record_type":"financial_verification",
// entity_profile | form_discovery | employment_compliance |
// financial_verification | risk_signals | credit_recommendation
"generated_at":"…","entity":{…},"data":{…} }
Retries
On failure ModernTax retries up to 5 attempts with backoff 10s, 30s, 90s, 5m, 15m. A 4xx response (except 413 and 429) marks the delivery dead immediately — no further retries.
Employment & income verification
A separate product for W-2 and 1099 verification. Same x-api-key scheme.
Poll returns parsed employment_data instead of transcripts. Ask for the employment schema when provisioning.
Errors
| Status | Meaning |
|---|---|
| 400 | Validation error (bad TID, non-PDF upload, file > 10 MB, missing required field, invalid schema value — body lists the valid values). |
| 401 | Missing or invalid x-api-key. |
| 402 | Account not enrolled in billing — a payment method or billing setup is required before ordering. |
| 404 | request_token or entity not found for your account. |
| 409 | Duplicate request_token, or an entity already has a monitoring subscription. |
| 422 | designee_caf_mismatch on /api/intake/8821-pdf — the uploaded 8821 names a non-ModernTax designee CAF; body includes designee_cafs. |
| 429 | Account request quota reached (body includes usage). |
Rate limits
A cumulative request quota is enforced per account when configured. Exceeding it returns 429 with { "error": "...", "usage": { "used", "limit" } }. Quotas are set with your plan — contact sales to raise one.
System status
Returns { "status": "ok" } when healthy — safe to poll for monitoring.
Developer portal
portal.moderntax.io/developer is self-serve management for your API account.
- API keys — rotate keys without a support ticket.
- Webhooks — set the delivery URL and signing secret (secret shown once) and fire signed test deliveries at your endpoint.
- Default response schema — pin
minimal,underwriting, orraworg-wide; an explicitschema=query parameter still overrides. - Usage — request counts against your quota.
Pricing & quotas
API accounts are billed monthly via ACH (Mercury) or Stripe at your negotiated rates, from $39.98 per verified entity — record-of-account plus return transcript, up to three years.
Setup fee and monthly minimums apply per contract. Add-ons — monitoring enrollment, per-pull monitoring, entity pre-validation — and volume pricing above 100 pulls per month are quoted with your plan. Email sales@moderntax.io and we'll send a firm rate the same day.
Security & compliance
- SOC 2 Type I complete; Type II in progress.
- Encrypted in transit (TLS 1.2+) and at rest (AES-256; encrypted SSN and credential columns).
- API keys SHA-256 hashed, constant-time verified.
- Per-tenant storage isolation via Postgres Row-Level Security on the storage bucket.
- Every partner request and result fetch is audit-logged with client identity, timestamp, and token.
- Signed transcript and 8821 URLs expire in 1 hour.
For SOC 2 evidence or vendor questionnaires: security@moderntax.io.
Versioning
This document describes the v1 surface — the /api/intake/* endpoints and /api/v1/transcripts/*. Breaking changes land under a new version path; existing endpoints remain stable.
matt@moderntax.io · ModernTax, Inc. — this reference is kept in sync with the deployed API.