File 1099s, W-2s and more β from your code.
REST over HTTPS, JSON in and out, OAuth 2.0. Every write is idempotent, every environment is app-scoped, and nothing reaches the IRS from sandbox β under one IRS-authorized transmitter, LEIRAD GROUP LLC.
Base URLs
sk_sandbox_β¦sk_live_β¦How filing works
Two ways in β the Forms API (scheme JSON) or a FIRE file β both land as ready_to_file forms under a payer. Nothing is charged until you move them to the cart, choose a service, and pay; that one payment kicks off IRIS submission automatically.
ready_to_file. Forms or a FIRE file. Nothing is filed or charged.{ form_id, service }.Quickstart
One recipient, one 1099-NEC, filed end to end against sandbox β five calls, nothing reaches the IRS. Swap sk_sandbox_β¦ for sk_live_β¦ (and the host) when you're ready for real.
| Step | Call |
|---|---|
| 1 Β· Token | POST /oauth/token β exchange your app's client credentials. |
| 2 Β· Form shape | GET /v1/forms/1099-NEC β the scheme JSON to fill. |
| 3 Β· File | POST /v1/payers/{payerId}/forms β fill the scheme, add to cart. |
| 4 Β· Review | GET /v1/cart/details β confirm the line item and price. |
| 5 Β· Pay | POST /v1/cart/pay β charge once, transmit. |
/v1/payers lists the ones your app can file under. In sandbox, your company comes pre-seeded.# 1 Β· get a token
TOKEN=$(curl -s https://sandbox.e1099f.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=$CLIENT_ID -d client_secret=$CLIENT_SECRET \
| jq -r .access_token)
# 2 Β· get the form's shape (fill it from here)
curl -s https://sandbox.e1099f.com/v1/forms/1099-NEC \
-H "Authorization: Bearer $TOKEN"
# 3 Β· file one form under a payer, straight into the cart
curl -s https://sandbox.e1099f.com/v1/payers/$PAYER_ID/forms \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "reference": "inv-1041", "addToCart": true,
"forms": [{ "form": "1099-NEC", "tax_year": 2025,
"jsonForm": {
"RecipientDetail": {
"TIN": "412557890", "TINSubmittedTypeCd": "INDIVIDUAL_TIN",
"PersonName": { "PersonFirstNm": "Jordan", "PersonLastNm": "Ellis" },
"MailingAddressGrp": { "USAddress": {
"AddressLine1Txt": "88 Palm Ave", "CityNm": "Miami",
"StateAbbreviationCd": "FL", "ZIPCd": "33101" } } },
"NonemployeeCompensationAmt": 18400 } }] }'
# 4 Β· review the cart
curl -s https://sandbox.e1099f.com/v1/cart/details \
-H "Authorization: Bearer $TOKEN"
# 5 Β· pay β the only call that transmits
curl -s https://sandbox.e1099f.com/v1/cart/pay \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"Sandbox
A parallel world with its own database. Free, deterministic, and it never reaches the IRS β so you can assert on outcomes in your tests.
Deterministic rules
| Filing | A payer or recipient reference containing reject simulates an IRS rejection; everything else is accepted. |
| TIN match | An all-zeros TIN returns not_issued; a TIN whose last digit is even returns match; odd returns mismatch. |
Reset
Wipes the account's sandbox filings back to a clean slate β useful between CI runs. Sandbox keys only.
curl β¦/v1/payers/9ead482d-β¦/forms -H "Content-Type: application/json" \
-d '{ "forms": [{ "form": "1099-NEC", "tax_year": 2025,
"jsonForm": {
"RecipientDetail": { "TIN": "412557890",
"PersonName": { "PersonFirstNm": "reject-me", "PersonLastNm": "Ellis" }, β¦ },
"NonemployeeCompensationAmt": 100 } }] }'{ "object": "sandbox_reset", "status": "reset", "cleared": 128 }Errors
One envelope, a machine-readable type. Branch on type, not the message.
| 422 | invalid_request β malformed or missing parameters |
| 401 | unauthorized β missing, expired, or revoked credential |
| 403 | insufficient_scope β token lacks the required scope |
| 403 | ip_not_allowed β production source IP isn't in the account's allowlist |
| 402 | payment_required β production filing not enabled / no funds |
| 404 | not_found β no such resource in this environment |
| 429 | rate_limited β wait for the Retry-After interval |
{
"error": {
"type": "invalid_request",
"message": "`tax_year` must be a 4-digit year."
}
}Rate limits, paging & idempotency
600 requests / minute, per credential. The limit counts requests, not forms β a 1,000-form batch is one request. A 429 carries Retry-After in seconds.
Pagination
GET lists take ?limit= (default 100, max 500) and ?starting_after=<id>. Loop while next_cursor is non-null β cursors, not page numbers, because these collections are written to while you walk them.
Idempotency
Send Idempotency-Key on filing POSTs. The key is claimed before the work starts, so a retry that races an in-flight request gets 409 request_in_progress rather than filing twice.
{
"object": "list",
"data": [ /* β¦ */ ],
"has_more": true,
"next_cursor": "9f2a4c1e-4b0f-4a01-9fbd"
}Idempotency-Key: 6f1e9c0a-6a1d-4f0e-9a11-2f2b1f0f6d55
Versioning
The API is versioned by date. Pin a version with the API-Version header and every response tells you which version served it. There is one active version today β 2026-06-15.
| How a request resolves its version | |
|---|---|
| API-Versionheader | Send a dated version, e.g. API-Version: 2026-06-15. An unrecognized value is rejected with 400 invalid_request β never a silent downgrade. |
| account defaultsetting | A production request with no header uses your account's default version, set in developer console β Settings. |
| currentfallback | With neither of the above, the request runs the current version. Sandbox always runs current. |
Every response carries an API-Version header naming the version that served it, so you can assert on it in tests.
Backwards-compatible changes
We add to the current version without cutting a new one. Non-breaking and shipped to everyone: a new endpoint, a new field on a response, a new optional request parameter, a new enum value, a new webhook event, or support for a new form or tax year. Write your parser to ignore unknown fields and new enum values so these never break you.
When we cut a new version
Removing or renaming a field, changing a field's type, or tightening validation is breaking β that only ever lands in a new dated version, never in one you're already on. Pin the version you built against (header or account default) and a newer version can't move under you until you deliberately adopt it.
curl β¦/v1/forms/1099-NEC \ -H "Authorization: Bearer sk_live_β¦" \ -H "API-Version: 2026-06-15" # every response echoes it back: # API-Version: 2026-06-15
{
"error": {
"type": "invalid_request",
"message": "Unknown API version `2020-01-01`. Supported: 2026-06-15."
}
}+ new endpoint + new response field + new optional parameter + new enum value / webhook event + new form or tax year
Authentication
Every request carries a bearer token β a server-side API key, or an OAuth 2.0 access token minted for one of your customers.
On behalf of customers (OAuth 2.0)
Authorization-code flow with PKCE (S256 required). The customer signs in, picks a single payer you may act for, approves your scopes. Codes are single-use, 5-minute; access tokens live 1 hour.
Scopes
| filings:write | Create and e-file returns |
| filings:read | Read filings and status |
| payers:read | List payers |
| recipients:write | Create and update recipients |
| tin:match | Run IRS TIN matches |
| webhooks:manage | Manage webhook subscriptions |
curl https://sandbox.e1099f.com/v1/payers \ -H "Authorization: Bearer sk_sandbox_9f2a4c1e77b0d31a"
const res = await fetch(base + "/v1/payers", {
headers: { Authorization: "Bearer " + process.env.E1099F_KEY }
});r = requests.get(base + "/v1/payers",
headers={"Authorization": f"Bearer {key}"})OAuth 2.0
File on behalf of your customers. The authorization-code flow with PKCE (S256) lets a customer approve your app for one of their payers without ever handing you a password.
1 Β· Authorize
Send the customer here. They sign in on the apex (a sandbox host never renders a password field), pick a single payer, and approve your scopes. You get a code back at your redirect_uri β single-use, expires in 5 minutes. Redirect URIs must match a registered value exactly and be HTTPS (http://localhost excepted).
2 Β· Token
Exchange the code for tokens. Supported grants: authorization_code, refresh_token, client_credentials (server-to-server, acting under your own account). Access tokens live 1 hour.
Refresh & revoke
Refresh rotates: the presented refresh token is revoked and a fresh pair minted, so a leaked, already-used token is inert. A customer can revoke your app per payer from Connected apps β the token endpoint then refuses with invalid_grant, and access ends when the current access token expires. Grants are per (app, account, payer): revoking one payer doesn't touch the others.
GET https://sandbox.e1099f.com/oauth/authorize ?response_type=code &client_id=clnt_7Kd2p9Xa &redirect_uri=https://app.example.com/cb &scope=filings:write%20payers:read &state=<csrf> &code_challenge=<BASE64URL(SHA256(verifier))> &code_challenge_method=S256
curl https://sandbox.e1099f.com/oauth/token \ -u clnt_7Kd2p9Xa:csec_sb_9f2a⦠\ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=https://app.example.com/cb \ -d code_verifier=<verifier>
{
"access_token": "eyJhbGciOiJβ¦",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_9f2a4c1eβ¦"
}{
"error": "invalid_grant",
"error_description": "Authorization was revoked by the account owner."
}Forms
Intake: file forms under a payer (one or many per call), import a FIRE file, or correct an accepted return β all brought to ready_to_file. These endpoints do not file and do not charge β pricing, the cart and payment live under Payments.
File under a payer
File one or many forms under a single payer. Each form is a filled scheme JSON β the exact object GET /v1/forms/{form} hands you β so you work in the IRS's own vocabulary and never touch internal box names. Saved as ready_to_file; pass addToCart: true to stage them in the cart.
GET /v1/payers β get the form's shape from GET /v1/forms/{form} β fill that scheme (recipient + amounts) β POST it here under the payer's id. Every form in the call publishes under {payerId}.Body
| Field | Description |
|---|---|
| formsarrayrequired | One or more { form, tax_year, jsonForm }. jsonForm is the filled scheme β its RecipientDetail is the payee (matched-or-created under the payer), the element values are the amounts. |
| referencestringoptional | Your id, echoed back on each row |
| addToCartbooleanoptional | Stage the saved forms in the cart β rows come back status: "in_cart". |
| servicestringoptional | Only honored with addToCart: true; otherwise the cart's default service applies. |
| physical_mailbooleanoptional | Only honored with addToCart: true. Overrides what service implies for the mail add-on; omit to keep service's default. |
| portal_accessbooleanoptional | Only honored with addToCart: true. Overrides what service implies for billed portal access; omit to keep service's default. |
boxes: the payer is the route id (an account has many payers), the recipient rides inside the scheme, and there is nothing to learn about our box names β the scheme tells you exactly what to send. A payer-scoped app can only file under payers granted to it.efile_print_mail β e-file to the IRS + print & mail the paper copy + billed recipient portal access. Corrections default to efile_print_mail too β a correction can be e-filed, e-filed + printed & mailed, or print & mail only, exactly like an original. Use physical_mail/portal_access for combinations the three service names can't express.curl β¦/v1/payers/9ead482d-β¦/forms \
-H "Authorization: Bearer sk_sandbox_9f2aβ¦" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1e9c0a-β¦" \
-d '{
"reference": "inv-1041", "addToCart": true,
"forms": [{
"form": "1099-NEC", "tax_year": 2025,
"jsonForm": {
"RecipientDetail": {
"TIN": "412557890", "TINSubmittedTypeCd": "INDIVIDUAL_TIN",
"PersonName": { "PersonFirstNm": "Jordan", "PersonLastNm": "Ellis" },
"MailingAddressGrp": { "USAddress": {
"AddressLine1Txt": "88 Palm Ave", "CityNm": "Miami",
"StateAbbreviationCd": "FL", "ZIPCd": "33101" } }
},
"NonemployeeCompensationAmt": 18400
}
}]
}'await client.payers("9ead482d-β¦").filings.create({
reference: "inv-1041", addToCart: true,
forms: [{
form: "1099-NEC", tax_year: 2025,
jsonForm: {
RecipientDetail: {
TIN: "412557890", TINSubmittedTypeCd: "INDIVIDUAL_TIN",
PersonName: { PersonFirstNm: "Jordan", PersonLastNm: "Ellis" },
MailingAddressGrp: { USAddress: {
AddressLine1Txt: "88 Palm Ave", CityNm: "Miami",
StateAbbreviationCd: "FL", ZIPCd: "33101" } }
},
NonemployeeCompensationAmt: 18400
}
}]
});{
"object": "payer_forms_intake",
"payer": "9ead482d-β¦", "created": 1, "failed": 0,
"livemode": true,
"forms": [{
"reference": "inv-1041#0", "id": "6a1d2e8a-β¦",
"status": "in_cart", "added_to_cart": true,
"cart_item_id": "c71f-β¦", "service": "efile_print_mail"
}]
}{
"object": "payer_forms_intake",
"payer": "9ead482d-β¦", "created": 0, "failed": 1,
"forms": [{
"reference": "inv-1041#0", "id": null, "status": "failed",
"error": { "type": "invalid_request",
"message": "`RecipientDetail.TIN` is required." }
}]
}FIRE import
The migration door off the retiring IRS FIRE system. Upload a Pub 1220 fixed-width file; we parse every payer, recipient and amount and bring the forms to ready_to_file. It parses and imports β it never files and never charges.
Raw request body (the file bytes), up to 12 MB. Pass addToCart: true to stage imported forms with the default efile_print_mail service.
curl "β¦/v1/payers/{payerId}/fire?addToCart=true" \
-H "Authorization: Bearer sk_sandbox_9f2aβ¦" \
-H "Content-Type: application/octet-stream" \
--data-binary @IRSTAX.txt{
"object": "fire_file", "bytes": 48210,
"rows_parsed": 42, "forms_imported": 40,
"forms_added_to_cart": 40, "service": "efile_print_mail",
"form_ids": ["6a1d2e8a-β¦", "1271f344-β¦"],
"cart_item_ids": ["c71f-β¦", "c8a0-β¦"],
"forms": [
{ "form": "1099-NEC", "payer": "Cedar & Oak LLC",
"recipient": "Jordan Ellis", "status": "in_cart" } ],
"notes": [ "2 rows skipped: unsupported form 1099-DA." ],
"next": "Added to the cart with service `efile_print_mail`. Pay with /v1/cart/pay."
}Corrections
A material change after the IRS accepted a return β a new return that supersedes it, at the correction price. Distinct from a retransmission of a rejected filing, which is free.
| Field | Description |
|---|---|
| {id}path Β· uuidrequired | The original accepted filing being corrected. Must be accepted or accepted_with_errors β a rejected filing is re-filed, not corrected (404 if unknown). Payer, recipient, form and year are inherited from it. |
| boxesobjectrequired | Just the fields that changed, keyed by the same IRS IRIS element names the scheme uses (e.g. NonemployeeCompensationAmt). The original's payer, recipient and unchanged values carry over β a correction is a targeted edit to a known filing, so it takes only the delta, not a whole scheme. |
| addToCartbooleanoptional | Stage with the default efile_print_mail (e-file + print&mail + portal). A correction is delivered like any form β you may choose efile or print_mail instead. |
{
"object": "correction", "id": "fil_c88",
"corrects": "fil_a91", "form": "1099-NEC",
"tax_year": 2025, "status": "queued",
"amount_charged_cents": 794
}{
"error": { "type": "not_found", "message": "No such filing." }
}{
"error": { "type": "not_correctable",
"message": "That filing was rejected, so it is not on file β submit it again instead of correcting it." }
}Void a filing
Cancel a filing that hasn't gone anywhere yet and get the money back. Once it has left for the IRS the remedy is a correction.
{
"object": "filing", "id": "fil_a91d20c7",
"status": "voided", "amount_refunded_cents": 503
}{
"error": { "type": "not_cancellable",
"message": "That filing is submitted and can no longer be cancelled. File a correction instead." }
}Payments
Once forms are ready_to_file: read the prices, add each form to the cart with a chosen service, then pay. Payment charges once and sends every form through the e-file pipeline. Volume pricing is progressive and per order β one payment is one order.
Prices & services
Read the services you can buy for a form and what each costs, so you can choose one per form before adding it to the cart. Brackets are progressive; the quote is the price, not an estimate.
/quote?form=1099-NEC&service=efile_print_mail returns what this account pays for its next filing, itemized β the same method checkout charges with.
{
"object": "rate_card", "currency": "usd",
"services": [
{ "id": "efile", "label": "E-file",
"unit_cents": 269, "online_access_cents": 49 },
{ "id": "efile_print_mail", "label": "E-file + print & mail",
"unit_cents": 269, "print_mail_cents": 185, "online_access_cents": 49 },
{ "id": "print_mail", "label": "Print & mail only",
"print_mail_cents": 185, "online_access_cents": 49 } ],
"brackets": [
{ "forms": "1-10", "efile_cents": 269 },
{ "forms": "11-100", "efile_cents": 169 },
{ "forms": "5001+", "efile_cents": 69 } ]
}Add to the cart
Move ready_to_file forms into the account cart, each with its chosen service β a list of { form_id, service }.
physical_mail/portal_access booleans β additive overrides on top of service. Omit them to get exactly what service has always implied; set them for a combination the three legacy service names can't express on their own.curl β¦/v1/cart/items -H "Content-Type: application/json" \
-d '{ "forms": [
{ "form_id": "6a1d2e8a-β¦", "service": "efile" },
{ "form_id": "1271f344-β¦", "service": "efile_print_mail" },
{ "form_id": "d92e6a11-β¦", "service": "efile", "physical_mail": true }
] }'{
"object": "cart", "id": "crt_44b1", "item_count": 2,
"total_cents": 821, "currency": "usd",
"charge_lines": [
{ "type": "FederalEFile", "amount_cents": 538 },
{ "type": "RecipientPortal", "amount_cents": 98 } ],
"pricing_segments": [
{ "pricing_group": "FederalInformationReturns",
"bracket": "1-10", "from_position": 1, "to_position": 2,
"quantity": 2, "unit_cents": 269, "amount_cents": 538 } ]
}Pay the cart
Charge the whole cart as one order, then send every form through the e-file pipeline β the same path every form in the app takes. Every item is validated and prepared before your card is touched.
| A bad row costs nothing | 422 cart_not_payable lists each problem β nothing charged, nothing filed. |
| Retrying is safe | Paid cart β receipt with already_paid; racing an in-flight pay β 409. |
| Paid means filed | A charged form that can't transmit returns its money to your balance; the rest stay filed. |
{
"object": "cart_payment", "cart": "crt_44b1",
"status": "paid", "filed": 2, "failed": 0,
"amount_charged": 821, "currency": "usd",
"filings": [
{ "reference": "inv-1041", "id": "fil_a91",
"status": "submitted", "amount_charged": 318 },
{ "reference": "inv-1042", "id": "fil_a92",
"status": "submitted", "amount_charged": 503 } ]
}{
"error": { "type": "cart_not_payable",
"message": "1 of 2 filings can't be submitted. Nothing was charged and nothing was filed." },
"problems": [
{ "item": "β¦", "reference": "inv-1042",
"type": "invalid_request",
"message": "Recipient is missing a mailing address." } ]
}Orders & receipts
A paid cart becomes an order β the receipt of that payment. List them, read one with its full charge breakdown, or fetch the IRS acknowledgment for everything it filed.
The receipt carries stable charge_lines (Membership, FederalEFile, RecipientPortal, PrintAndMail, StateFiling, Correction, Discountβ¦) and pricing_segments β the exact progressive brackets the order was priced at, stored so an old receipt never re-prices when rates change.
{
"object": "list", "data": [
{ "id": "ord_71c2", "number": "E1F-2025-000148",
"payment_status": "paid", "forms": 2,
"created_at": "2025-05-01T05:14:00Z" } ]
}{
"object": "order", "id": "ord_71c2",
"number": "E1F-2025-000148", "payment_status": "paid",
"created_at": "2025-05-01T05:14:00Z",
"charge_lines": [
{ "type": "FederalEFile", "label": "Federal e-file", "amount_cents": 538 },
{ "type": "PrintAndMail", "label": "Print & mail", "amount_cents": 370 },
{ "type": "RecipientPortal", "label": "Online access", "amount_cents": 98 } ],
"pricing_segments": [
{ "pricing_group": "FederalInformationReturns", "bracket": "1-10",
"from_position": 1, "to_position": 2, "quantity": 2,
"unit_cents": 269, "amount_cents": 538 } ],
"grand_total_cents": 1006, "currency": "usd",
"forms": [
{ "id": "fil_a91", "form": "1099-NEC", "recipient": "Jordan Ellis",
"status": "accepted" } ]
}Prepaid balance
A wallet you can pre-fund; filing charges draw it before the card. The ledger is append-only and every line carries the running balance.
{
"object": "balance", "available_cents": 24500, "currency": "usd",
"auto_replenish": { "enabled": true, "threshold_cents": 5000, "topup_cents": 20000 },
"recent": [
{ "kind": "topup", "amount_cents": 20000, "balance_after_cents": 24500 },
{ "kind": "filing", "amount_cents": -503, "balance_after_cents": 4500 } ]
}Add-ons
The compliance services around a filing β TIN matching and recipient statements. Priced per use, with membership allotments applied automatically.
TIN matching
Check a recipient's name + TIN against IRS records before filing. Asynchronous by necessity β Pub 2108-A caps interactive matching at 999/24h on one platform User ID.
not_issued; even last digit β match; odd β mismatch.{ "object": "tin_match_run", "id": "tmr_5c19",
"status": "processing", "count": 2 }{
"object": "tin_match_run", "id": "tmr_5c19",
"status": "completed",
"results": [
{ "recipient": "rcp_5f1cβ¦", "result": "match" },
{ "recipient": "rcp_8a20β¦", "result": "mismatch" } ]
}Recipient statements (Copy B)
The recipient's copy as a branded PDF, per filing, per batch, or per order. Available once the IRS accepts the return.
HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="1099-NEC-copyb.pdf"
{
"error": { "type": "not_ready",
"message": "Copy B is available once the IRS accepts the filing." }
}Status & events
How a filing's state is reported, what the IRS said, and how to be told the moment it changes.
Filing status vocabulary
Stable, and deliberately not our internal enum names β the pipeline can grow without breaking your code.
| queued | Accepted by us, not yet transmitted. |
| submitted | Sent to the IRS; awaiting acknowledgment. |
| furnishing | A print-&-mail-only return β we furnish the recipient copy but don't e-file it (you filed elsewhere). E-file + print&mail returns transmit and furnish, and report submitted β accepted. |
| accepted | The IRS acknowledged it, clean. |
| accepted_with_errors | Acknowledged with correctable issues. |
| rejected | Not on record β re-file it (free). |
| voided | Cancelled before transmit; refunded. |
| superseded | Replaced by a correction. |
Acknowledgments
The IRS decision for a filing, with its own rule ids and messages passed through unmodified.
Batch and order acks return every filing's acknowledgment in one response β the raw IRS rule ids, severity, xpath and message, passed through unmodified.
{
"object": "acknowledgment", "filing": "fil_a91",
"status": "accepted_with_errors",
"receipt_id": "1095720250000123",
"errors": [
{ "code": "F1099-032", "severity": "warning",
"xpath": "/Return/β¦/RecipientNm",
"message": "Recipient name control mismatch." } ]
}Webhooks
Signed delivery with retries β filing.accepted, filing.rejected, tin_match.completed, cart.paid.
Register, list, update the URL or events, disable, delete, rotate the signing secret, and inspect recent deliveries (status, attempts, last error). The secret is returned once β on create and on rotate.
Verifying the signature
Every delivery carries E1099-Signature: t=<unix>,v1=<hex>. The v1 digest is HMAC-SHA256 over "{t}.{rawBody}" β the timestamp is signed, so a captured delivery can't be replayed. Verify against raw bytes, compare in constant time, reject anything older than your tolerance.
{
"id": "evt_9c0a", "type": "filing.accepted",
"created": 1753008421,
"data": { "id": "fil_a91d20c7", "form": "1099-NEC",
"tax_year": 2025, "status": "accepted",
"receipt_id": "1095720250000123" }
}const [t, v1] = header.split(",").map(p => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(v1))
&& (Date.now()/1000 - t) < 300;var p = header.Split(',').Select(x => x.Split('=',2))
.ToDictionary(x => x[0], x => x[1]);
var expected = Convert.ToHexString(HMACSHA256.HashData(
key, Encoding.UTF8.GetBytes($"{p["t"]}.{rawBody}")))
.ToLowerInvariant();
var ok = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(p["v1"]));Compliance
Extensions of time, and the payer & recipient records every filing points at.
Extensions of time (8809)
When the deadline is close and the data isn't, ask the IRS for more time. Free β a request to the IRS, not a product.
| Field | Description |
|---|---|
| payeruuidrequired | One request covers one payer |
| tax_yearintegerrequired | |
| formsarrayrequired | 1099, 5498, 1042S, 1099QA, 5498QA, 5498ESA, 5498SA |
| reasonsobjectTY2026+ | The IRS requires a reason from TY2026 (rule F8809016) |
already_requested: true β a duplicate 8809 is a rejection, not a longer extension.curl β¦/v1/extensions -H "Content-Type: application/json" \
-d '{ "payer": "9ead482d-β¦", "tax_year": 2026,
"forms": ["1099", "5498"],
"reasons": { "payee_statements_not_received": true } }'{
"object": "extension", "id": "ext_71c2",
"payer": "9ead482d-β¦", "tax_year": 2026,
"forms": ["1099", "5498"], "status": "accepted",
"receipt_id": "8809-2026-000045", "livemode": true
}Payers & recipients
The entity whose EIN goes on the return, and the payees it files for. A payer-scoped OAuth grant may not create payers.
A PATCH changes only the fields it names. A payer's TIN can't be changed through the API (it identifies the entity on returns already filed); a recipient's TIN can't be edited once they have filings β file a correction instead.
{
"object": "recipient", "id": "rcp_5f1c9a",
"payer": "9ead482d-β¦", "name": "Jordan Ellis",
"tin_type": "ssn", "tin_last4": "4417",
"state": "FL", "delivery_method": "electronic"
}Objects & models
Every request and response is built from the objects below. A field marked required must be present when you create the object; optional fields may be omitted. Write-only fields are accepted on input but never returned; response fields are server-assigned and ignored on input.
tin_last4) are ever returned β the raw tin never leaves your app.{
"id": "uuid, server-assigned",
"object": "the type name, e.g. \"payer\"",
"tin": "write-only Β· digits only",
"tin_last4": "response Β· last 4 digits",
"state": "2-letter USPS code, upper-cased",
"country": "defaults to \"US\" when omitted"
}Payer
The business that files the return β the entity in the top-left box of every 1099. Created by POST /v1/payers, referenced by id everywhere else.
| Field | Description |
|---|---|
| iduuidresponse | Server-assigned identifier. object is "payer". |
| legal_namestringrequired | Legal name of the filing business. |
| tinstring Β· 9 digitsrequired Β· write-only | EIN or SSN, digits only. Never returned. |
| tin_typeenumrequired | ein or ssn. |
| tin_last4stringresponse | Last four digits of the stored TIN. |
| address_line1stringrequired | Street address. |
| address_line2stringoptional | Suite, unit or PO box. |
| citystringrequired | City. |
| statestring Β· 2 charrequired | USPS state / territory code, upper-cased on save. |
| postal_codestringrequired | ZIP or ZIP+4. |
| countrystringoptional | Two-letter country code. Defaults to US. |
| contact_namestringrequired | Person the IRS can contact about the filing. |
| contact_emailstringrequired | Contact email. |
| contact_phonestring Β· digitsrequired Β· write-only | Contact phone; stored as digits. |
| is_main_companybooleanresponse Β· list only | Returned by GET /v1/payers. true for the account's own company β the business the account itself is, as opposed to a client it files on behalf of. At most one payer is flagged, and many accounts flag none; filing works identically either way. |
{
"object": "payer",
"id": "9ead482d-3b7c-4a1e-β¦",
"legal_name": "Northwind Freight LLC",
"tin_type": "ein",
"tin_last4": "4821",
"address_line1": "410 Cedar St",
"address_line2": "Suite 300",
"city": "Austin",
"state": "TX",
"postal_code": "78701",
"country": "US",
"contact_name": "Dana Reyes",
"contact_email": "ap@northwind.example"
}Recipient
The person or business that receives the statement β the payee. Belongs to exactly one payer. Created by POST /v1/recipients, or inline inside a filing (see below).
| Field | Description |
|---|---|
| iduuidresponse | Server-assigned. object is "recipient". |
| payeruuidrequired | The payer this recipient belongs to (from GET /v1/payers). |
| namestringrequired | Recipient name. |
| tinstring Β· 9 digitsrequired Β· write-only | SSN or EIN, digits only. |
| tin_typeenumoptional | ssn or ein. Omit to match a recipient of any TIN type. |
| tin_last4stringresponse | Last four digits of the stored TIN. |
| address_line1stringrequired | Street address. |
| address_line2stringoptional | Suite, unit or PO box. |
| citystringrequired | City. |
| statestring Β· 2 charrequired | USPS state / territory code. |
| postal_codestringrequired | ZIP or ZIP+4. |
| countrystringoptional | Defaults to US. |
| emailstringconditional | Required when delivery_method is electronic. |
| delivery_methodenumoptional | paper (default) or electronic. |
| account_numberstringoptional | Your account number for this recipient; printed on the statement. |
POST /v1/payers/{payerId}/forms also accepts these fields as an inline object in place of a recipient id β { tin, tin_type?, name, address_line1, address_line2?, city, state, postal_code, country? }. It is matched to an existing recipient under the payer by TIN, or created.{
"object": "recipient",
"id": "rcp_5f1c9a",
"payer": "9ead482d-β¦",
"name": "Jordan Ellis",
"tin_type": "ssn",
"tin_last4": "4417",
"address_line1": "88 Palm Ave",
"city": "Miami",
"state": "FL",
"postal_code": "33101",
"country": "US",
"email": "jordan@example.com",
"delivery_method": "electronic",
"account_number": "A-2043"
}Filing
One information return for one recipient. Created by POST /v1/payers/{payerId}/forms as ready_to_file; it becomes billable only once it is in the cart and paid. Sandbox ids are prefixed fil_sb_.
| Field | Description |
|---|---|
| objectstringresponse | "filing". |
| iduuidresponse | Filing identifier. |
| formstringresponse | Form type, e.g. 1099-NEC. |
| tax_yearintegerresponse | Four-digit tax year. |
| statusenumresponse | Lifecycle state β see Filing status under Enumerations below. |
| payeruuidresponse | The filing business. |
| recipientuuidresponse | The payee. |
| serviceenumresponse | Delivery service β efile, efile_print_mail or print_mail. |
{
"object": "filing",
"id": "6a1d2e8a-4b4b-4d20-β¦",
"form": "1099-NEC",
"tax_year": 2025,
"status": "accepted",
"payer": "9ead482d-β¦",
"recipient": "rcp_5f1c9a",
"service": "efile_print_mail"
}Correction intake
Returned by POST /v1/filings/{id}/corrections. A correction is a filing like any other β its correction fee comes from the form type, not the service β so it defaults to efile_print_mail and can be delivered any way an original can.
| Field | Description |
|---|---|
| objectstringresponse | "correction_intake". |
| iduuidresponse | The correction form's id. |
| correctsuuidresponse | Id of the original filing. You must have filed the original through e1099f β the IRS needs its receipt id. |
| formstringresponse | Inherited from the original. |
| tax_yearintegerresponse | Inherited from the original. |
| statusenumresponse | ready_to_file, or in_cart when addToCart was set. |
| currencystringresponse | usd. |
| added_to_cartbooleanresponse | Whether it was staged in the cart in the same call. |
| cart_item_iduuid | nullresponse | Set when added_to_cart is true. |
| serviceenumresponse | Defaults to efile_print_mail. |
{
"object": "correction_intake",
"id": "b2c9β¦",
"corrects": "6a1d2e8a-β¦",
"form": "1099-NEC",
"tax_year": 2025,
"status": "ready_to_file",
"currency": "usd",
"added_to_cart": false,
"cart_item_id": null,
"service": "efile_print_mail"
}Webhook endpoint
A subscription that receives event callbacks for the environment it was created in. The signing secret is returned in full exactly once β on create and on rotate-secret; afterward only its prefix is shown.
| Field | Description |
|---|---|
| iduuidresponse | Endpoint identifier. |
| urlstring Β· httpsrequired | HTTPS URL that receives the event POSTs. |
| eventsstring[]required | Event types to subscribe to, or ["*"] for all. |
| statusenumoptional | active or disabled. |
| signing_secretstringresponse Β· once | whsec_β¦. Verify each delivery's signature with it. Shown in full only on create / rotate. |
signing_secret when you first receive it. It cannot be read back β if you lose it, call POST /v1/webhooks/{id}/rotate-secret for a new one.{
"id": "e8b1β¦",
"url": "https://app.example.com/hooks/e1099f",
"events": ["*"],
"status": "active",
"signing_secret": "whsec_3nZ8β¦"
}Cart item
A ready-to-file form staged for payment. Added with POST /v1/carts/{id}/items; the cart is billed as one order by POST /v1/cart/pay.
| Field | Description |
|---|---|
| form_iduuidrequired | A ready_to_file form's id. |
| serviceenumoptional | efile, efile_print_mail or print_mail. Omitted or unrecognised falls back to the form's default service (efile_print_mail). |
{
"form_id": "6a1d2e8a-β¦",
"service": "efile_print_mail"
}Enumerations
The closed sets of string values used across the objects above.
| Field | Values |
|---|---|
| tin_type | ein Β· ssn |
| delivery_method | paper Β· electronic |
| service | efile Β· efile_print_mail Β· print_mail |
Filing status
| Value | Meaning |
|---|---|
| ready_to_file | Saved by intake, not yet in the cart. Not billed. |
| in_cart | Staged in the cart, awaiting payment. |
| queued | Paid; waiting to transmit / furnish. |
| submitted | Transmitted to the IRS; awaiting acknowledgement. |
| furnishing | Print-&-mail-only: the recipient copy is being furnished, with no IRS transmission for this form. |
| accepted | Accepted by the IRS. |
| accepted_with_errors | Accepted, but the IRS flagged issues to correct. |
| rejected | Rejected by the IRS. |
| voided | Cancelled before transmission. |
| superseded | Replaced by a later filing. |
ready_to_file
β in_cart
β queued ββ pay
ββ submitted β accepted
β β accepted_with_errors
β β rejected
ββ furnishing (print & mail only)
voided (before transmit)
superseded (replaced later)Error
Every 4xx and 5xx response carries the same envelope. The HTTP status carries the class; error.type is the machine-readable code and error.message the human-readable reason.
| Field | Description |
|---|---|
| error.typestringresponse | Stable code, e.g. invalid_request, not_found, conflict. |
| error.messagestringresponse | Human-readable explanation of what to fix. |
Status codes
| HTTP | Typical type |
|---|---|
| 400 | Malformed request. |
| 401 | Missing or invalid access token. |
| 403 | ip_not_allowed β a production credential used from outside the account's IP allowlist. |
| 404 | not_found. |
| 409 | conflict β e.g. a correction already in flight, or a duplicate original. |
| 422 | invalid_request β a field failed validation. |
{
"error": {
"type": "invalid_request",
"message": "`tin` must be nine digits."
}
}