The IRISfile API

The IRISfile API checks 1099 files and converts them to IRIS XML. It files them with the IRS under IRISfile's transmitter control code (TCC) and reports each receipt and acknowledgement by webhook. The API is in private beta.

OpenAPI 3.1 spec: https://irisfile.co/openapi.json

The beta runs on sandbox keys

During the private beta, every call uses a sandbox key. A sandbox key files with the IRS test environment and never files a live return. To join the beta, email support@irisfile.co.

Test TINs only

Every payer and payee TIN in a sandbox file must start with 000, as IRS Publication 5719 requires for test data. Any other TIN fails with: "Sandbox files accept only IRS test TINs (Pub 5719: first three digits 000). Use a live API key for real taxpayer data."

No license and no charges

Sandbox files need no license and use no form credits: debitedCents and every quote read 0. balanceCents still shows your account's credit balance.

XML output only

A sandbox file serves the xml output. The portal-csv and copy-b outputs answer 409 sandbox_output_unavailable.

Separate from live data

A sandbox key sees only sandbox submissions and sandbox webhook endpoints. Any other id answers 404 not_found.

Base URL and authentication

Base URL
https://irisfile.co/api/v1
API key
Send it as Authorization: Bearer <key> or as an x-api-key header. Sandbox keys start with irf_test_.
Rate limit
60 requests a minute per key, across all servers. Over it, the API answers 429 with a Retry-After header.
Request size
Up to 4.5 MB per request. Upload a larger file, up to 50 MB, through POST /uploads.
Errors
Every error has the shape {"error": {"code", "message", "requestId"}}. Branch on code. The requestId appears on operational failures; quote it to support.

Quick start: from a file to an IRS acknowledgement

These steps file two test 1099-NEC records with the IRS sandbox using curl. Each step uses the values the step before it returned.

  1. Create a sandbox key

    In the dashboard, open API keys, choose Sandbox and click Create key. Copy the key: IRISfile shows it once. The API key help article shows each screen.

    export IRISFILE_API_KEY=irf_test_...   # your sandbox key
    export BASE=https://irisfile.co/api/v1
  2. Register a webhook endpoint

    Register an https endpoint before you send a file: IRISfile sends an event only to the endpoints that exist when it happens. Copy the secret from the response: it is not shown again. Send a test event, then check the delivery.

    curl -s "$BASE/webhook-endpoints" \
      -H "Authorization: Bearer $IRISFILE_API_KEY" \
      -H "content-type: application/json" \
      -d '{"url":"https://example.com/irisfile/webhook","description":"Staging"}'
    {
      "endpoint": { "id": "0b7d...", "url": "https://example.com/irisfile/webhook", "environment": "sandbox", ... },
      "secret": "whsec_..."
    }
    export ENDPOINT=0b7d...   # endpoint.id
    curl -s -X POST "$BASE/webhook-endpoints/$ENDPOINT/test" -H "Authorization: Bearer $IRISFILE_API_KEY"
    curl -s "$BASE/webhook-endpoints/$ENDPOINT/deliveries" -H "Authorization: Bearer $IRISFILE_API_KEY"
  3. Validate a test file

    Save these two records as sandbox-nec.csv. They use the IRISfile CSV template columns and IRS test TINs. Validating stores nothing.

    form_type,tax_year,payer_tin,payer_tin_type,payer_name,payer_address,payer_city,payer_state,payer_zip,payer_phone,payee_tin,payee_tin_type,payee_name,payee_address,payee_city,payee_state,payee_zip,account_number,box_1
    NEC,2025,00-0111111,EIN,Harbor Light Bookkeeping LLC,40 Wharf St,Portland,ME,04101,2075550100,000-11-1112,SSN,Dana M Whitfield,18 Spring St,Portland,ME,04102,HL-001,8400.00
    NEC,2025,00-0111111,EIN,Harbor Light Bookkeeping LLC,40 Wharf St,Portland,ME,04101,2075550100,000-11-1113,SSN,Luis A Ferreira,7 Ocean Ave,South Portland,ME,04106,HL-002,12650.00
    
    curl -s "$BASE/validate" \
      -H "Authorization: Bearer $IRISFILE_API_KEY" \
      -F "file=@sandbox-nec.csv"
    {
      "validation": {
        "source": "csv",
        "recordCount": 2,
        "counts": { "parseErrors": 0, "validationErrors": 0, "warnings": 0, "infos": 1, "excludedRecords": 0 },
        "readyToConvert": true,
        "diagnostics": [{
          "recordIndex": null, "sourceRow": null, "field": null,
          "severity": "info", "code": "cfsf.auto_elected",
          "message": "CF/SF elected from the payee's state for 2 records (ME 2)."
        }],
        "diagnosticsTruncated": false
      }
    }
  4. Create a submission

    The same file, sent to /submissions, is stored and converted to IRIS XML in the same request. The answer is 201 with the submission. A file over 4.5 MB goes through /uploads first, and that submission converts in the background (202).

    curl -s "$BASE/submissions" \
      -H "Authorization: Bearer $IRISFILE_API_KEY" \
      -F "file=@sandbox-nec.csv"
    {
      "submission": {
        "id": "6f1c2a9e-...",
        "status": "xml_generated",
        "stage": "ready_to_file",
        "counts": { "records": 2, "validationErrors": 0, "lockedForms": 0, ... },
        "transmissions": [{ "sequence": 1, "taxYear": 2025, "records": 2, "status": "not_filed", ... }],
        ...
      }
    }
    # Files over 4.5 MB: reserve, PUT, then create from the token.
    curl -s "$BASE/uploads" -H "Authorization: Bearer $IRISFILE_API_KEY" \
      -H "content-type: application/json" \
      -d '{"filename":"big.csv","size":31457280,"contentType":"text/csv"}'
    curl -X PUT "<uploadUrl>" -H "content-type: text/csv" --data-binary @big.csv
    curl -s "$BASE/submissions" -H "Authorization: Bearer $IRISFILE_API_KEY" \
      -H "content-type: application/json" \
      -d '{"uploadToken":"<uploadToken>","filename":"big.csv"}'
  5. Check the status

    Poll the submission until its stage is ready_to_file or ready_to_unlock, or wait for the submission.converted webhook. Then file it. ready_to_unlock means some forms are not paid for yet. The file call pays for them from your credits, and it needs the tax year's license. If the stage reads needs_fixing, list the errors, fix the file and create a new submission.

    export ID=6f1c2a9e-...   # submission.id
    curl -s "$BASE/submissions/$ID" -H "Authorization: Bearer $IRISFILE_API_KEY"
    curl -s "$BASE/submissions/$ID/diagnostics?severity=error" -H "Authorization: Bearer $IRISFILE_API_KEY"
  6. File with the IRS

    The file call sends each transmission to the IRS sandbox and returns its receipt ID. A sandbox file needs no body. If the answer is 409 filing_incomplete, send the same call again later: a transmission is never sent twice.

    curl -s -X POST "$BASE/submissions/$ID/file" \
      -H "Authorization: Bearer $IRISFILE_API_KEY"
    {
      "status": "submitted",
      "transmissions": [{
        "sequence": 1,
        "status": "receipted",
        "utid": "c55915bb-...:IRIS:...::A",
        "receiptId": "2025-...",
        "state": "submitted",
        ...
      }],
      "billing": { "debitedCents": 0, "balanceCents": 0 }
    }
  7. Download the XML

    The outputs route answers 302 to a storage link that expires in 5 minutes. Its JSON body repeats the link with the billing.

    # Without -L, curl prints the JSON body: the link, its expiry and the billing.
    curl -s "$BASE/submissions/$ID/outputs/xml" -H "Authorization: Bearer $IRISFILE_API_KEY"
    # With -L, curl follows the link and saves the file.
    curl -sL "$BASE/submissions/$ID/outputs/xml" -H "Authorization: Bearer $IRISFILE_API_KEY" -o irs.xml
  8. Wait for the acknowledgement

    The IRS sandbox acknowledges a transmission after it gives the receipt. IRISfile asks the IRS for acknowledgements at most every 60 minutes, and less often after an IRS lookup error. Your endpoint then gets transmission.acknowledged with the ack code. Without a webhook, poll the submission until each transmission has an ackCode.

Endpoints

Every path is relative to https://irisfile.co/api/v1. The OpenAPI spec has each request body, response and error code.

POST /validate
Check a file or JSON records. Stores nothing.
POST /uploads
Reserve a direct upload for a file over 4.5 MB.
POST /submissions
Create a submission from a file or an upload, and convert it.
GET /submissions/{id}
Status, counts, the quote for unpaid forms and each transmission.
GET /submissions/{id}/diagnostics
Errors, warnings and notices, one page at a time.
POST /submissions/{id}/file
File with the IRS. Safe to retry.
GET /submissions/{id}/outputs/{kind}
Redirect to the xml, portal-csv or copy-b download.
GET /webhook-endpoints
List your webhook endpoints.
POST /webhook-endpoints
Register an endpoint. Returns its signing secret once.
DELETE /webhook-endpoints/{id}
Delete an endpoint and cancel its pending deliveries.
POST /webhook-endpoints/{id}/test
Send a webhook.test event to one endpoint.
GET /webhook-endpoints/{id}/deliveries
The endpoint's recent deliveries and their results.

Common error codes

Only the file call and the outputs route spend form credits. A form stays paid after its first debit, so an error after the debit costs nothing to retry. The spec lists every code per route.

invalid_api_key 401
Send a current key as Authorization: Bearer or x-api-key.
rate_limited 429
Wait for the seconds in Retry-After, then retry.
invalid_request 400
Fix the body or the query. The message names the problem.
not_found 404
The id is not in this account, or it belongs to the other environment.
not_ready 409
The file is still converting. Retry after Retry-After.
filing_incomplete 409
Some transmissions may already be filed and paid. Keep the submission, read transmissions and billing in the body, then retry the same call. Nothing is sent twice.
transmission_required 409
Add ?transmission=<sequence> to download one transmission's XML.
sandbox_output_unavailable 409
Sandbox files serve only the xml output.
license_required 402
Buy the tax year's license in the dashboard.
payment_required 402
Buy form credits. The body carries the quote and the balance.
irs_filing_failed 502
The IRS refused or failed the call. Retry later; the body lists each transmission.

Credits and billing headers

Live filing needs the tax year's license and prepaid form credits. The file call debits credits for each unpaid form before any transmission exists. A form paid once, in the dashboard or through the API, costs nothing again. See pricing for the rates.

With too few credits, the call answers 402 payment_required with the quote and the balance. Successful file calls return a billing object. The outputs redirect carries the same numbers as headers: IRISfile-Debited-Cents, IRISfile-Balance-Cents and, when known, IRISfile-Excluded-Records. A client that follows the redirect loses those headers, so read them with redirects off.

Webhooks

IRISfile posts each event as JSON to your https endpoints in the key's environment. An account can hold 5 endpoints per environment. Payloads carry ids, statuses, counts and IRS receipt IDs, never a TIN, a name or an amount.

submission.converted

A conversion finished, once per submission: ready_to_file, needs_fixing, split or failed.

transmission.receipted

The IRS gave a transmission its receipt ID.

transmission.acknowledged

The IRS acknowledgement arrived: ackCode A, E, P or R.

transmission.failed

The IRS refused the transmission, or 72 hours passed with no acknowledgement. Checks continue after it.

webhook.test

You asked for a test event for one endpoint.

An event

{
  "id": "5d0e...",
  "type": "transmission.acknowledged",
  "created": "2026-10-21T18:04:11.000Z",
  "apiVersion": "2026-10-21",
  "environment": "sandbox",
  "data": {
    "submission": { "id": "6f1c2a9e-...", "parentId": null },
    "transmission": { "id": "...", "sequence": 1, "taxYear": 2025, "receiptId": "2025-...",
                      "ackCode": "A", "irsStatus": "Accepted", "errorCount": 0 }
  }
}

Delivery

  • Each post carries IRISfile-Signature, IRISfile-Event-Id and IRISfile-Event-Type headers.
  • Answer with any 2xx status within 10 seconds. A redirect or any other status counts as a failure.
  • A failed post is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. The delivery stops after 7 attempts.
  • Delivery is at least once, so skip an event id you have already handled.

Verify the signature

The header reads t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256 of ${t}.${raw body}, keyed with the endpoint's whole whsec_ secret. Compute it over the raw body before you parse it, compare in constant time, and reject a t more than 300 seconds from your clock.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received, before JSON.parse.
// header: the IRISfile-Signature header. secret: your whsec_... string.
export function verifyIrisfileSignature(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  let t = null;
  const v1 = [];
  for (const part of header.split(",")) {
    const i = part.indexOf("=");
    if (i < 0) continue;
    const key = part.slice(0, i).trim();
    const value = part.slice(i + 1).trim();
    if (key === "t" && /^\d{1,12}$/.test(value)) t = Number(value);
    if (key === "v1" && /^[0-9a-f]{64}$/.test(value)) v1.push(value);
  }
  if (t === null || v1.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
  const expected = Buffer.from(
    createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex"),
    "ascii",
  );
  return v1.some((s) => timingSafeEqual(Buffer.from(s, "ascii"), expected));
}

In Express, read the body raw on the webhook route only:

import express from "express";
import { verifyIrisfileSignature } from "./verify.js";

const app = express();
app.post("/irisfile/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verifyIrisfileSignature(body, req.get("IRISfile-Signature"), process.env.IRISFILE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(body);
  // Delivery is at least once: skip an event.id you have already handled.
  res.sendStatus(204);
});

Questions

Can the API file real 1099s during the beta?
The beta runs on sandbox keys. A sandbox key files only with the IRS test environment, marked as test data, and accepts only IRS test TINs that start with 000.
Is there an OpenAPI spec for the IRISfile API?
Yes. The OpenAPI 3.1 spec is at https://irisfile.co/openapi.json and needs no key. It lists every route, every error code each route returns and the webhook payloads.
How do I know when the IRS accepts a filing?
Register a webhook endpoint and handle transmission.acknowledged; ackCode A means accepted. You can also poll GET /api/v1/submissions/{id} and read each transmission's status.
Is it safe to call the file endpoint twice?
Yes. IRISfile writes a transmission's UTID before it calls the IRS and never resubmits a transmission that has one. A repeat call looks up the receipt instead, and a form already paid for costs nothing again.

File your first test 1099 with the IRS sandbox.

Get notified