ModernTaxAPI Reference
Partner API · v1

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.

x-api-key auth · no OAuth dance Self-serve sandbox, 250 requests SOC 2 Type I complete

Overview

A typical automated integration — how a reseller such as a lending platform runs it end to end.

1

Submit

Send one or more entities to POST /api/intake/transcript.

2

Authorize

Each entity needs a signed IRS Form 8821 — upload a pre-signed PDF you collected yourself, or let ModernTax collect the signature.

3

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.

Headers
x-api-key: mt_live_txn_yourco_xxxxxxxxxxxx
Content-Type: application/json    # or multipart/form-data where noted
How keys are handled

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

Base
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

POST/api/intake/transcript

Creates a request with one or more entities and queues them for IRS retrieval.

Authorization is separate unless you attach it inline

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
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.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"
      }
    }
  ]
}
FieldReqNotes
request_tokenyesYour unique id for the request. Reusing one returns 409.
loan_numbernoFalls back to request_token. Used on invoices.
entities[].entity_nameyesLegal name on file with the IRS.
entities[].tid / tid_kindyesEIN (XX-XXXXXXX) or SSN (XXX-XX-XXXX); "EIN"/"SSN".
entities[].yearsyes4-digit years, 1990–2028.
entities[].form_typeno1040, 1065, 1120, 1120S, 941. Inferred from tid_kind if omitted.
entities[].signed_8821noInline 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

201 Created
{
  "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 }
}
A bad designee never fails your request

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

POST/api/intake/8821-pdf

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 · multipart
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"
Fields · multipart/form-data
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
200 OK
{ "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" } }
422 · designee_caf_mismatch

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

POST/api/intake/8821

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.

Response
{ "request_id":"uuid","entities_created":12,"signature_requests_sent":12,"status":"8821_sent" }

Poll for results

GET/api/intake/transcript?token=<request_token>

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.

200 OK · legacy shape
{
  "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

GET/api/intake/transcript?token=<request_token>&schema=minimal|underwriting|raw

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.

schema=minimal

Just tell me if it's clean — filed years, unfiled years, flags, and file URLs.

schema=underwriting

The lines a credit memo needs — financials, liabilities, repayment posture, headline.

schema=raw

You parse it yourself — artifact list with per-file sha256.

cURL
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.

Shared envelope
{
  "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

Entity object
{ "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:

Entity object
{ …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

Entity object
{ "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

GET/api/v1/transcripts/{entityId}/structured

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.

GET/api/v1/transcripts/{entityId}/cross-reference

Companion endpoint: compares declared values against IRS-of-record values.

200 OK
{ "entity": {…}, "filings": [{ "year":"2023","form":"1120S","filed":true,… }],
  "compliance": {…}, "income_baseline": {…} }

Enroll in monitoring

POST/api/intake/monitoring
Request
{ "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.

Two things to know

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

Request headers we send
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:

Node
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.

v2 · incremental file delivery
{ "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":"…" }] }
v3 · structured records
{ "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.

POST/api/webhook/employment-intakeLive
GET/api/webhook/employment-result?token=<request_token>

Poll returns parsed employment_data instead of transcripts. Ask for the employment schema when provisioning.

Errors

StatusMeaning
400Validation error (bad TID, non-PDF upload, file > 10 MB, missing required field, invalid schema value — body lists the valid values).
401Missing or invalid x-api-key.
402Account not enrolled in billing — a payment method or billing setup is required before ordering.
404request_token or entity not found for your account.
409Duplicate request_token, or an entity already has a monitoring subscription.
422designee_caf_mismatch on /api/intake/8821-pdf — the uploaded 8821 names a non-ModernTax designee CAF; body includes designee_cafs.
429Account 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

GET/api/public/statusNo auth

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, or raw org-wide; an explicit schema= 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.

Questions or a key

matt@moderntax.io · ModernTax, Inc. — this reference is kept in sync with the deployed API.