# Payerworks docs, every page > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says transmitted: false, and is yours to send on your own connection. A test key is for invented data only, never a real patient. # Payerworks docs > Every page is a job you came to do: the request to send, how to read the answer, what to do when it is refused, and the code lists. Each page is also plain markdown for you or your coding agent. The API writes files and explains answers; it sends nothing to a payer. Page: https://payerworks.com/docs · Markdown: https://payerworks.com/docs/index.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## Start here | Page | What it covers | | --- | --- | | [Building with a coding agent](https://payerworks.com/docs/coding-agents) | Point a coding agent at the Payerworks docs, the OpenAPI document and the MCP server, with prompts to paste and a check of what it builds against the sandbox. | ## Quickstarts | Page | What it covers | | --- | --- | | [A sandbox claim, to its 835](https://payerworks.com/docs/quickstart-sandbox-claim) | Send one invented claim to the Payerworks sandbox payer and read its 999, its 277CA and its 835 back in words, with a test key, in a few minutes. | | [A first 271, explained](https://payerworks.com/docs/quickstart-first-271) | Write an eligibility question as a 270, get a 271 back from the eligibility sandbox, and read the deductible, out-of-pocket and copays in words. | | [Following an event](https://payerworks.com/docs/quickstart-events) | Set a webhook, run the claims sandbox, check each event’s signature on your server, and list or send again the events Payerworks recorded for your key. | | [The enrollment sandbox](https://payerworks.com/docs/quickstart-enrollment) | Create an invented provider, request enrollment with the sandbox payer PWSANDBOX, submit it, and advance it through its statuses one step at a time. | | [A first 275](https://payerworks.com/docs/quickstart-first-275) | Announce an attachment on a claim, then write that attachment as an X12 275 under the same control number, ready for you to send on your own connection. | ## By job | Page | What it covers | | --- | --- | | [Eligibility](https://payerworks.com/docs/eligibility) | Write eligibility questions as 270s, explain any 271 per patient, find a patient again after a no-match, put plans in order, and print an answer. | | [Claim status](https://payerworks.com/docs/claim-status) | Write claim status questions as 276s, explain each 277 answer with the next step and when to ask again, and follow up on your open claims. | | [Claims](https://payerworks.com/docs/claims) | Check claims against one published edit table, write 837P, 837I and 837D files, plan corrected and void claims, and run the claims sandbox. | | [Acknowledgments](https://payerworks.com/docs/acknowledgments) | Explain every rejection in a 999 or a 277CA as the fix in your claim data, and track each claim from the 837s, 999s, 277CAs and 835s you hold. | | [Remittances and denials](https://payerworks.com/docs/remittances) | Explain an 835 claim by claim, say each denial as the next step, write appeal letters, and find duplicates, deposits, recoupments and filing windows. | | [Analytics from your own files](https://payerworks.com/docs/analytics) | Count your own denials and payments, check payments against your fee schedule, check a claim against your past denials, and rank open denials. | | [Documents and attachments](https://payerworks.com/docs/documents) | Draw claims on the CMS-1500, print remittances and eligibility answers, fill Medicare’s appeal forms, and write attachments as an X12 275. | | [Prior authorization](https://payerworks.com/docs/prior-authorization) | Write a prior authorization request as a 278, explain the payer’s response, run the authorization sandbox, and check Medicare’s published lists. | | [Patient estimates and money](https://payerworks.com/docs/patient-money) | Estimate a patient’s share from a 271, lay out a Good Faith Estimate, find a replacement window and a poverty guideline percent, and check a charges file. | | [The payer directory](https://payerworks.com/docs/payers) | Search payers and plan programs from public federal files, with a payer ID only where the payer publishes it. It is not a list Payerworks sends to. | | [Transaction enrollment](https://payerworks.com/docs/enrollment) | Create providers, request transaction enrollment with a payer, answer its tasks and upload its PDFs, import many at once, and run the enrollment sandbox. | | [Events and webhooks](https://payerworks.com/docs/events) | Receive signed events at your webhook or several destinations, check each signature, list every event for 30 days, and send one again. | | [Keys and your account](https://payerworks.com/docs/keys) | Test and live keys, your account’s rate and usage this month, named keys, rotation that keeps the key id, your notice address, and the hosted MCP server. | | [The SFTP file drop](https://payerworks.com/docs/sftp) | Make an SFTP user under your own SSH key, drop 837P claim files in one folder, and read the answers Payerworks writes to another, where the drop is open. | | [Batch jobs](https://payerworks.com/docs/jobs) | Run up to 1,000 calls to one route from one uploaded file, or write one 270 of up to 7,500 questions, and fetch the result for 24 hours. | | [Organizations and members](https://payerworks.com/docs/organizations) | Make an organization for your account, invite people with a role, sign in with an authenticator app, and manage keys as a member while keys call the API. | ## Reference | Page | What it covers | | --- | --- | | [Every error](https://payerworks.com/docs/errors) | Every refusal code the Payerworks API answers, with its HTTP status and what it means, how a refusal is shaped, and which ones a client should retry. | | [Compatibility and deprecation](https://payerworks.com/docs/compatibility) | What may change under /v1 without notice, what counts as a breaking change, how much notice a removal gets, and how a deprecation is marked. | | [Changelog](https://payerworks.com/docs/changelog) | Every dated change to the Payerworks API: what shipped, how it was before and how it is now, the sandbox support, and what it means for an integration. | ## For agents and tools - Every page as markdown: add .md to its address, as [/docs/eligibility.md](https://payerworks.com/docs/eligibility.md). Each page also has a Copy for agent button. - [/docs/llms.txt](https://payerworks.com/docs/llms.txt): the docs’ index, and [/llms-full.txt](https://payerworks.com/llms-full.txt): every page whole, in one file. - [/openapi.json](https://payerworks.com/openapi.json): the whole contract, with a real example for every route. - [/sdk/payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs): JavaScript. An ES module for Node 18 or later, on your server: one method per route, typed in JSDoc, and uploadDocument for an enrollment task’s PDF. Your key is a bearer key, so never put it in browser code. - [/sdk/payerworks.py](https://payerworks.com/sdk/payerworks.py): Python. For Python 3.9 or later, the standard library only: one method per route. - [/sdk/payerworks-mcp.mjs](https://payerworks.com/sdk/payerworks-mcp.mjs): MCP server. For an AI agent: run it with node over stdio, and each route that answers a question is a tool, with the reads of your account and your enrollment requests, except the routes that answer with a PDF, the two that read a picture with a model, and the enrollment export. Setting, removing or testing your webhook, sending an event again, and every change to a provider, an enrollment request, a task, a document, a key or a destination stay with you. One tool writes to your account: a sandbox claims run records its events and posts them to your webhook, when one is set. - [/docs/changelog.atom](https://payerworks.com/docs/changelog.atom): the changelog as an Atom feed. # Building with a coding agent > What to hand an agent, the prompts that work, and how to check its code against the sandbox payers before a person reads it. Page: https://payerworks.com/docs/coding-agents · Markdown: https://payerworks.com/docs/coding-agents.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. A coding agent builds against Payerworks well when it reads what the API answers rather than guessing. Every page here is also markdown, the whole set is one file, and the contract is machine-readable, so you can hand an agent everything it needs in a few lines. ## What to hand it - [/llms-full.txt](https://payerworks.com/llms-full.txt): every docs page as markdown, in one file. [/docs/llms.txt](https://payerworks.com/docs/llms.txt) is its index. - [/openapi.json](https://payerworks.com/openapi.json): every route with its request and answer schemas and real examples. - [/sdk/payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [/sdk/payerworks.py](https://payerworks.com/sdk/payerworks.py): the clients, one method per route. - The MCP server, to let the agent call the API itself while it works: run [/sdk/payerworks-mcp.mjs](https://payerworks.com/sdk/payerworks-mcp.mjs) over stdio, or use the hosted one at POST /v1/mcp with your key. - The [examples](https://payerworks.com/docs/examples/sample-data.mjs), one per page, each run against the API in Payerworks’ own tests. Add the MCP server to your AI client’s configuration: ```json { "mcpServers": { "payerworks": { "command": "node", "args": ["/absolute/path/to/payerworks-mcp.mjs"], "env": { "PAYERWORKS_API_KEY": "pw_test_...", "PAYERWORKS_BASE_URL": "" } } } } ``` The same tools are also served by the API itself: POST /v1/mcp, over MCP’s Streamable HTTP transport (revision 2026-07-28, and the 2025 handshakes), with your key as the bearer and nothing to download. Each tool call is a call to its route under your key, metered and rate-limited as that route. With a live key no tool is called unless the request carries Payerworks-MCP-Allow-Live-Key: 1. With a test key, send invented data only. With a live key, the AI model and its provider read the patient data each tool sends and receives, so the server refuses a live key unless you set PAYERWORKS_MCP_ALLOW_LIVE_KEY=1. Set it only where that is permitted, for example under a business associate agreement that covers your AI client. ## The rules to give it - Use a test key from an environment variable, and invented patients only. Never paste a real patient into an agent’s session. - Keep the key on a server: it is a bearer key, never browser code. - Payerworks sends nothing. A file the API writes is for your system to send on your own connection. - A refusal is a PayerworksError with a status, a code and a request id: handle it by its code, and retry only what the clients retry (429, 503 and 409 idempotency_in_flight). ## Prompts ### Start the integration ```text Read https://payerworks.com/llms-full.txt and https://payerworks.com/openapi.json. Using the JavaScript client from https://payerworks.com/sdk/payerworks.mjs, write a module that checks a claim with POST /v1/claims/validate and, only when it is valid, writes it with POST /v1/claims/837p. Read the key from PAYERWORKS_API_KEY and the base URL from PAYERWORKS_BASE_URL. Handle a PayerworksError by its code. Use invented test data only. ``` ### Test it against the sandbox ```text Write tests for that module against the Payerworks claims sandbox, POST /v1/sandbox/claims, with one claim per member ID ending the claims docs page lists (a paid claim, REJ, DENY and PEND). Assert on the explained 999, 277CA and 835 the sandbox answers, never on figures you typed. ``` ### Receive events ```text Add a webhook receiver for Payerworks events. Check the Standard Webhooks signature over the raw request body with verifyStandardWebhook from the client before parsing it, skip a webhook-id already handled, and answer 2xx quickly. The events and their fields are on https://payerworks.com/docs/events.md. ``` ### Explain eligibility ```text Using https://payerworks.com/docs/eligibility.md, write a function that turns a 271 into the patient’s deductible and out-of-pocket remaining, in network, for service type 30, by calling POST /v1/eligibility/explain. A figure the payer did not state stays null: never print it as zero. ``` ## Checking what it built - Run what it wrote against the sandbox payers with a test key, and compare it with the matching example file. - A signature checked after the body was parsed and written again fails: the check reads the raw bytes. - A claim status of not_found is not a rejection, and a remittance’s verdict is not CLP02 alone. An agent that maps codes itself instead of reading the answer gets both wrong. - Nothing it builds should keep a patient’s data in an enrollment request or a provider: those are for a practice’s business facts. # A sandbox claim, to its 835 > One invented claim, checked, written and answered by the sandbox payer as a 999, a 277CA and an 835, each explained. Page: https://payerworks.com/docs/quickstart-sandbox-claim · Markdown: https://payerworks.com/docs/quickstart-sandbox-claim.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## What you need - A test key, from [the developers page](https://payerworks.com/developers). It is for invented data only. - Node 18 or later, the JavaScript client [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs), and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs), the invented patient and clinic every example uses, in one folder. - Two environment variables: `PAYERWORKS_API_KEY` (your key) and `PAYERWORKS_BASE_URL` (the base URL shown with your key). ## The steps 1. Check the claim with POST /v1/claims/validate. A claim that validates is well formed and valid on its date; it is not yet paid. 2. Send it to the sandbox payer with POST /v1/sandbox/claims. One call answers what a payer and its front end would over days: a 999 for the file, a 277CA for each claim, and an 835 for every claim it adjudicated, each already explained. 3. Read the explained answers: `explained.acknowledgment_999`, `explained.claim_acknowledgment_277ca` and `explained.remittance`. Each file is also in `files`, and reads the same when you explain it again later. 4. Change how the member ID ends to see another answer: REJ is rejected at the 277CA, DENY is denied on the 835, PEND waits with no 835. The whole list is on [the claims page](https://payerworks.com/docs/claims#code-lists). ## The code [sandbox-claim.mjs](https://payerworks.com/docs/examples/sandbox-claim.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. sandbox-claim.mjs ```js // Payerworks example: a sandbox claim, to its 835. Docs: https://payerworks.com/docs/quickstart-sandbox-claim // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node sandbox-claim.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Check the claim first: a claim that validates is well formed, not yet paid. const claim = claimEndingIn(''); const checked = await pw.postClaimsValidate({ claim }); // 2. The sandbox payer answers it in one call: a 999 for the file, a 277CA for the claim, an 835 for the money. const run1 = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claim] }); const { acknowledgment_999: file, claim_acknowledgment_277ca: ack, remittance: paid } = run1.explained; // 3. Each file is also yours to read again later: the 835 explained from the file alone. const again = await pw.postRemittancesExplain({ x12: run1.files.remittance_835 }); // 4. The failure path: the same reference twice in one file is refused, because answers match back on it. const refused = await refusalOf(() => pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claim, claim] }), { PayerworksError }); return { valid: checked.valid, file: file.groups[0].verdict, claim: ack.claims[0].verdict, paid: paid.claims[0].paid, same_reading: again.claims[0].paid === paid.claims[0].paid, transmitted: run1.transmitted, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "valid": true, "file": "A", "claim": "A", "paid": 103.2, "same_reading": true, "transmitted": false, "refused": { "status": 422, "code": "duplicate_reference", "request_id": "req_4" } } ``` ## What you should see - `file` and `claim` are A, accepted; `paid` is what the 835 paid; `refused` is the failure path, the same reference sent twice. ## Next - Set a webhook and the same run posts each answer to you as an event: [Following an event](https://payerworks.com/docs/quickstart-events). - Track the claim from the files you hold with POST /v1/claims/track: [Acknowledgments](https://payerworks.com/docs/acknowledgments). # A first 271, explained > An eligibility question written as a 270, answered by the eligibility sandbox as a 271, and that 271 said per patient in words. Page: https://payerworks.com/docs/quickstart-first-271 · Markdown: https://payerworks.com/docs/quickstart-first-271.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## What you need - A test key, from [the developers page](https://payerworks.com/developers). It is for invented data only. - Node 18 or later, the JavaScript client [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs), and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs), the invented patient and clinic every example uses, in one folder. - Two environment variables: `PAYERWORKS_API_KEY` (your key) and `PAYERWORKS_BASE_URL` (the base URL shown with your key). ## The steps 1. Write the question as a 270 with POST /v1/eligibility/270: the payer, the provider, the subscriber, the day and the service types, as the JSON most clearinghouse APIs take. The 270 is yours to send on your own connection. 2. Send the same question to the eligibility sandbox (POST /v1/sandbox/eligibility). It answers as a payer would, with a 271 in `files.eligibility_271`. 3. Explain that 271 with POST /v1/eligibility/explain, naming what you asked about in `asked`. Read `members[0].coverage.state`, then each service’s deductible and out-of-pocket figures, in and out of network. 4. A member ID ending in NOTFOUND is not found, BADDOB has a birth date that does not match: each refusal is said as its reason and the field to fix. ## The code [eligibility.mjs](https://payerworks.com/docs/examples/eligibility.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. eligibility.mjs ```js // Payerworks example: eligibility, a 270 written and a 271 explained. Docs: https://payerworks.com/docs/eligibility // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node eligibility.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { ELIGIBILITY_QUESTION, SENDER_ID, PAYER_ID, copy, endingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Write the question as a 270. The file is yours to send on your own connection. const written = await pw.postEligibility270({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 12, request: copy(ELIGIBILITY_QUESTION) }); // 2. The eligibility sandbox answers the same question as a payer would: a 270 and its 271. const sandbox = await pw.postSandboxEligibility({ request: copy(ELIGIBILITY_QUESTION) }); const the271 = sandbox.files.eligibility_271; // 3. Explain the 271 per patient, for the services and the day you asked about. const explained = await pw.postEligibilityExplain({ x12: the271, asked: { service_type_codes: ['30'], date_of_service: '2026-10-20' } }); const member = explained.members[0]; // 4. A member ID ending in NOTFOUND: the payer cannot find the subscriber, and the answer says why in words. const notFound = await pw.postSandboxEligibility({ request: endingIn(ELIGIBILITY_QUESTION, 'NOTFOUND') }); // 5. The failure path: an NPI that fails its check digit is refused before anything is written. const refused = await refusalOf(() => pw.postEligibility270({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 13, request: { ...copy(ELIGIBILITY_QUESTION), provider: { organizationName: 'SAMPLE FAMILY MEDICINE', npi: '1234567890' } }, }), { PayerworksError }); return { written: { transmitted: written.transmitted, starts: written.x12.slice(0, 3) }, coverage: member.coverage.state, not_found: notFound.outcomes[0].outcome, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "written": { "transmitted": false, "starts": "ISA" }, "coverage": "active", "not_found": "subscriber_not_found", "refused": { "status": 422, "code": "invalid_requests", "request_id": "req_5" } } ``` ## What you should see - `coverage` is active; `not_found` is the sandbox’s outcome for NOTFOUND; `refused` is a 270 with an NPI that fails its check digit, refused before anything is written. ## Next - Use a real 271 the same way: explain accepts any 271, as X12 or as JSON. [Eligibility](https://payerworks.com/docs/eligibility) has every field. # Following an event > A webhook set, a sandbox run that makes events, each signature checked on your own server, and the event log read back. Page: https://payerworks.com/docs/quickstart-events · Markdown: https://payerworks.com/docs/quickstart-events.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## What you need - A test key, from [the developers page](https://payerworks.com/developers). It is for invented data only. - Node 18 or later, the JavaScript client [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs), and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs), the invented patient and clinic every example uses, in one folder. - Two environment variables: `PAYERWORKS_API_KEY` (your key) and `PAYERWORKS_BASE_URL` (the base URL shown with your key). ## The steps 1. Set your webhook with PUT /v1/account/webhook: an https address at a public hostname. The answer shows its signing secret once; store it. 2. Run the claims sandbox. Each answer is recorded as an event on your account and posted to your webhook: the 999, each claim’s 277CA, and each check’s 835. 3. On your server, check each delivery’s signature over the raw body before you parse it (`readDelivery` in the example), and skip a webhook-id you have already handled: a delivery may arrive more than once. 4. List what was recorded with GET /v1/events, read one with GET /v1/events/{id}, and send one again with POST /v1/events/{id}/redeliver. Events are kept for 30 days. ## The code [events.mjs](https://payerworks.com/docs/examples/events.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. events.mjs ```js // Payerworks example: events, signed, listed and sent again. Docs: https://payerworks.com/docs/events // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node events.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError, verifyStandardWebhook } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** * On YOUR server, for each delivery: check the signature over the raw body, before you parse it. * `secret` is the whsec_ secret PUT /v1/account/webhook answered once; `headers` the request's headers. * A delivery may arrive more than once: skip a webhook-id you have handled. * @param {string} secret @param {Record} headers @param {string} rawBody */ export function readDelivery(secret, headers, rawBody, seen = new Set()) { if (!verifyStandardWebhook(secret, headers, rawBody)) return { ok: false, reason: 'signature' }; const id = headers['webhook-id']; if (seen.has(id)) return { ok: true, duplicate: true }; seen.add(id); return { ok: true, event: JSON.parse(rawBody) }; } /** @param {Payerworks} pw */ export async function run(pw) { // 1. Your webhook: an https address of yours. The signing secret is shown once: store it. const hook = await pw.putAccountWebhook({ url: 'https://hooks.example.com/payerworks' }); // 2. A sandbox run makes events: the 999, each claim's 277CA, and each check's 835. const sandbox = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claimEndingIn('')] }); // 3. Every event is kept for 30 days: list them, read one, and send one again. const listed = await pw.getEvents({ query: { limit: 10 } }); const one = await pw.getEventsById(listed.data[0].id); const again = await pw.postEventsByIdRedeliver(one.id); // 4. The failure path: an id this key never had. const refused = await refusalOf(() => pw.getEventsById(`evt_${'0'.repeat(24)}`), { PayerworksError }); return { secret_starts: hook.secret.slice(0, 6), made: sandbox.events.map((e) => e.type), listed: listed.data.length, redelivered: Boolean(again), refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "secret_starts": "whsec_", "made": [ "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "listed": 3, "redelivered": true, "refused": { "status": 404, "code": "event_not_found", "request_id": "req_6" } } ``` ## What you should see - `made` lists the three event types one paid claim makes; `refused` is an event id the key never had. ## Next - Add more destinations, each with its own event types: [Events and webhooks](https://payerworks.com/docs/events). # The enrollment sandbox > A provider, an enrollment request with the sandbox payer, submitted and moved through its statuses, with the events each move makes. Page: https://payerworks.com/docs/quickstart-enrollment · Markdown: https://payerworks.com/docs/quickstart-enrollment.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## What you need - A test key, from [the developers page](https://payerworks.com/developers). It is for invented data only. - Node 18 or later, the JavaScript client [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs), and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs), the invented patient and clinic every example uses, in one folder. - Two environment variables: `PAYERWORKS_API_KEY` (your key) and `PAYERWORKS_BASE_URL` (the base URL shown with your key). ## The steps 1. Create the provider once with POST /v1/providers: its name, NPI, tax id and a contact. Enrollment is the one part of the API that keeps what you send; it is for a practice’s business facts, never a patient’s. 2. Request enrollment with the sandbox payer: POST /v1/enrollments with `payer.id` PWSANDBOX, a transaction, and `submit: true` to hand it to Payerworks at once. 3. The last word of the provider’s name picks the sandbox payer’s scenario. SLOW waits with the payer until you advance it with POST /v1/sandbox/enrollments/{id}/advance; TASKS raises tasks for you to answer; REJECT is refused with the payer’s words. 4. Every move is an event, and GET /v1/enrollments/{id} answers where the request is and whose turn it is. ## The code [enrollment.mjs](https://payerworks.com/docs/examples/enrollment.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. enrollment.mjs ```js // Payerworks example: transaction enrollment with the sandbox payer. Docs: https://payerworks.com/docs/enrollment // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node enrollment.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { PROVIDER, ENROLLMENT_CONTACT, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The provider, once. Enrollment keeps what you send (a practice's business facts, never a patient's). // The sandbox payer reads the last word of the provider's name: SLOW waits with the payer until you advance it. const provider = await pw.postProviders({ ...copy(PROVIDER), name: `${PROVIDER.name} SLOW` }); // 2. Ask the sandbox payer PWSANDBOX for its remittances, and hand the request to Payerworks at once. const requested = await pw.postEnrollments({ provider_id: provider.id, payer: { id: 'PWSANDBOX' }, transaction: 'remittances', primary_contact: copy(ENROLLMENT_CONTACT), notify_email: 'dev@sampleclinic.example', submit: true, }); // 3. The sandbox payer's next move: with the payer, then live. Each move is an event too. const advanced = await pw.postSandboxEnrollmentsByIdAdvance(requested.id); // 4. The failure path: a live request has nothing left for the sandbox payer to do. const refused = await refusalOf(() => pw.postSandboxEnrollmentsByIdAdvance(requested.id), { PayerworksError }); return { submitted: requested.status, moved: advanced.moved, now: advanced.enrollment.status, events: advanced.events.map((e) => e.type), refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "submitted": "with_payer", "moved": { "from": "with_payer", "to": "live" }, "now": "live", "events": [ "enrollment.live" ], "refused": { "status": 409, "code": "sandbox_nothing_to_advance", "request_id": "req_4" } } ``` ## What you should see - `submitted` is with_payer; the advance moves it to live and makes one event; `refused` is a second advance, with nothing left for the sandbox payer to do. ## Next - A real payer’s request waits with Payerworks today: Payerworks is registered with no payer yet. [Transaction enrollment](https://payerworks.com/docs/enrollment) has every status. # A first 275 > A claim that announces its attachment, and the 275 that carries it under the same control number. Page: https://payerworks.com/docs/quickstart-first-275 · Markdown: https://payerworks.com/docs/quickstart-first-275.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. ## What you need - A test key, from [the developers page](https://payerworks.com/developers). It is for invented data only. - Node 18 or later, the JavaScript client [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs), and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs), the invented patient and clinic every example uses, in one folder. - Two environment variables: `PAYERWORKS_API_KEY` (your key) and `PAYERWORKS_BASE_URL` (the base URL shown with your key). ## The steps 1. Announce the attachment on the claim: a report type, the transmission code EL (sent electronically) and a control number, in `claimInformation.claimSupplementalInformation.reportInformations`. 2. Check the claim with POST /v1/claims/validate. It is valid, and it carries the warning `pwk_el_needs_attachment`: the payer will wait for a 275 under that control number. 3. Write the attachment with POST /v1/attachments/275 under the same control number: each file base64 in its own segment, with its LOINC document type when you give one. The payer matches the two by that number. 4. Read the answer’s `shape`: the 275 is written as public payer companion guides show it, not from the licensed implementation guide. Check your payer’s own guide before you send it. ## The code [attachment-275.mjs](https://payerworks.com/docs/examples/attachment-275.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. attachment-275.mjs ```js // Payerworks example: a claim attachment as an X12 275. Docs: https://payerworks.com/docs/quickstart-first-275 // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node attachment-275.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { CLAIM, SENDER_ID, PAYER_ID, copy, refusalOf } from './sample-data.mjs'; /** The control number the claim announces and the 275 carries: the payer matches the two by it. */ const ATTACHMENT = 'EXATT0001'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The claim announces its attachment: report type 09 (a progress report), sent electronically (EL), under ATTACHMENT. const claim = copy(CLAIM); claim.claimInformation.claimSupplementalInformation = { reportInformations: [{ attachmentReportTypeCode: '09', attachmentTransmissionCode: 'EL', attachmentControlNumber: ATTACHMENT }] }; const checked = await pw.postClaimsValidate({ claim }); // 2. The attachment itself, as a 275 under the same control number. An invented one-page PDF here. const file = Buffer.from('%PDF-1.4\n% an invented progress note\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n').toString('base64'); const written = await pw.postAttachments275({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 7, claim, attachments: [{ control_number: ATTACHMENT, content_type: 'application/pdf', content_base64: file, filename: 'progress note.pdf', loinc: '11506-3' }], }); // 3. The failure path: a control number the claim does not announce is refused, so the payer is never sent an attachment it cannot match. const refused = await refusalOf(() => pw.postAttachments275({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 8, claim, attachments: [{ control_number: 'EXATT0002', content_type: 'application/pdf', content_base64: file, filename: 'other.pdf' }], }), { PayerworksError }); return { claim_valid: checked.valid, warnings: checked.claims[0].warnings.map((w) => w.code), written: { transmitted: written.transmitted, has_trn: written.x12.includes(`TRN*1*${ATTACHMENT}`), shape_verified: written.shape?.verified }, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "claim_valid": true, "warnings": [ "pwk_el_needs_attachment" ], "written": { "transmitted": false, "has_trn": true, "shape_verified": false }, "refused": { "status": 422, "code": "invalid_attachments", "request_id": "req_3" } } ``` ## What you should see - `warnings` is the reminder that a 275 must follow; `has_trn` says the 275 carries the claim’s control number; `refused` is a control number the claim never announced. ## Next - The other documents (the CMS-1500, the remittance and eligibility PDFs, Medicare’s appeal forms) are on [Documents and attachments](https://payerworks.com/docs/documents). # Eligibility > Ask whether a patient is covered and what they owe, from the JSON you already hold, and read the payer’s 271 back in words. Page: https://payerworks.com/docs/eligibility · Markdown: https://payerworks.com/docs/eligibility.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Eligibility is two halves. You write the question: POST /v1/eligibility/270 turns the JSON your system holds (the payer, the provider, the subscriber, the day and the service types) into a 270 file. You send that file on your own connection. Then you hand Payerworks the 271 that came back, as X12 or as JSON, and POST /v1/eligibility/explain says it per patient: active or not on the day you asked about, and the deductible, out-of-pocket limit, coinsurance and copay in and out of network. The eligibility sandbox (POST /v1/sandbox/eligibility) answers a 270 the way a payer would, so you can build the whole loop before you have a connection. The member ID’s ending picks the answer; the list is under Code lists below. New here? Start with [A first 271, explained](https://payerworks.com/docs/quickstart-first-271). ## Routes - [POST /v1/eligibility/270](#post-v1-eligibility-270): Write a 270 - [POST /v1/eligibility/explain](#post-v1-eligibility-explain): Explain an eligibility answer - [POST /v1/sandbox/eligibility](#post-v1-sandbox-eligibility): Run the eligibility sandbox - [GET /v1/codes/service-type/{code}](#get-v1-codes-service-type-code): A service type code - [GET /v1/codes/aaa/{code}](#get-v1-codes-aaa-code): A reject reason code - [POST /v1/eligibility/identity-retry](#post-v1-eligibility-identity-retry): Find the patient again - [POST /v1/eligibility/service-type-test](#post-v1-eligibility-service-type-test): Does a payer answer a service type - [POST /v1/eligibility/coordination](#post-v1-eligibility-coordination): Which plan pays first - [POST /v1/eligibility/discovery/verify](#post-v1-eligibility-discovery-verify): Is this coverage the patient’s Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/eligibility/270 **Write a 270.** Up to one hundred eligibility questions written as one 005010X279A1 interchange, from the JSON shape most clearinghouse APIs take: the payer, the provider, the subscriber and at most one dependent, the day and the service types. Ask about any day, a later one included; with no day named, the payer answers for its own current day. A question may also ask about procedures (CPT, HCPCS, CDT or NDC codes with up to four modifiers), point each to the patient’s diagnoses, and carry the place of service, a prior authorization or referral number, the provider’s taxonomy, a payer PIN and a birth order for twins; a question that asks only about procedures asks no service type beside them. Each question is checked field by field first, and nothing is written if any would be refused. You are told when a member ID begins with the card issuer number (80840), a name holds a title, a degree or a suffix, a backtick was written as an apostrophe, or the day is more than a year back. A question that uses none of these fields is written exactly as before. A test key writes test files only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/270" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibility270(body); ``` With the Python client ```python answer = pw.post_eligibility_270(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 12, "request": { "tradingPartnerServiceId": "99999", "tradingPartnerName": "SAMPLE HEALTH PLAN", "provider": { "organizationName": "SAMPLE FAMILY MEDICINE", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "19800214", "gender": "F" }, "encounter": { "dateOfService": "20261020", "serviceTypeCodes": [ "30" ] }, "controlNumber": "ELIG-0001" } } ``` The answer (200) ```json { "version": "005010X279A1", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000012*0*T*:~GS*HS*SAMPLEBILLER*99999*20261019*1500*12*X*005010X279A1~ST*270*0001*005010X279A1~BHT*0022*13*ELIG-0001*20261019*1500~HL*1**20*1~NM1*P…", "interchange_control_number": "000000012", "group_control_number": "12", "transaction_set_control_numbers": [ "0001" ], "segments": 17, "requests": [ { "index": 0, "trace": "ELIG-0001", "transaction_set_control_number": "0001", "warnings": [] } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | The implementation guide written. | | `usage` | one of "T", "P" | T or P. | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | | `requests` | list of object | | ### POST /v1/eligibility/explain **Explain an eligibility answer.** Any 271, as X12 or as the JSON most clearinghouse APIs return, said per patient: whether the coverage is active on the day you asked about, the plan, and for each service type the deductible and out-of-pocket limit with what is met and what remains, in and out of network, individual and family, the coinsurance, the copay, and what is not covered or needs prior authorization at each tier. A refusal comes back as every reason in words, with the field to fix and where to check it, or when to ask again. The JSON may be either shape most clearinghouse APIs return, a flat list of benefits or benefits grouped by plan with each kind in its own list, and one answer explains the same in any of the three forms; what the grouped shape holds that is not read is named. Add ?include=answer for the 271 read to JSON as well, and &shape=plans to have it in the grouped shape. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityExplain(body); ``` With the Python client ```python answer = pw.post_eligibility_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000091*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*91*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0001*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0001*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nREF*6P*G7700*SAMPLE EMPLOYER GROUP~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*G*IND*30***23*6000*****Y~\nEB*G*IND*30***29*4100*****Y~\nEB*C*FAM*30***23*3000*****Y~\nEB*C*FAM*30***29*1800*****Y~\nEB*A*IND*30*****.2****Y~\nEB*C*IND*30***23*3000*****N~\nEB*C*IND*30***29*3000*****N~\nEB*A*IND*30*****.4****N~\nEB*B*IND*98****25*****Y~\nEB*A*IND*98*****.4****N~\nEB*1*IND*62********Y~\nSE*27*0001~\nGE*1*91~\nIEA*1*000000091~", "asked": { "service_type_codes": [ "30", "98", "62" ], "date_of_service": "2026-10-20" } } ``` The answer (200) ```json { "kind": "271", "source": "x12", "envelope": { "sender_qualifier": "ZZ", "sender": "SAMPLEPAYER", "receiver_qualifier": "ZZ", "receiver": "SAMPLEBILLER", "date": "261018", "time": "0900", "control_number": "000000091", "usage": "T", "functional_group": "HB", "version": "005010X279A1", "transaction_sets": { "271": 1 }, "segments": 30 }, "asked": { "service_type_codes": [ "30", "98" ], "date_of_service": "20261020" }, "members": [ { "member": "subscriber", "who": { "first_name": "SAMPLE", "last_name": "PATIENT", "member_id": "W123456789", "date_of_birth": "1980-02-14", "relationship": "18" }, "payer": { "name": "SAMPLE HEALTH PLAN", "id": "99999" }, "coverage": { "state": "active", "words": "The payer says the coverage is active.", "active_on_date": "yes", "by_insurance_type": null }, "plan": { "name": "SAMPLE PPO", "group_number": "G7700", "group_name": "SAMPLE EMPLOYER GROUP", "plan_number": "", "begins": "2026-01-01", "ends": "", "eligibility_begins": "", "eligibility_ends": "", "insurance_type": { "code": "PR", "words": "preferred provider organization (PPO)" } }, "services": [ { "service_type": "98", "words": "physician visit, office", "known": true, "asked": true, "answered": true, "coverage": "not_stated", "in_network": { "individual": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": null, "copay": 25, "not_covered": null, "prior_auth_required": null }, "out_of_network": { "individual": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": 40, "copay": null, "not_covered": null, "prior_auth_required": null }, "network_not_stated": null, "prior_auth_required": null, "not_covered": null, "limitations": [], "messages": [] }, { "service_type": "62", "words": "MRI or CAT scan", "known": true, "asked": true, "answered": true, "coverage": "active", "in_network": null, "out_of_network": null, "network_not_stated": { "individual": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": null, "copay": null, "not_covered": null, "prior_auth_required": true }, "prior_auth_required": true, "not_covered": null, "limitations": [], "messages": [] } ], "plan_level": { "in_network": { "individual": { "deductible": { "total": 1500, "remaining": 600, "met": 900, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": 6000, "remaining": 4100, "met": 1900, "period": "calendar year", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": 3000, "remaining": 1800, "met": 1200, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": 20, "copay": null, "not_covered": null, "prior_auth_required": null }, "out_of_network": { "individual": { "deductible": { "total": 3000, "remaining": 3000, "met": 0, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": null, "remaining": null, "met": null, "period": "", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": 40, "copay": null, "not_covered": null, "prior_auth_required": null }, "network_not_stated": null, "figures_source": { "deductible": "plan_lines", "out_of_pocket": "plan_lines" }, "stated_on": { "deductible": [], "out_of_pocket": [] }, "prior_auth_required": null, "not_covered": null, "limitations": [], "messages": [] }, "procedures": [], "other_payers": [], "refusal": null, "notes": [ "A remaining amount is what was left on the day the payer answered.", "In network and out of network are the plan’s benefit tiers as the payer stated them (EB12): what the plan pays at each tier. A 271 does not say whether this provider participates with the plan." ] } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "271" | | | `source` | one of "x12", "json" | | | `envelope` | Envelope or null | | | `asked` | object | | | `members` | list of EligibilityMember | | | `answer` | list of values | Only with ?include=answer: the 271 read to JSON, in the shape answer_shape names. | | `answer_shape` | one of "plans" | With &shape=plans: the answer is written grouped by plan, each kind of benefit in its own list. | ### POST /v1/sandbox/eligibility **Run the eligibility sandbox.** Your eligibility questions written as a 270 and answered by the sandbox payer with a 271 in the same call, every answer explained. Any key may call it, every file is a test file, and how the member ID ends decides the answer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/eligibility" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxEligibility(body); ``` With the Python client ```python answer = pw.post_sandbox_eligibility(body) ``` request.json ```json { "request": { "tradingPartnerServiceId": "99999", "tradingPartnerName": "SAMPLE HEALTH PLAN", "provider": { "organizationName": "SAMPLE FAMILY MEDICINE", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "19800214", "gender": "F" }, "encounter": { "dateOfService": "20261020", "serviceTypeCodes": [ "30" ] }, "controlNumber": "ELIG-0001" } } ``` The answer (200) ```json { "usage": "T", "transmitted": false, "control": { "interchange_270": "000000001", "interchange_271": "000000002", "sets": [ "0001" ] }, "files": { "eligibility_270": "ISA*00* *00* *ZZ*SUBMITTER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HS*SUBMITTER*PWSANDBOX*20261019*1500*1*X*005010X279A1~ST*270*0001*005010X279A1~BHT*0022*13*ELIG-0001*20261019*1500~HL*1**20*1~NM1*P…", "eligibility_271": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SUBMITTER *261019*1500*^*00501*000000002*0*T*:~GS*HB*PWSANDBOX*SUBMITTER*20261019*1500*2*X*005010X279A1~ST*271*0001*005010X279A1~BHT*0022*11*ELIG-0001*20261019*1500~HL*1**20*1~NM1*P…" }, "outcomes": [ { "index": 0, "trace": "ELIG-0001", "outcome": "active", "trigger": "" } ], "requests": [ { "index": 0, "trace": "ELIG-0001", "warnings": [] } ], "explained": [ { "index": 0, "trace": "ELIG-0001", "member": "subscriber", "who": { "first_name": "SAMPLE", "last_name": "PATIENT", "member_id": "W123456789", "date_of_birth": "1980-02-14", "relationship": "18" }, "payer": { "name": "PAYERWORKS SANDBOX PAYER", "id": "PWSANDBOX" }, "coverage": { "state": "active", "words": "The payer says the coverage is active.", "active_on_date": "yes", "by_insurance_type": null }, "plan": { "name": "SANDBOX PPO PLAN", "group_number": "PWG44444", "group_name": "SANDBOX GROUP", "plan_number": "", "begins": "2026-01-01", "ends": "2026-12-31", "eligibility_begins": "", "eligibility_ends": "", "insurance_type": { "code": "PR", "words": "preferred provider organization (PPO)" } }, "services": [], "plan_level": { "in_network": { "individual": { "deductible": { "total": 2500, "remaining": 1750, "met": 750, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": 5000, "remaining": 4250, "met": 750, "period": "calendar year", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": 5000, "remaining": 4250, "met": 750, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": 10000, "remaining": 9250, "met": 750, "period": "calendar year", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": 20, "copay": null, "not_covered": null, "prior_auth_required": null }, "out_of_network": { "individual": { "deductible": { "total": 5000, "remaining": 5000, "met": 0, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": 10000, "remaining": 10000, "met": 0, "period": "calendar year", "begins": "", "ends": "", "status": null } }, "family": { "deductible": { "total": 10000, "remaining": 10000, "met": 0, "period": "calendar year", "begins": "", "ends": "", "status": null }, "out_of_pocket": { "total": 20000, "remaining": 20000, "met": 0, "period": "calendar year", "begins": "", "ends": "", "status": null } }, "coinsurance_percent": 40, "copay": null, "not_covered": null, "prior_auth_required": null }, "network_not_stated": null, "figures_source": { "deductible": "plan_lines", "out_of_pocket": "plan_lines" }, "stated_on": { "deductible": [], "out_of_pocket": [] }, "prior_auth_required": null, "not_covered": null, "limitations": [], "messages": [] }, "procedures": [], "other_payers": [], "refusal": null, "notes": [ "A remaining amount is what was left on the day the payer answered.", "In network and out of network are the plan’s benefit tiers as the payer stated them (EB12): what the plan pays at each tier. A 271 does not say whether this provider participates with the plan." ] } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of object | | | `requests` | list of object | | | `explained` | list of EligibilityMember | | ### GET /v1/codes/service-type/{code} **A service type code.** A service type code in words: its 005010 meaning where Payerworks has verified it, and the meaning later X12 versions gave a code they reassigned. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/service-type/98" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesServiceTypeByCode("98"); ``` With the Python client ```python answer = pw.get_codes_service_type_by_code("98") ``` The answer (200) ```json { "code": "98", "words": "physician visit, office", "known": true, "verified_in_005010": true, "later_words": "prescription drug", "label_source": "paraphrase" } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `words` | string | | | `known` | boolean | | | `verified_in_005010` | boolean | | | `later_words` | string | | | `label_source` | the value "paraphrase" | | ### GET /v1/codes/aaa/{code} **A reject reason code.** A reject reason from a 271 (its AAA segment) in words, whether asking again can help, and the request fields to correct. Add ?loop=2100D for a dependent. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/aaa/72" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesAaaByCode("72"); ``` With the Python client ```python answer = pw.get_codes_aaa_by_code("72") ``` The answer (200) ```json { "code": "72", "words": "the subscriber’s member ID is missing or not valid", "known": true, "retryable": false, "fix_field": "subscriber.memberId", "fix_fields": [ "subscriber.memberId" ], "loop": "", "label_source": "paraphrase" } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `words` | string | | | `known` | boolean | | | `retryable` | boolean | | | `fix_field` | string | | | `fix_fields` | list of string | | | `loop` | string | | | `label_source` | the value "paraphrase" | | ### POST /v1/eligibility/identity-retry **Find the patient again.** After a payer could not match the patient (reject reasons 65, 67, 71, 72, 73, 75 or 77, sent as reasons or read from the 271 itself): the next questions to try, in order, each built from your own question and the facts you give (the legal first name, a previous last name, a dependent’s own member ID), each with its changes listed and checked as a 270 is. No name is guessed and there is no nickname table; a day and month swapped is offered only as one to check first. Suggestions only: Payerworks looks nobody up, and nothing is sent. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/identity-retry" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityIdentityRetry(body); ``` With the Python client ```python answer = pw.post_eligibility_identity_retry(body) ``` request.json ```json { "request": { "tradingPartnerServiceId": "99999", "tradingPartnerName": "SAMPLE HEALTH PLAN", "provider": { "organizationName": "SAMPLE FAMILY MEDICINE", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAM", "lastName": "PATIENT-JONES", "dateOfBirth": "19800214", "gender": "F" }, "encounter": { "dateOfService": "20261020", "serviceTypeCodes": [ "30" ] }, "controlNumber": "ELIG-0001" }, "reasons": [ { "code": "73", "loop": "2100C" } ], "known": { "legal_first_name": "SAMANTHA" } } ``` The answer (200) ```json { "applies": true, "reasons": [ { "code": "73", "loop": "2100C" } ], "answers_codes": [ "65", "67" ], "person": "subscriber", "steps": [ { "step": "legal_name", "status": "ready", "why": "A nickname or short name is the most common mismatch: ask with the full legal first name, as the card or a government ID prints it.", "changes": [ { "op": "replace", "path": "subscriber.firstName", "value": "SAMANTHA" } ], "request": { "tradingPartnerServiceId": "99999", "tradingPartnerName": "SAMPLE HEALTH PLAN", "provider": { "organizationName": "SAMPLE FAMILY MEDICINE", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAMANTHA", "lastName": "PATIENT-JONES", "dateOfBirth": "19800214", "gender": "F" }, "encounter": { "dateOfService": "20261020", "serviceTypeCodes": [ "30" ] }, "controlNumber": "ELIG-0001" }, "errors": [], "warnings": [], "needs": [] }, { "step": "compound_first_part", "status": "ready", "why": "A compound last name may be held as one part only: ask with the first part.", "changes": [ { "op": "replace", "path": "subscriber.lastName", "value": "PATIENT" } ], "request": { "tradingPartnerServiceId": "99999", "tradingPartnerName": "SAMPLE HEALTH PLAN", "provider": { "organizationName": "SAMPLE FAMILY MEDICINE", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAM", "lastName": "PATIENT", "dateOfBirth": "19800214", "gender": "F" }, "encounter": { "dateOfService": "20261020", "serviceTypeCodes": [ "30" ] }, "controlNumber": "ELIG-0001" }, "errors": [], "warnings": [], "needs": [] } ], "notes": [], "transmitted": false, "scope": "Suggestions only, each built from the request and the facts you sent: Payerworks looks nobody up, and sends nothing." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `applies` | boolean | | | `reasons` | list of object | | | `answers_codes` | list of string | | | `person` | string | | | `steps` | list of object | | | `notes` | list of string | | | `transmitted` | the value false | | | `scope` | string | | ### POST /v1/eligibility/service-type-test **Does a payer answer a service type.** Two answers you hold from one payer, one to 30 (or 35, for dental) and one to a specific service type: whether the payer answered the specific one, likely ignored it, answered it under other service types, or refused it, with how many lines it added and removed. It reads only the two answers, as they stood when they were given: a payer may change what it answers. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/service-type-test" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityServiceTypeTest(body); ``` With the Python client ```python answer = pw.post_eligibility_service_type_test(body) ``` request.json ```json { "baseline": { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000091*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*91*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0001*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0001*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nREF*6P*G7700*SAMPLE EMPLOYER GROUP~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*G*IND*30***23*6000*****Y~\nEB*G*IND*30***29*4100*****Y~\nEB*C*FAM*30***23*3000*****Y~\nEB*C*FAM*30***29*1800*****Y~\nEB*A*IND*30*****.2****Y~\nEB*C*IND*30***23*3000*****N~\nEB*C*IND*30***29*3000*****N~\nEB*A*IND*30*****.4****N~\nEB*B*IND*98****25*****Y~\nEB*A*IND*98*****.4****N~\nEB*1*IND*62********Y~\nSE*27*0001~\nGE*1*91~\nIEA*1*000000091~" }, "specific": { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000096*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*96*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0002*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0002*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*A*IND*30*****.2****Y~\nSE*16*0001~\nGE*1*96~\nIEA*1*000000096~" }, "service_type_code": "98", "baseline_service_type": "30" } ``` The answer (200) ```json { "code": "98", "words": "physician visit, office", "baseline": "30", "verdict": "answered_differently", "said": "The answer to 98 differs from the baseline but never names 98: the payer reads the code, and answers under other service types. Read the lines it added.", "named_in_answer": false, "differs": true, "added": 0, "removed": 10, "notes": [], "scope": "Read from the two answers you sent, for this payer at the time they were given: a payer may change what it answers." } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `words` | string | | | `baseline` | string | | | `verdict` | string | | | `said` | string | | | `named_in_answer` | boolean | | | `differs` | boolean | | | `added` | integer | | | `removed` | integer | | | `notes` | list of string | | | `scope` | string | | ### POST /v1/eligibility/coordination **Which plan pays first.** The plans you list for one person, two to ten, put in the order they pay by the NAIC coordination of benefits model regulation and Medicare’s secondary payer rules, each step naming its rule and its section. Medicaid always pays last, and after Medicare a Medigap policy comes before TRICARE; injury coverage not related to the service is left out of the order. Where a rule needs a fact you did not send, no order is given and the fact is named, never guessed. It knows only the plans you send: it cannot find a person’s other coverage. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/coordination" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityCoordination(body); ``` With the Python client ```python answer = pw.post_eligibility_coordination(body) ``` request.json ```json { "person": { "medicare": { "basis": "age" } }, "date_of_service": "2026-10-20", "plans": [ { "id": "RETIREE", "kind": "employer_group", "covers_as": "self", "subscriber_status": "retired", "through": "retirement" }, { "id": "MEDICARE", "kind": "medicare" }, { "id": "SPOUSE", "kind": "employer_group", "covers_as": "dependent", "dependent_of": "spouse", "subscriber_status": "active", "through": "spouse_employment", "employer_size": 250 } ] } ``` The answer (200) ```json { "decided": true, "order": [ { "position": 1, "plan_id": "SPOUSE", "kind": "employer_group" }, { "position": 2, "plan_id": "MEDICARE", "kind": "medicare" } ], "not_ordered": [], "steps": [ { "between": [ "SPOUSE", "RETIREE" ], "first": "SPOUSE", "rule": "medicare_reversal", "source": "naic", "words": "6.D(1)(b): the person is a Medicare beneficiary, Medicare is secondary to the plan covering them as a dependent and primary to the plan covering them otherwise, so the dependent plan pays first." }, { "between": [ "SPOUSE", "MEDICARE" ], "first": "SPOUSE", "rule": "msp_current_employment", "source": "cms_msp", "words": "65 or older, covered through current employment at an employer of 20 or more employees: the group plan pays first." } ], "needs": [], "notes": [], "scope": "Ordered from the plans you listed and the facts you gave, by the NAIC model regulation and CMS’s Medicare Secondary Payer rules. Payerworks does not know a person’s other coverage: a plan you did not list is not here, and a state’s own rule…", "sources": [ { "id": "naic", "title": "NAIC Coordination of Benefits Model Regulation (MO-120), October 2013", "url": "https://content.naic.org/sites/default/files/model-law-120.pdf", "read": "10/19/2026" }, { "id": "cms_msp", "title": "CMS, Medicare Secondary Payer, MLN006903 (July 2025), Table 1", "url": "https://www.cms.gov/Outreach-and-Education/Medicare-Learning-Network-MLN/MLNProducts/Downloads/MSP_Fact_Sheet.pdf", "read": "10/19/2026" } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `decided` | boolean | | | `order` | list of object | Empty unless every step was decided. | | `partial_order` | list of object | When undecided: what could be ordered. Not an order to bill by. | | `not_ordered` | list of object | Plans no rule placed, each with why; pays_this_service false is coverage that pays nothing for this service and leaves the order decided. | | `steps` | list of object | | | `needs` | list of object | Each fact a rule needs, by plan. | | `notes` | list of string | | | `scope` | string | | | `sources` | list of object | | ### POST /v1/eligibility/discovery/verify **Is this coverage the patient’s.** Up to ten coverages an insurance discovery search found, each a 271, checked against the patient: the last name (a two-part surname allowed), the first name, the exact date of birth, the gender and the address. Each is said as a match, one to review or not this patient, and one the payer reports as pending is marked to confirm with the payer. Payerworks runs no discovery search itself. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/discovery/verify" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityDiscoveryVerify(body); ``` With the Python client ```python answer = pw.post_eligibility_discovery_verify(body) ``` request.json ```json { "patient": { "first_name": "SAMPLE", "last_name": "PATIENT", "date_of_birth": "1980-02-14", "gender": "F" }, "coverages": [ { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000091*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*91*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0001*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0001*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nREF*6P*G7700*SAMPLE EMPLOYER GROUP~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*G*IND*30***23*6000*****Y~\nEB*G*IND*30***29*4100*****Y~\nEB*C*FAM*30***23*3000*****Y~\nEB*C*FAM*30***29*1800*****Y~\nEB*A*IND*30*****.2****Y~\nEB*C*IND*30***23*3000*****N~\nEB*C*IND*30***29*3000*****N~\nEB*A*IND*30*****.4****N~\nEB*B*IND*98****25*****Y~\nEB*A*IND*98*****.4****N~\nEB*1*IND*62********Y~\nSE*27*0001~\nGE*1*91~\nIEA*1*000000091~" } ], "date_of_service": "2026-10-20" } ``` The answer (200) ```json { "results": [ { "coverage_index": 0, "patient_in_answer": 0, "verdict": "matches", "said": "The name and the date of birth match the patient’s, and no address on it differs.", "checks": [ { "check": "last_name", "result": "same" }, { "check": "first_name", "result": "same" } ], "coverage": { "state": "active", "active_on_date": "yes", "payer": { "name": "SAMPLE HEALTH PLAN", "id": "99999" }, "member_id": "W123456789", "plan": "SAMPLE PPO" }, "status_codes": [ "1" ], "confirm_with_payer": false } ], "summary": { "coverages": 1, "matched": 1, "to_review": 0, "not_this_patient": 0, "confirm_with_payer": 0 }, "notes": [ "A coverage that matches is the patient’s as far as these answers show; discovery says nothing of which plan pays first: order them with POST /v1/eligibility/coordination, or from a payer’s coordination denial." ], "scope": "Checked against the answers and the patient you sent: Payerworks finds no coverage itself and keeps nothing." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `results` | list of object | | | `summary` | object | | | `notes` | list of string | | | `scope` | string | | ## Reading the answer - Read `members[].coverage.state` first: active, inactive or not stated, for the day in `asked`. A figure the payer did not state is `null`, never 0. - Each service type is under `services[]`, with its figures split by `in_network` and `out_of_network`, then `individual` and `family`. A remaining amount is never read as a total: the payer’s own EB06 says which a figure is. - When the plan has more than one period or insurance type, the one covering the day asked about is said first and the rest are kept under `other_periods` or `by_insurance_type`, so nothing is dropped. - A refusal (an AAA segment) comes back as every reason in words with the field to fix, or when to ask again. The payer’s own follow-up code decides that advice before the reason does. ## Troubleshooting - `invalid_requests` (422): One question would be refused, so nothing was written. Each question’s errors are listed with the field; fix those and send again. - `test_key_production_file` (403): A test key writes test files only (ISA15 T). Leave usage out, or use a live key. - A member ID that begins with 80840 has the card issuer number in front of it: the answer warns, because most payers want the member ID alone. - Asking about a later day is allowed: a check before an appointment is about that day. With no day named, the payer answers for its own today. - `not_a_271` (422): POST /v1/eligibility/explain reads a 271 with its ISA envelope. A 270 sent there by mistake is refused here. - After the payer could not match the patient (reject reasons such as 72, 73 or 75), POST /v1/eligibility/identity-retry plans the next questions from what you hold. It never guesses a name. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_aaa_code` | 400 | A reject reason code (AAA03) is one or two letters or digits, such as 72 or T4. | | `invalid_asked` | 400 | asked is { service_type_codes: [codes], date_of_service: YYYYMMDD }, each optional. | | `invalid_control_number` | 400 | control_number is the interchange control number you track, a whole number from 1 to 999999999. | | `invalid_discovery_verify` | 400 | Send { patient: { first_name, last_name, date_of_birth, address?, previous_addresses?, gender? }, coverages: [answers, x12 or json], date_of_service? }. | | `invalid_include` | 400 | include names what to add to the answer: answer (the normalized read of the 271). | | `invalid_known` | 400 | known holds only text facts the caller gives: legal_first_name, previous_last_name, dependent_member_id, each at most 80 characters. | | `invalid_loop` | 400 | loop is the loop the reason was returned in, such as 2100C or 2110D. | | `invalid_reasons` | 400 | reasons is a list of { code, loop }: code an AAA reject reason (such as 75), loop where it was returned (such as 2100C), at most 20. | | `invalid_receiver_id` | 400 | receiver_id is the receiver's interchange id: 2 to 15 letters, digits or spaces. | | `invalid_sender_id` | 400 | sender_id is your submitter id at the receiver: 2 to 15 letters, digits or spaces. | | `invalid_service_type` | 400 | A service type code is one to three letters or digits, such as 30 or 98. | | `invalid_service_type_test` | 400 | Send { baseline, specific, service_type_code, baseline_service_type }: two answers (x12 or json), the specific service type code asked, and the baseline asked (30, or 35 for dental). | | `invalid_shape` | 400 | shape names how the answer that include=answer adds is written: normalized (the default) or plans (the grouped JSON shape). | | `no_answer` | 400 | Send the 271 as `x12` (the file's text) or as `json` (the JSON answer). | | `no_answer_to_read` | 400 | Send the 271 as `x12` (the file’s text) or as `json` (either JSON eligibility answer). | | `no_identity_request` | 400 | Send `request` (the eligibility request the payer refused, in the JSON POST /v1/eligibility/270 takes) and the reasons: `reasons` [{ code, loop }] or the refusing `answer` (x12 or json). | | `no_request` | 400 | The body has no eligibility request: send `requests` (a list), `request`, or the request itself in the JSON shape the docs describe. | | `too_many_coverages` | 400 | Verify at most 10 coverages a call. | | `too_many_requests` | 400 | At most 100 eligibility requests a call. | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `answer_too_large` | 413 | This answer is larger than one response can carry: send less in each call (split the file, or send fewer claims or requests). | | `file_too_large` | 413 | This file holds more than one call can read: the error names what was counted (`unit`) and the most one call reads (`limit`). Split it and send each part in its own call. | | `answer_not_read` | 422 | An answer could not be read as an eligibility answer: send an X12 271 with its envelope, or a JSON eligibility answer. | | `duplicate_trace` | 422 | Two requests in one file carry the same trace (controlNumber); each must be unique so its answer matches back. | | `invalid_coordination` | 422 | The coordination question would not be answered as sent: each problem is listed. | | `invalid_identity_request` | 422 | The request would be refused as a 270 before any retry is planned: its errors are listed. | | `invalid_requests` | 422 | One or more eligibility requests would be refused; nothing was written. Each request's errors are listed. | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | | `no_reasons` | 422 | Neither reasons nor the answer names a reject reason to plan from. | | `not_a_271` | 422 | This is not an X12 271 eligibility answer: send the 271 with its ISA envelope. | | `not_a_271_json` | 422 | This JSON is not an eligibility answer: it holds no plans, benefitsInformation, errors or subscriber. | ## Code lists Any one code in words: [GET /v1/codes/service-type/{code}](#get-v1-codes-service-type-code), [GET /v1/codes/aaa/{code}](#get-v1-codes-aaa-code). ### The eligibility sandbox’s member ID endings | Ending | Outcome | What it answers | | --- | --- | --- | | `NOTFOUND` | `subscriber_not_found` | A member ID ending in NOTFOUND is not found: the 271 refuses in the subscriber’s loop with AAA 75 and states no benefits. | | `BADDOB` | `birth_date_mismatch` | A member ID ending in BADDOB is found, but the birth date does not match: AAA 71 in the patient’s loop, and no benefits. | | `DOWN` | `payer_unavailable` | A member ID ending in DOWN reaches a payer that cannot answer: AAA 42 in the payer’s loop, follow-up R. Ask again later, unchanged. | | `INACTIVE` | `inactive` | A member ID ending in INACTIVE answers with coverage that ended last year: EB 6 on service type 30, with last year’s plan dates. | | `OON` | `out_of_network_only` | A member ID ending in OON is active with out-of-network benefits only: every figure is stated for the out-of-network tier. | | `AUTH` | `prior_auth_required` | A member ID ending in AUTH is active, and each service asked about requires prior authorization (EB11 Y). | | `MET` | `deductible_met` | A member ID ending in MET is active with the deductible met: the remaining deductible is zero. | | `OTHER` | `other_payer_primary` | A member ID ending in OTHER is active, and another payer is primary: an EB R line names it in a 2120 loop. | | `` | `active` | Any other member ID is active, with the full grid: individual and family deductible and out-of-pocket limit for the calendar year, in and out of network, each with what remains, plus coinsurance and a copay per visit on each service asked. | ## Example [eligibility.mjs](https://payerworks.com/docs/examples/eligibility.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. eligibility.mjs ```js // Payerworks example: eligibility, a 270 written and a 271 explained. Docs: https://payerworks.com/docs/eligibility // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node eligibility.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { ELIGIBILITY_QUESTION, SENDER_ID, PAYER_ID, copy, endingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Write the question as a 270. The file is yours to send on your own connection. const written = await pw.postEligibility270({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 12, request: copy(ELIGIBILITY_QUESTION) }); // 2. The eligibility sandbox answers the same question as a payer would: a 270 and its 271. const sandbox = await pw.postSandboxEligibility({ request: copy(ELIGIBILITY_QUESTION) }); const the271 = sandbox.files.eligibility_271; // 3. Explain the 271 per patient, for the services and the day you asked about. const explained = await pw.postEligibilityExplain({ x12: the271, asked: { service_type_codes: ['30'], date_of_service: '2026-10-20' } }); const member = explained.members[0]; // 4. A member ID ending in NOTFOUND: the payer cannot find the subscriber, and the answer says why in words. const notFound = await pw.postSandboxEligibility({ request: endingIn(ELIGIBILITY_QUESTION, 'NOTFOUND') }); // 5. The failure path: an NPI that fails its check digit is refused before anything is written. const refused = await refusalOf(() => pw.postEligibility270({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 13, request: { ...copy(ELIGIBILITY_QUESTION), provider: { organizationName: 'SAMPLE FAMILY MEDICINE', npi: '1234567890' } }, }), { PayerworksError }); return { written: { transmitted: written.transmitted, starts: written.x12.slice(0, 3) }, coverage: member.coverage.state, not_found: notFound.outcomes[0].outcome, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "written": { "transmitted": false, "starts": "ISA" }, "coverage": "active", "not_found": "subscriber_not_found", "refused": { "status": 422, "code": "invalid_requests", "request_id": "req_5" } } ``` # Claim status > Ask where a claim stands, read the payer’s answer as a next step, and know when to ask again. Page: https://payerworks.com/docs/claim-status · Markdown: https://payerworks.com/docs/claim-status.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Write the claim status question as a 276 from JSON (POST /v1/claim-status/276), send it on your own connection, and give Payerworks the 277 that comes back (POST /v1/claim-status/explain). Each question is answered with where the claim stands, whether money is coming and how much, the next step, and when to ask again. POST /v1/claims/follow-up works the other way round: send your open claims with the day each was sent, and your own history of when each payer paid, and each claim comes back with when its remittance is expected and whether it is late. Each payer’s days to pay are learned from your history alone, with how many paid claims they rest on, and only from five up. ## Routes - [POST /v1/claim-status/276](#post-v1-claim-status-276): Write a 276 - [POST /v1/claim-status/explain](#post-v1-claim-status-explain): Explain a claim status answer - [POST /v1/sandbox/claim-status](#post-v1-sandbox-claim-status): Run the claim status sandbox - [GET /v1/codes/claim-status/{category}](#get-v1-codes-claim-status-category): A claim status category - [GET /v1/codes/claim-status-code/{code}](#get-v1-codes-claim-status-code-code): A claim status code - [POST /v1/claims/follow-up](#post-v1-claims-follow-up): Follow up on open claims Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/claim-status/276 **Write a 276.** Up to one hundred claim status questions, from the JSON shape most clearinghouse APIs take, written as one 005010X212 interchange for you to send on your own connection. Each question is checked field by field first, and nothing is written if any would be refused. A test key writes test files only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claim-status/276" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimStatus276(body); ``` With the Python client ```python answer = pw.post_claim_status_276(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 21, "payer_name": "SAMPLE HEALTH PLAN", "inquiry": { "tradingPartnerServiceId": "99999", "providers": [ { "providerType": "BillingProvider", "npi": "1234567893", "organizationName": "SAMPLE FAMILY MEDICINE" } ], "subscriber": { "memberId": "W123456789", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "19800214" }, "encounter": { "trackingNumber": "STATUS-0001", "beginningDateOfService": "20261001", "endDateOfService": "20261001", "submittedAmount": "215.00", "patientControlNumber": "PW-2001" } } } ``` The answer (200) ```json { "version": "005010X212", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000021*0*T*:~GS*HR*SAMPLEBILLER*99999*20261019*1500*21*X*005010X212~ST*276*0001*005010X212~BHT*0010*13*STATUS-0001*20261019*1500~HL*1**20*1~NM1*PR*…", "interchange_control_number": "000000021", "group_control_number": "21", "transaction_set_control_numbers": [ "0001" ], "inquiries": [ { "index": 0, "trace": "STATUS-0001", "transaction_set_control_number": "0001", "warnings": [] } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | The implementation guide written. | | `usage` | one of "T", "P" | T or P. | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | | `inquiries` | list of object | | ### POST /v1/claim-status/explain **Explain a claim status answer.** A 277 claim status answer, as X12 or as the JSON most clearinghouse APIs return, said per question: where the claim stands, whether money is coming and how much, what to do next and when to ask again. A claim the payer cannot find yet is never called a rejection, and a question the payer refused because of its own data is never asked again unchanged. A claim the payer’s search did not find comes back with the next questions to ask, in order, each built from the question you sent (asked.request) and what you hold (asked.known), never from a value you did not give; a payer whose system did not answer, with a retry schedule for the same day. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claim-status/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimStatusExplain(body); ``` With the Python client ```python answer = pw.post_claim_status_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000092*0*T*:~\nGS*HN*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*92*X*005010X212~\nST*277*0001*005010X212~\nBHT*0010*08*STATUS-0001*20261018*0900*DG~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*41*2*SAMPLE FAMILY MEDICINE*****46*SAMPLEBILLER~\nHL*3*2*19*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*4*3*22*0~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nTRN*2*STATUS-0001~\nSTC*F1:65*20261015**215*103.2*20261015*CHK*20261016*CHK10021~\nREF*1K*PAYER-CN-21~\nREF*EJ*PW-2001~\nDTP*472*RD8*20261001-20261001~\nHL*5*3*22*0~\nNM1*IL*1*PATIENT*SECOND****MI*W987654321~\nTRN*2*STATUS-0002~\nSTC*P3:123:1P*20261015**350*0*****P3:297:1P~\nREF*1K*PAYER-CN-22~\nREF*EJ*PW-2002~\nDTP*472*RD8*20261002-20261002~\nSE*23*0001~\nGE*1*92~\nIEA*1*000000092~", "asked": { "submitted_on": "2026-10-05", "today": "2026-10-19" } } ``` The answer (200) ```json { "kind": "277", "label_source": "paraphrase", "business_days": "Business days are Monday to Friday. Holidays are not counted, so a payer closed on a holiday may need one day more.", "today": "2026-10-19", "inquiries": [ { "trace": "STATUS-0001", "claims": [ 0 ], "answered_by": 0, "state": "paid", "meaning": "The claim was finalized and paid: the claim or line has been paid (STC F1:65).", "money": { "coming": "yes", "amount": 103.2, "check_number": "CHK10021", "check_date": "2026-10-16", "adjudicated_on": "2026-10-15", "why": "The payer reports $103.20 paid on check or payment CHK10021 dated 10/16/2026. The remittance (835) carries the allowed amount, the adjustments and the patient's share; this answer does not." }, "next_step": { "action": "post_remittance", "words": "Nothing to send again. Post the remittance (835) when it arrives: it says what was allowed and what the patient owes." }, "ask_again": { "after_business_days": null, "not_before": null, "never": true, "why": "The payer has paid it: the remittance (835) is the rest of the answer, and asking again returns the same." }, "asks": [], "next_inquiries": [], "retry_schedule": null }, { "trace": "STATUS-0002", "claims": [ 1 ], "answered_by": 1, "state": "info_requested", "meaning": "The claim was pending for provider information: more information was asked of the provider (STC P3:123:1P).", "money": { "coming": "not_yet", "amount": 0, "check_number": "", "check_date": "", "adjudicated_on": "", "why": "Nothing is paid until the payer receives what it asked for." }, "next_step": { "action": "send_records", "words": "Send the payer the records it asked for: medical notes or a report. The claim does not move until they arrive." }, "ask_again": { "after_business_days": 5, "not_before": "2026-10-26", "never": false, "why": "After what was asked is sent. The usual schedule: 3, 5 and 7 business days apart, then every 10, counted from this answer." }, "asks": [ { "status_code": "297", "procedure_code": "", "words": "medical notes or a report" } ], "next_inquiries": [], "retry_schedule": null } ], "claims": [ { "index": 0, "trace": "STATUS-0001", "payer_claim_number": "PAYER-CN-21", "clearinghouse_claim_number": "", "patient_control_number": "PW-2001", "service_date": { "from": "2026-10-01", "to": "2026-10-01" }, "charge": 215, "status": { "category_code": "F1", "status_code": "65", "entity_code": "" }, "state": "paid", "meaning": "The claim was finalized and paid: the claim or line has been paid (STC F1:65).", "money": { "coming": "yes", "amount": 103.2, "check_number": "CHK10021", "check_date": "2026-10-16", "adjudicated_on": "2026-10-15", "why": "The payer reports $103.20 paid on check or payment CHK10021 dated 10/16/2026. The remittance (835) carries the allowed amount, the adjustments and the patient's share; this answer does not." }, "next_step": { "action": "post_remittance", "words": "Nothing to send again. Post the remittance (835) when it arrives: it says what was allowed and what the patient owes." }, "ask_again": { "after_business_days": null, "not_before": null, "never": true, "why": "The payer has paid it: the remittance (835) is the rest of the answer, and asking again returns the same." }, "asks": [], "next_inquiries": [], "retry_schedule": null, "lines": [], "matches_requested": null, "describes_replaced_claim": false }, { "index": 1, "trace": "STATUS-0002", "payer_claim_number": "PAYER-CN-22", "clearinghouse_claim_number": "", "patient_control_number": "PW-2002", "service_date": { "from": "2026-10-02", "to": "2026-10-02" }, "charge": 350, "status": { "category_code": "P3", "status_code": "123", "entity_code": "1P" }, "state": "info_requested", "meaning": "The claim was pending for provider information: more information was asked of the provider (STC P3:123:1P).", "money": { "coming": "not_yet", "amount": 0, "check_number": "", "check_date": "", "adjudicated_on": "", "why": "Nothing is paid until the payer receives what it asked for." }, "next_step": { "action": "send_records", "words": "Send the payer the records it asked for: medical notes or a report. The claim does not move until they arrive." }, "ask_again": { "after_business_days": 5, "not_before": "2026-10-26", "never": false, "why": "After what was asked is sent. The usual schedule: 3, 5 and 7 business days apart, then every 10, counted from this answer." }, "asks": [ { "status_code": "297", "procedure_code": "", "words": "medical notes or a report" } ], "next_inquiries": [], "retry_schedule": null, "lines": [], "matches_requested": null, "describes_replaced_claim": false } ], "notices": [], "errors": [] } ``` | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "277", "json" | | | `label_source` | the value "paraphrase" | | | `business_days` | string | | | `today` | string | | | `inquiries` | list of ClaimStatusReading | | | `claims` | list of ClaimStatusClaim | | | `notices` | list of object | | | `errors` | list of object | | ### POST /v1/sandbox/claim-status **Run the claim status sandbox.** Your claim status questions, as JSON or a 276, answered by the sandbox payer with a 277 in the same call and explained. It agrees with the claims sandbox: the same payer claim number and, for each claim, the same paid amount. How the member ID ends decides the answer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/claim-status" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxClaimStatus(body); ``` With the Python client ```python answer = pw.post_sandbox_claim_status(body) ``` request.json ```json { "inquiry": { "tradingPartnerServiceId": "99999", "providers": [ { "providerType": "BillingProvider", "npi": "1234567893", "organizationName": "SAMPLE FAMILY MEDICINE" } ], "subscriber": { "memberId": "W12345INFO", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "19800214" }, "encounter": { "trackingNumber": "STATUS-0001", "beginningDateOfService": "20261001", "endDateOfService": "20261001", "submittedAmount": "215.00", "patientControlNumber": "PW-2001" } } } ``` The answer (200) ```json { "usage": "T", "transmitted": false, "control": { "inquiry": "000000001", "answer": "000000002" }, "files": { "claim_status_inquiry_276": "ISA*00* *00* *ZZ*SUBMITTER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HR*SUBMITTER*PWSANDBOX*20261019*1500*1*X*005010X212~ST*276*0001*005010X212~BHT*0010*13*STATUS-0001*20261019*1500~HL*1**20*1~NM1*PR*…", "claim_status_277": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SUBMITTER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SUBMITTER*20261019*1500*2*X*005010X212~ST*277*0001*005010X212~BHT*0010*08*STATUS-0001*20261019*1500*DG~HL*1**20*1~NM1*…" }, "outcomes": [ { "trace": "STATUS-0001", "reference": "PW-2001", "rule": "INFO", "outcome": "info_requested", "category": "P3", "status": "123", "entity": "1P", "payer_claim_number": "PWS0120638906", "charge": 215, "paid": 0 } ], "explained": { "label_source": "paraphrase", "business_days": "Business days are Monday to Friday. Holidays are not counted, so a payer closed on a holiday may need one day more.", "today": "2026-10-19", "inquiries": [ { "trace": "STATUS-0001", "claims": [ 0 ], "answered_by": 0, "state": "info_requested", "meaning": "The claim was pending for provider information: more information was asked of the provider (STC P3:123:1P).", "money": { "coming": "not_yet", "amount": 0, "check_number": "", "check_date": "", "adjudicated_on": "", "why": "Nothing is paid until the payer receives what it asked for." }, "next_step": { "action": "send_records", "words": "Send the payer the records it asked for: medical notes or a report. The claim does not move until they arrive." }, "ask_again": { "after_business_days": 5, "not_before": "2026-10-26", "never": false, "why": "After what was asked is sent. The usual schedule: 3, 5 and 7 business days apart, then every 10, counted from this answer." }, "asks": [ { "status_code": "297", "procedure_code": "", "words": "medical notes or a report" } ], "next_inquiries": [], "retry_schedule": null } ], "claims": [ { "index": 0, "trace": "STATUS-0001", "payer_claim_number": "PWS0120638906", "clearinghouse_claim_number": "", "patient_control_number": "PW-2001", "service_date": { "from": "2026-10-01", "to": "2026-10-01" }, "charge": 215, "status": { "category_code": "P3", "status_code": "123", "entity_code": "1P" }, "state": "info_requested", "meaning": "The claim was pending for provider information: more information was asked of the provider (STC P3:123:1P).", "money": { "coming": "not_yet", "amount": 0, "check_number": "", "check_date": "", "adjudicated_on": "", "why": "Nothing is paid until the payer receives what it asked for." }, "next_step": { "action": "send_records", "words": "Send the payer the records it asked for: medical notes or a report. The claim does not move until they arrive." }, "ask_again": { "after_business_days": 5, "not_before": "2026-10-26", "never": false, "why": "After what was asked is sent. The usual schedule: 3, 5 and 7 business days apart, then every 10, counted from this answer." }, "asks": [ { "status_code": "297", "procedure_code": "", "words": "medical notes or a report" } ], "next_inquiries": [], "retry_schedule": null, "lines": [], "matches_requested": null, "describes_replaced_claim": false } ], "notices": [], "errors": [] } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of object | | | `explained` | ClaimStatusAnswer | | ### GET /v1/codes/claim-status/{category} **A claim status category.** A claim status category code in words, read both ways: on a claim acknowledgment (277CA), whether it means accepted, rejected or denied, and in a claim status answer, the state the explainer gives it. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/claim-status/P3" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesClaimStatusByCategory("P3"); ``` With the Python client ```python answer = pw.get_codes_claim_status_by_category("P3") ``` The answer (200) ```json { "category": "P3", "words": "pending for provider information", "verdict": "A", "known": true, "acknowledgment_verdict": { "verdict": "A", "words": "Not a refusal: read on a claim acknowledgment (277CA), the claim was received, accepted, pended or finalized without a denial." }, "status_answer": { "state": "info_requested", "meaning": "Pending for provider information." } } ``` | Field | Type | What it is | | --- | --- | --- | | `category` | string | | | `words` | string | | | `verdict` | string | As read on a 277CA: A, R, D or empty. | | `known` | boolean | | | `acknowledgment_verdict` | object | | | `status_answer` | object | | ### GET /v1/codes/claim-status-code/{code} **A claim status code.** A claim status code in words, and whether it names an entity. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/claim-status-code/297" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesClaimStatusCodeByCode("297"); ``` With the Python client ```python answer = pw.get_codes_claim_status_code_by_code("297") ``` The answer (200) ```json { "code": "297", "words": "medical notes or a report", "names_an_entity": false, "known": true, "label_source": "paraphrase" } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `words` | string | | | `names_an_entity` | boolean | | | `known` | boolean | | | `label_source` | the value "paraphrase" | | ### POST /v1/claims/follow-up **Follow up on open claims.** Your open claims (up to 200) with the day each was sent, and your history of when each payer paid (up to 20,000 rows), or the 837Ps, 277CAs and 835s themselves. Each payer’s days to pay are learned from your history alone, with how many paid claims they rest on, and only from five up; below that the answer says there is not enough history. Each claim comes back with when its remittance is expected, whether it is late, and the next step: ask its status now, wait until a day, the next questions after a not found, or retries for a payer whose system did not answer. Your own follow-up codes come back as you sent them, never read. Nothing is stored, nothing is sent, and no payer is asked. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/follow-up" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsFollowUp(body); ``` With the Python client ```python answer = pw.post_claims_follow_up(body) ``` request.json ```json { "claims": [ { "reference": "PW-3001", "payer_id": "99999", "sent_on": "2026-10-01", "service_date": "2026-09-29", "charge_amount": 215 }, { "reference": "PW-3002", "payer_id": "99999", "sent_on": "2026-08-31", "service_date": "2026-08-27", "charge_amount": 150, "last_status": { "state": "not_found", "asked_on": "2026-10-02", "checks_made": 1 } } ], "history": [ { "payer_id": "99999", "sent_on": "2026-06-01", "remitted_on": "2026-06-19" }, { "payer_id": "99999", "sent_on": "2026-06-15", "remitted_on": "2026-07-08" }, { "payer_id": "99999", "sent_on": "2026-07-01", "remitted_on": "2026-07-17" }, { "payer_id": "99999", "sent_on": "2026-07-20", "remitted_on": "2026-08-14" }, { "payer_id": "99999", "sent_on": "2026-08-03", "remitted_on": "2026-08-21" }, { "payer_id": "99999", "sent_on": "2026-08-17", "remitted_on": "2026-09-11" } ], "today": "2026-10-19" } ``` The answer (200) ```json { "today": "2026-10-19", "business_days": "Business days are Monday to Friday. Holidays are not counted, so a payer closed on a holiday may need one day more.", "floor": 5, "method": "Days to pay are calendar days from the day a claim was sent to the day its remittance (835) came, learned per payer from the history you sent. A payer's window runs from the 25th to the 90th percentile of its own days, with the median as th…", "payers": [ { "payer_id": "99999", "history": { "n": 6, "median_days": 20.5, "p25_days": 18, "p90_days": 25, "min_days": 16, "max_days": 25, "remitted_from": "2026-06-19", "remitted_to": "2026-09-11" }, "basis": "your_history", "words": "Your history: 6 paid claims from this payer, paid in 20.5 days at the median, and in 18 to 25 days from the 25th to the 90th percentile." } ], "claims": [ { "index": 0, "reference": "PW-3001", "payer_id": "99999", "sent_on": "2026-10-01", "disposition": null, "open": true, "expected_by": { "from": "2026-10-19", "likely": "2026-10-22", "to": "2026-10-26", "basis": "your_history", "n": 6 }, "overdue": false, "days_since_sent": 18, "next_step": { "action": "wait_until", "on": "2026-10-27", "words": "Nothing to do yet: the remittance is expected by 10/26/2026, most likely around 10/22/2026. If none has come by then, ask the payer's status on 10/27/2026." }, "inquiry_window": null, "next_inquiries": [], "retry_schedule": null }, { "index": 1, "reference": "PW-3002", "payer_id": "99999", "sent_on": "2026-08-31", "disposition": null, "open": true, "expected_by": { "from": "2026-09-18", "likely": "2026-09-21", "to": "2026-09-25", "basis": "your_history", "n": 6 }, "overdue": true, "days_since_sent": 49, "next_step": { "action": "follow_not_found_plan", "on": "2026-10-19", "words": "The payer's search found no claim. A search can miss a claim the payer has: ask again with the next questions, in order (next_inquiries), before sending the claim again, since a claim the payer has, sent again, is a duplicate." }, "inquiry_window": null, "next_inquiries": [ { "step": 1, "kind": "add_charge_amount", "route": "POST /v1/claim-status/276", "builds_on": null, "status": "needs_value", "changes": [], "inquiry": null, "warnings": [], "errors": [], "needs": [ "the question that found nothing (asked.request)" ], "why": "Add the claim's total charge: some payers search by it, and a question without it can miss the claim." }, { "step": 2, "kind": "add_payer_claim_number", "route": "POST /v1/claim-status/276", "builds_on": null, "status": "needs_value", "changes": [], "inquiry": null, "warnings": [], "errors": [], "needs": [ "the question that found nothing (asked.request)", "known.payer_claim_number" ], "why": "Add the payer's own claim number, from its claim acknowledgment (the 277CA's REF*1K): a payer finds its own number first. With no 277CA from the payer, there is no number to add: the payer may never have accepted the claim." } ], "retry_schedule": null } ], "counts": { "claims": 2, "open": 2, "overdue": 1, "not_overdue": 1, "cannot_say": 0 }, "history_used": { "rows": 6, "from_files": 0 }, "reads": "Reads only what you send. Nothing is stored, nothing is sent, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `today` | string | | | `business_days` | string | | | `floor` | integer | The fewest paid claims a payer’s days to pay are read from. | | `method` | string | | | `payers` | list of object | | | `claims` | list of object | | | `counts` | object | | | `history_used` | object | | | `reads` | string | | ## Reading the answer - A claim the payer cannot find (A4 or D0) is `not_found`, never a rejection. The answer gives the next questions to ask, in order, built only from what you sent. - E1, E2 and 689 are the payer’s own system: ask again later, with the retry schedule the answer gives. E0, E3 and E4 are the question’s own error: correct it before asking again. - Only F1 says paid. F0, F3, F3F and F3N wait for the amount, and a figure the payer did not state is `null`. - The claims sandbox and the status sandbox agree: a status question about a sandbox claim answers the same payer claim number and paid amount as that claim’s 277CA and 835. ## Troubleshooting - `invalid_inquiries` (422): One question would be refused, so nothing was written. Each question’s errors are listed with the field. - `not_a_claim_status_answer` (422): The explain route reads a 277 claim status answer (005010X212). A 277CA is an acknowledgment: send it to POST /v1/acknowledgments/explain. - `ambiguous_answer` (400): Send the answer once, as `x12` or as `json`, never both. - A claim the payer did not find yet is often a claim it has not loaded: the answer says when to ask again rather than calling it lost. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `ambiguous_answer` | 400 | Send the answer once: as `x12` or as `json`, not both. | | `ambiguous_follow_up` | 400 | Send the claims once: as `claims` or as `files`, not both. | | `ambiguous_question` | 400 | Send the claim status questions once: as `inquiry` or `inquiries`, or as `x12` (a 276), not both. | | `invalid_category` | 400 | A claim status category is a letter and up to two letters or digits, such as A1 or F2. | | `invalid_claim_status_json` | 400 | `json` is the JSON claim status answer, an object with its `claims` list (or its errors). | | `invalid_follow_up_request` | 400 | A field of the request is not in the shape it takes; each problem is listed with its field. | | `invalid_payer_name` | 400 | `payer_name` is the payer's name as the 276 writes it (2100A NM103): text of at most 60 characters. | | `invalid_status_asked` | 400 | `asked` describes the question you asked; one of its fields is not in the form it takes. | | `invalid_status_code` | 400 | A claim status code is one to five digits, such as 65 or 689. | | `invalid_submitter_name` | 400 | `submitter_name` is your organization's name as the 276 writes it (2100B NM103): text of at most 60 characters. | | `no_claim_status_answer` | 400 | Send the payer's claim status answer as `x12` (a 277) or `json` (the JSON answer). | | `no_claims_to_follow` | 400 | Send the open claims as `claims` (each with its reference, payer_id and sent_on), or the files as `files` (x12_837, acknowledgments, remittances). | | `no_inquiry` | 400 | Send the claim status question as `inquiry` (or `inquiries`), in the JSON claim status shape. | | `too_many_inquiries` | 400 | At most 100 claim status questions a call. | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `answer_too_large` | 413 | This answer is larger than one response can carry: send less in each call (split the file, or send fewer claims or requests). | | `file_too_large` | 413 | This file holds more than one call can read: the error names what was counted (`unit`) and the most one call reads (`limit`). Split it and send each part in its own call. | | `too_much_to_follow` | 413 | More was sent than one call takes: the error names what was counted (`unit`) and the most one call takes (`limit`). | | `duplicate_inquiry_trace` | 422 | Two questions in one file carry the same trace (encounter.trackingNumber); each must be unique so its answer matches back. | | `invalid_inquiries` | 422 | One or more claim status questions would be refused; nothing was written. Each question's errors are listed. | | `not_a_claim_status_answer` | 422 | This is not a claim status answer Payerworks reads: send a 277 (005010X212) or the JSON answer. | | `not_a_claim_status_question` | 422 | This is not a claim status question: send a 276 (005010X212) as `x12`, or the questions as `inquiry` or `inquiries`. | ## Code lists Any one code in words: [GET /v1/codes/claim-status/{category}](#get-v1-codes-claim-status-category), [GET /v1/codes/claim-status-code/{code}](#get-v1-codes-claim-status-code-code). ### The claim status sandbox’s member ID endings A claim status question follows the claims list, so it agrees with that claim’s 277CA and 835: the same payer claim number and the same paid amount. These endings are the status sandbox’s own, the answers a status question alone can get: | Ending | Outcome | What it answers | | --- | --- | --- | | `NOFILE` | `not_found` | A member ID ending in NOFILE is not found: the payer has no claim on file for the question (A4:35). It is never a rejection. | | `SLOW` | `payer_error` | A member ID ending in SLOW gets no answer in time: the payer’s system did not respond (E2:689:PR), so there is nothing to record and the question is asked again on the next business day. | | `INFO` | `info_requested` | A member ID ending in INFO is pended for information the payer already asked the provider for (P3:123:1P), and the answer names it: medical notes or a report (P3:297:1P). | | `NOPAY` | `no_payment` | A member ID ending in NOPAY is finalized with no further payment forthcoming (F4:1). The claims sandbox pays the same claim, and the answer states what it paid: nothing more is coming. | One exception: when a claim’s payment depends on its service lines (member IDs ending in D97), the status answer says the claim is finalized and leaves what was paid to the remittance, because a status question carries no lines. ### Where a claim stands (state) `paid`, `denied`, `rejected`, `pending`, `info_requested`, `not_found`, `member_not_found`, `no_payment`, `finalized_unpaid`, `payer_error`, `request_error`, `review` ### The next step (action) `post_remittance`, `work_denial`, `correct_and_resubmit`, `wait`, `confirm_receipt`, `check_coverage`, `ask_again`, `fix_request`, `send_records`, `send_information`, `read_remittance`, `read_answer`, `ask_other_entity` ## Example [claim-status.mjs](https://payerworks.com/docs/examples/claim-status.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. claim-status.mjs ```js // Payerworks example: claim status, a 276 written and a 277 explained. Docs: https://payerworks.com/docs/claim-status // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node claim-status.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { STATUS_QUESTION, SENDER_ID, PAYER_ID, copy, endingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Write the question as a 276, to send on your own connection. const written = await pw.postClaimStatus276({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 21, payer_name: 'SAMPLE HEALTH PLAN', inquiry: copy(STATUS_QUESTION) }); // 2. The status sandbox answers it as a 277, already explained. const sandbox = await pw.postSandboxClaimStatus({ inquiry: copy(STATUS_QUESTION) }); // 3. Explain a 277 you hold: the same reading, from the file alone. const explained = await pw.postClaimStatusExplain({ x12: sandbox.files.claim_status_277 }); const claim = explained.claims[0]; // 4. A member ID ending in NOFILE: the payer has no claim on file. That is not found, never rejected. const notFound = await pw.postSandboxClaimStatus({ inquiry: endingIn(STATUS_QUESTION, 'NOFILE') }); // 5. The failure path: the answer sent twice, as X12 and as JSON, is refused. const refused = await refusalOf(() => pw.postClaimStatusExplain({ x12: sandbox.files.claim_status_277, json: { claims: [] } }), { PayerworksError }); return { written: { transmitted: written.transmitted }, state: claim.state, next_step: Boolean(claim.next_step), not_found: notFound.explained.claims[0]?.state ?? notFound.explained.inquiries[0]?.state, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "written": { "transmitted": false }, "state": "paid", "next_step": true, "not_found": "not_found", "refused": { "status": 400, "code": "ambiguous_answer", "request_id": "req_5" } } ``` # Claims > Check a claim the way a payer’s front end would, write it as X12, and run it through the sandbox payer before it goes anywhere. Page: https://payerworks.com/docs/claims · Markdown: https://payerworks.com/docs/claims.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. A claim is JSON in the 837P’s own structure: submitter, receiver, billing, subscriber, the providers, and claimInformation with its service lines. POST /v1/claims/validate checks it the way a payer’s front end would, against one published edit table (GET /v1/edits), and lists every reason it would be refused, each with a code, the field’s path and a sentence. POST /v1/claims/837p writes up to one hundred claims as one file, after the same checks; nothing is written when any claim would be refused. The 837I and 837D routes take the same JSON with the institutional or dental fields. POST /v1/claims/correction-plan says how to replace or void a claim already sent, and POST /v1/x12/parse reads any of these files back to the same JSON. Before a claim goes anywhere, run it through the sandbox payer: POST /v1/sandbox/claims answers it as a 999, a 277CA and an 835 in one call, each explained, and posts the answers to your webhook as events. POST /v1/claims/submit is how a live key will send claims once Payerworks has a trading partner for the payer. Payerworks is connected to no payer today, so it answers 501 `submit_not_offered` with each of its five gates and whether it is open, and nothing is written or sent. GET /v1/partners lists the partners it sends to, and is empty today. New here? Start with [A sandbox claim, to its 835](https://payerworks.com/docs/quickstart-sandbox-claim). ## Routes - [POST /v1/claims/validate](#post-v1-claims-validate): Validate claims - [POST /v1/claims/837p](#post-v1-claims-837p): Write an 837P - [POST /v1/claims/837i](#post-v1-claims-837i): Write an 837I - [POST /v1/claims/837i/validate](#post-v1-claims-837i-validate): Validate institutional claims - [POST /v1/claims/837d](#post-v1-claims-837d): Write an 837D - [POST /v1/claims/837d/validate](#post-v1-claims-837d-validate): Validate dental claims - [POST /v1/claims/correction-plan](#post-v1-claims-correction-plan): Plan a corrected or void claim - [GET /v1/edits](#get-v1-edits): Every claim edit - [GET /v1/edits/{code}](#get-v1-edits-code): One claim edit - [POST /v1/edits/rules/check](#post-v1-edits-rules-check): Check your own claim rules - [POST /v1/x12/parse](#post-v1-x12-parse): Read an X12 file - [POST /v1/sandbox/claims](#post-v1-sandbox-claims): Run the sandbox payer - [POST /v1/sandbox/claims/837i](#post-v1-sandbox-claims-837i): Run the sandbox payer on an 837I - [POST /v1/sandbox/claims/837d](#post-v1-sandbox-claims-837d): Run the sandbox payer on an 837D - [POST /v1/sandbox/claims/timeline](#post-v1-sandbox-claims-timeline): Run the sandbox over days - [POST /v1/claims/submit](#post-v1-claims-submit): Send claims to a payer - [GET /v1/partners](#get-v1-partners): Trading partners Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/claims/validate **Validate claims.** Every reason a claim would be refused at the front door or by a payer’s first edit, each with a code, the field’s path and a sentence, from the one edit table GET /v1/edits lists: the NPIs’ check digits, the dates, the pointers, a total that must equal its lines, the filing indicator, a character X12 reserves, and each diagnosis, HCPCS Level II code and modifier against the release in force on its date of service (the ICD-10 diagnosis releases for fiscal 2025 to 2027, CMS’s HCPCS file from January 2025) and the place of service against CMS’s list. A CPT code is checked for its shape only, and a dental D code or a CPT modifier, which CMS’s file does not carry, is listed as not checked. Every claim carries what the guides require: the submitter’s contact, the billing provider’s tax id, the patient’s address and date of birth, a facility’s address when one is sent, and the filing indicator. Formatting is repaired first, on a copy, and each claim lists its repairs (phone punctuation, a ZIP+4 hyphen, letter case, a diagnosis code’s dot, spaces) and not_checked, every check that could not run and why. For a claim to a later payer: the payers’ order, each earlier payer’s paid amount, claim and line balance, the adjudication date and the patient’s remaining share each at one level, the adjustment groups, the Medicare secondary payer type, and remark codes in force on the day; when Medicare is the later payer, the claim’s total equals each earlier payer’s paid amount plus all its adjustments, and a claim to Medicare is an original claim (frequency 1). Send the claims as JSON, or an 837P file as x12, read back claim by claim. Send your own rules beside them as rules, written as POST /v1/edits/rules/check reads them, and each is applied to every claim after its repairs: a rule that fires is listed as caller_rule with source caller, and one of severity error makes the claim invalid like any other error. Passing means the claim is well formed, not that a payer will pay it. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/validate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsValidate(body); ``` With the Python client ```python answer = pw.post_claims_validate(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } } } ``` The answer (200) ```json { "valid": true, "claims": [ { "index": 0, "reference": "PW-2001", "errors": [], "warnings": [], "repairs": [], "not_checked": [] } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `valid` | boolean | True when no claim has an error. | | `claims` | list of ClaimChecks | | ### POST /v1/claims/837p **Write an 837P.** Up to one hundred claims written as one 005010X222A1 interchange under your sender and receiver ids and your control number, a claim to a secondary or tertiary payer included: send each earlier payer’s adjudication (otherSubscriberInformation, and lineAdjudicationInformation on each line it paid) and the file carries the coordination of benefits loops. Every claim is checked as POST /v1/claims/validate checks it, your own rules included when you send them as rules, and written from its repaired copy; each claim’s answer lists its repairs, what was not checked, and a not_written warning for any field the 837P has no element for. Nothing is written if any claim would be refused. A test key writes test files only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/837p" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaims837p(body); ``` With the Python client ```python answer = pw.post_claims_837p(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 7 } ``` The answer (200) ```json { "version": "005010X222A1", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000007*0*T*:~GS*HC*SAMPLEBILLER*99999*20261019*1500*7*X*005010X222A1~ST*837*0001*005010X222A1~BHT*0019*00*PW-2001*20261019*1500*CH~NM1*41*2*Sample …", "interchange_control_number": "000000007", "group_control_number": "7", "transaction_set_control_numbers": [ "0001" ], "segments": 31, "claims": [ { "reference": "PW-2001", "transaction_set_control_number": "0001", "warnings": [], "repairs": [], "not_checked": [] } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | The implementation guide written. | | `usage` | one of "T", "P" | T or P. | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | | `claims` | list of object | | ### POST /v1/claims/837i **Write an 837I.** Up to one hundred institutional claims, the UB-04’s electronic form, written as one 005010X223A2 interchange from the same JSON as the 837P route, with the institutional fields: the facility type and frequency, the statement dates, admission type, source and patient status, revenue code lines, principal and admitting diagnoses with present on admission, procedures, occurrence, span, value and condition codes, and the attending and operating providers. Every claim is checked first, and nothing is written if any would be refused. A revenue code or a type of bill is checked for its shape: the code set’s words belong to the NUBC. A test key writes test files only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/837i" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaims837i(body); ``` With the Python client ```python answer = pw.post_claims_837i(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Community Hospital", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "organizationName": "Sample Community Hospital", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "282N00000X", "address": { "address1": "200 Hospital Drive", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19700101", "gender": "F", "groupNumber": "GRP1", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "attending": { "lastName": "Doctor", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "207R00000X" }, "claimInformation": { "patientControlNumber": "PW-3001", "claimChargeAmount": "1500.50", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "statementBeginDate": "20261001", "statementEndDate": "20261003", "admissionDateAndHour": "202610010800", "dischargeHour": "1400" }, "claimCodeInformation": { "admissionTypeCode": "1", "admissionSourceCode": "7", "patientStatusCode": "01" }, "principalDiagnosis": { "qualifierCode": "ABK", "principalDiagnosisCode": "J18.9", "presentOnAdmissionIndicator": "Y" }, "admittingDiagnosis": { "qualifierCode": "ABJ", "admittingDiagnosisCode": "R05" }, "otherDiagnosisInformationList": [ [ { "qualifierCode": "ABF", "otherDiagnosisCode": "E119", "presentOnAdmissionIndicator": "Y" } ] ], "serviceLines": [ { "institutionalService": { "serviceLineRevenueCode": "0120", "lineItemChargeAmount": "1000", "measurementUnit": "DA", "serviceUnitCount": "2" }, "serviceDate": "20261001", "serviceDateEnd": "20261002", "lineItemControlNumber": "L1" }, { "institutionalService": { "serviceLineRevenueCode": "0300", "procedureIdentifier": "HC", "procedureCode": "80053", "lineItemChargeAmount": "500.50", "serviceUnitCount": "1" }, "serviceDate": "20261001", "lineItemControlNumber": "L2" } ] } }, "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 8 } ``` The answer (200) ```json { "kind": "837I", "version": "005010X223A2", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000008*0*T*:~GS*HC*SAMPLEBILLER*99999*20261019*1500*8*X*005010X223A2~ST*837*0001*005010X223A2~BHT*0019*00*PW-3001*20261019*1500*CH~NM1*41*2*Sample …", "interchange_control_number": "000000008", "group_control_number": "8", "transaction_set_control_numbers": [ "0001" ], "segments": 41, "claims": [ { "reference": "PW-3001", "transaction_set_control_number": "0001", "warnings": [] } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | The implementation guide written. | | `usage` | one of "T", "P" | T or P. | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | | `kind` | one of "837I", "837D" | | | `claims` | list of object | | ### POST /v1/claims/837i/validate **Validate institutional claims.** The same institutional claims checked against the 837I’s rules without writing a file: each problem names its code, its field and a sentence. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/837i/validate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaims837iValidate(body); ``` With the Python client ```python answer = pw.post_claims_837i_validate(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Community Hospital", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "organizationName": "Sample Community Hospital", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "282N00000X", "address": { "address1": "200 Hospital Drive", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19700101", "gender": "F", "groupNumber": "GRP1", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "attending": { "lastName": "Doctor", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "207R00000X" }, "claimInformation": { "patientControlNumber": "PW-3001", "claimChargeAmount": "1500.50", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "statementBeginDate": "20261001", "statementEndDate": "20261003", "admissionDateAndHour": "202610010800", "dischargeHour": "1400" }, "claimCodeInformation": { "admissionTypeCode": "1", "admissionSourceCode": "7", "patientStatusCode": "01" }, "principalDiagnosis": { "qualifierCode": "ABK", "principalDiagnosisCode": "J18.9", "presentOnAdmissionIndicator": "Y" }, "admittingDiagnosis": { "qualifierCode": "ABJ", "admittingDiagnosisCode": "R05" }, "otherDiagnosisInformationList": [ [ { "qualifierCode": "ABF", "otherDiagnosisCode": "E119", "presentOnAdmissionIndicator": "Y" } ] ], "serviceLines": [ { "institutionalService": { "serviceLineRevenueCode": "0120", "lineItemChargeAmount": "1000", "measurementUnit": "DA", "serviceUnitCount": "2" }, "serviceDate": "20261001", "serviceDateEnd": "20261002", "lineItemControlNumber": "L1" }, { "institutionalService": { "serviceLineRevenueCode": "0300", "procedureIdentifier": "HC", "procedureCode": "80053", "lineItemChargeAmount": "500.50", "serviceUnitCount": "1" }, "serviceDate": "20261001", "lineItemControlNumber": "L2" } ] } } } ``` The answer (200) ```json { "kind": "837I", "version": "005010X223A2", "valid": true, "claims": [ { "index": 0, "reference": "PW-3001", "errors": [], "warnings": [] } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "837I", "837D" | | | `version` | string | | | `valid` | boolean | True when no claim has an error. | | `claims` | list of Claim837Checks | | ### POST /v1/claims/837d **Write an 837D.** Up to one hundred dental claims and predeterminations written as one 005010X224A2 interchange from the same JSON as the 837P route, with the teeth and surfaces, the area of the mouth, tooth status, prosthesis replacement, orthodontic months and up to four diagnoses. A predetermination carries no date of service. A dental procedure code is checked for its shape: the code set’s words belong to the ADA. Nothing is written if any claim would be refused, and a test key writes test files only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/837d" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaims837d(body); ``` With the Python client ```python answer = pw.post_claims_837d(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Dental", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Dental Plan" }, "billing": { "organizationName": "Sample Dental", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "1223G0001X", "address": { "address1": "3 Oak Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19800202", "gender": "F", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "rendering": { "lastName": "Dentist", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "1223G0001X" }, "claimInformation": { "patientControlNumber": "PW-4001", "claimChargeAmount": "385", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "serviceDate": "20261005" }, "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "K02.52" } ], "serviceLines": [ { "dentalService": { "procedureCode": "D2392", "lineItemChargeAmount": "210", "procedureCount": 1, "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "30", "toothSurfaceCodes": [ "M", "O" ] } ], "providerControlNumber": "DL1" }, { "dentalService": { "procedureCode": "D2740", "lineItemChargeAmount": "175", "prosthesisCrownOrInlayCode": "I", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "19" } ], "providerControlNumber": "DL2" } ] } }, "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 8 } ``` The answer (200) ```json { "kind": "837D", "version": "005010X224A2", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000008*0*T*:~GS*HC*SAMPLEBILLER*99999*20261019*1500*8*X*005010X224A2~ST*837*0001*005010X224A2~BHT*0019*00*PW-4001*20261019*1500*CH~NM1*41*2*Sample …", "interchange_control_number": "000000008", "group_control_number": "8", "transaction_set_control_numbers": [ "0001" ], "segments": 36, "claims": [ { "reference": "PW-4001", "transaction_set_control_number": "0001", "warnings": [] } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | The implementation guide written. | | `usage` | one of "T", "P" | T or P. | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | | `kind` | one of "837I", "837D" | | | `claims` | list of object | | ### POST /v1/claims/837d/validate **Validate dental claims.** The same dental claims checked against the 837D’s rules without writing a file: each problem names its code, its field and a sentence. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/837d/validate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaims837dValidate(body); ``` With the Python client ```python answer = pw.post_claims_837d_validate(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Dental", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Dental Plan" }, "billing": { "organizationName": "Sample Dental", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "1223G0001X", "address": { "address1": "3 Oak Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19800202", "gender": "F", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "rendering": { "lastName": "Dentist", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "1223G0001X" }, "claimInformation": { "patientControlNumber": "PW-4001", "claimChargeAmount": "385", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "serviceDate": "20261005" }, "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "K02.52" } ], "serviceLines": [ { "dentalService": { "procedureCode": "D2392", "lineItemChargeAmount": "210", "procedureCount": 1, "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "30", "toothSurfaceCodes": [ "M", "O" ] } ], "providerControlNumber": "DL1" }, { "dentalService": { "procedureCode": "D2740", "lineItemChargeAmount": "175", "prosthesisCrownOrInlayCode": "I", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "19" } ], "providerControlNumber": "DL2" } ] } } } ``` The answer (200) ```json { "kind": "837D", "version": "005010X224A2", "valid": true, "claims": [ { "index": 0, "reference": "PW-4001", "errors": [], "warnings": [] } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "837I", "837D" | | | `version` | string | | | `valid` | boolean | True when no claim has an error. | | `claims` | list of Claim837Checks | | ### POST /v1/claims/correction-plan **Plan a corrected or void claim.** Say whether you want to replace or void a claim, and give the payer’s claim number, or say the payer never processed it (adjudicated: false); with neither, the plan asks which rather than guess. The plan says the frequency code, whether REF*F8 goes, whether to use a new patient control number, and when a Medicare contractor fixes a processed claim by reopening instead. Send the replacement too, as JSON or an 837P (an 837D for a dental claim), and it is checked against the plan and every claim rule. Companion guides vary, and the answer says so. It writes and sends nothing. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/correction-plan" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsCorrectionPlan(body); ``` With the Python client ```python answer = pw.post_claims_correction_plan(body) ``` request.json ```json { "change": "replace", "payer_claim_number": "2026101900123", "original": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, "replacement": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001A", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "7", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ], "claimSupplementalInformation": { "claimControlNumber": "2026101900123" } } } } ``` The answer (200) ```json { "change": "replace", "claim_type": "professional", "stage": "adjudicated", "stage_reason": "The payer gave the claim its own number, so it took the claim into processing.", "medicare_mac": false, "medicare_mac_source": "default", "route": "send_corrected_claim", "frequency_code": "7", "ref_f8": { "send": true, "value": "2026101900123" }, "patient_control_number": { "new_one": true, "original": "PW-2001", "why": "A new reference keeps the corrected claim's acknowledgments and remittance apart from the original's, so neither is matched to the other; some payers ask for the original's, so read the companion guide." }, "steps": [ "Send the whole claim again with every service, corrected: frequency code 7 (CLM05-3).", "Put the payer's claim number in 2300 REF*F8 (2026101900123); without it the payer rejects the claim." ], "caveat": "Companion guides vary: this is the rule most payers publish, and a payer's own companion guide decides. Read it before the first corrected claim you send to that payer.", "sources": [ "https://www.bcbsil.com/docs/provider/il/claims/submission/claim-frequency-codes-prof.pdf", "https://x12.org/resources/requests-for-interpretation/rfi-1278-5010-837-claim-ccndcn" ], "replacement": { "valid": true, "checks": "plan_claim_and_coordination", "errors": [], "warnings": [] } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `change` | string | | | `claim_type` | string | | | `stage` | one of "not_adjudicated", "adjudicated" | | | `stage_reason` | string | | | `medicare_mac` | boolean | | | `medicare_mac_source` | one of "default", "caller", "claim_filing_code" | | | `route` | one of "send_as_new_claim", "nothing_to_void", "send_corrected_claim", "medicare_reopening" | | | `frequency_code` | string or null | CLM05-3. | | `ref_f8` | object | | | `patient_control_number` | object | | | `steps` | list of string | | | `caveat` | string | Companion guides vary, and the answer says so. | | `sources` | list of string | | | `if_returned_unprocessable` | object | A Medicare claim the contractor returned as unprocessable: sent again as a new claim. | | `replacement` | object or null | | ### GET /v1/edits **Every claim edit.** Every check Payerworks runs on a professional claim before it is written: its code, SNIP type, severity, what is wrong, how to fix it and where the rule comes from. Also the formatting repairs made first, the coordination of benefits checks a claim to a later payer gets, the checks a correction plan’s replacement gets, and the checks Payerworks does not run, with why. Filter with snip_type, severity, kind or runs_in, applied to every list. Passing every check means the claim is well formed and its codes are valid on their dates, not that a payer will pay it. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/edits?kind=code_set" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEdits({ query: {"kind":"code_set"} }); ``` With the Python client ```python answer = pw.get_edits(query={"kind": "code_set"}) ``` The answer (200) ```json { "version": "10/19/2026", "scope": "The edits Payerworks runs on a professional (837P) claim before it is written, from the one table the validator runs. Passing them means the claim is well formed and its codes are valid on their dates; it does not mean a payer will pay it, …", "claim_types": [ "837P" ], "filters": { "kind": "code_set" }, "count": 4, "edits": [ { "code": "place_of_service_unassigned", "snip_type": 5, "claim_types": [ "837P" ], "severity": "error", "kind": "code_set", "message": "The place of service is two digits but not a code CMS has assigned, or not one in force on the date of service.", "fix": "Send an assigned place of service code (11 office, 12 home, 22 outpatient hospital and the rest of CMS's list).", "source": { "title": "CMS Place of Service Code Set", "url": "https://www.cms.gov/medicare/coding-billing/place-of-service-codes/code-sets", "read_on": "10/19/2026" }, "runs_in": "edits" }, { "code": "diagnosis_not_valid_on_date", "snip_type": 5, "claim_types": [ "837P" ], "severity": "error", "kind": "code_set", "message": "A diagnosis code is not a valid ICD-10-CM code in the release in force on its date of service.", "fix": "Send a code valid on that day: a code is valid from the October 1 its release takes effect to the next September 30.", "source": { "title": "ICD-10-CM code files, FY2025 to FY2027 (CMS)", "url": "https://www.cms.gov/medicare/coding-billing/icd-10-codes", "read_on": "10/19/2026" }, "runs_in": "edits", "needs_code_set": "icd10cm" } ], "repairs": [ { "code": "spaces_trimmed", "does": "Removes spaces before and after a value." }, { "code": "phone_punctuation_removed", "does": "Writes a phone or fax number as its ten digits: parentheses, hyphens, dots, spaces and a leading +1 are removed." } ], "coordination_of_benefits": [], "replacement_checks": [], "not_run": [ { "code": "taxonomy_not_in_code_set", "snip_type": 5, "why": "The national provider taxonomy is licensed by the AMA through NUCC: its permission must be accepted on behalf of the organization before the set is used in a product. Only the code's shape is checked (invalid_taxonomy).", "source": { "url": "https://www.nucc.org/index.php/code-sets-mainmenu-41/provider-taxonomy-mainmenu-40/csv-mainmenu-57", "read_on": "10/19/2026" } }, { "code": "clia_not_on_file", "snip_type": 5, "why": "CMS publishes the certified laboratories in its quarterly Provider of Services file (15 MB to 36 MB), a lookup for the door, not a table for this claim check. Only the number's shape is checked (invalid_clia).", "source": { "url": "https://catalog.data.gov/dataset/provider-of-services-file-clinical-laboratories-2024-03-02", "read_on": "10/19/2026" } } ], "transmits": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | | | `scope` | string | | | `claim_types` | list of string | | | `filters` | object | The filters this answer applied. | | `count` | integer | | | `edits` | list of Edit | | | `repairs` | list of EditRepair | The formatting repaired on a copy before any check runs. | | `coordination_of_benefits` | list of EditCheckRow | The checks a claim to a later payer gets. | | `replacement_checks` | list of EditCheckRow | The checks a correction plan’s replacement gets. | | `not_run` | list of EditNotRun | | | `transmits` | the value false | | ### GET /v1/edits/{code} **One claim edit.** One check by its code, in any case. A check Payerworks does not run answers runs: false and says why; any other code is 404 edit_not_found. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/edits/future_date" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEditsByCode("future_date"); ``` With the Python client ```python answer = pw.get_edits_by_code("future_date") ``` The answer (200) ```json { "version": "10/19/2026", "runs": true, "edit": { "code": "future_date", "snip_type": 3, "claim_types": [ "837P" ], "severity": "error", "kind": "deterministic", "message": "A date on the claim has not happened yet: a claim bills a service already given.", "fix": "Send the claim after the service, with the day it was given.", "source": { "title": "Decided by the claim itself", "where": "a value that contradicts another on the same claim" }, "runs_in": "edits" } } ``` | Field | Type | What it is | | --- | --- | --- | | `version` | string | | | `runs` | boolean | False for a check Payerworks does not run. | | `edit` | Edit or EditCheckRow or EditNotRun | | ### POST /v1/edits/rules/check **Check your own claim rules.** Rules you write for your own payers: a field’s path, an operator from a fixed list of twenty, a value, the payers it applies to, your message and whether it is an error or a warning. Each rule is checked for the format, and applied to any claims you send with it (up to 100). The same rules may ride on POST /v1/claims/validate and POST /v1/claims/837p as rules, where a rule of severity error refuses the claim. Patterns are a small fixed format, never code, and each call has a step budget, and a field a rule reads may reach at most 1,000 values: a rule it does not reach, or whose field reaches more, is listed as not checked with its reason, never as passed. Nothing is stored. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/edits/rules/check" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEditsRulesCheck(body); ``` With the Python client ```python answer = pw.post_edits_rules_check(body) ``` request.json ```json { "rules": [ { "id": "visit-needs-pos-11", "payer_id": "99999", "field": "claimInformation.placeOfServiceCode", "operator": "not_equals", "value": "11", "severity": "error", "message": "This plan pays office visits in the office only." }, { "id": "member-id-shape", "field": "subscriber.memberId", "operator": "not_matches", "value": "W\\d{9}", "severity": "warning", "message": "Our member ids for this plan are W and nine digits." }, { "id": "strep-test-needs-qw", "scope": "claimInformation.serviceLines[]", "when": [ { "field": "professionalService.procedureCode", "operator": "equals", "value": "87880" } ], "field": "professionalService.procedureModifiers", "operator": "not_contains", "value": "QW", "severity": "error", "message": "A waived rapid strep test (87880) is billed with modifier QW." } ], "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } } } ``` The answer (200) ```json { "valid": true, "rules": [ { "index": 0, "id": "visit-needs-pos-11", "valid": true, "problems": [] }, { "index": 1, "id": "member-id-shape", "valid": true, "problems": [] } ], "operators": [ "exists", "absent" ], "stored": false, "claims": [ { "index": 0, "reference": "PW-2001", "errors": [ { "code": "caller_rule", "rule_id": "strep-test-needs-qw", "field": "claimInformation.serviceLines[1].professionalService.procedureModifiers", "message": "A waived rapid strep test (87880) is billed with modifier QW.", "severity": "error", "source": "caller" } ], "warnings": [], "not_checked": [] } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `valid` | boolean | | | `rules` | list of object | | | `operators` | list of one of "exists", "absent", "equals", "not_equals", "in", "not_in", "contains", "not_contains", "starts_with", "ends_with", "longer_than", "shorter_than", "gt", "gte", "lt", "lte", "before", "after", "matches", "not_matches" | | | `stored` | the value false | | | `claims` | list of object | Each claim sent, with the rules that fired on it (only when every rule is valid). | ### POST /v1/x12/parse **Read an X12 file.** Any X12 file Payerworks reads, to JSON with its envelope, in the file’s own delimiters: an 835, a 999, a 277CA, a 270 or 271, a 276 or 277, a 278, or an 837P, 837I or 837D. A claim file is read back to the same JSON its write route takes, each claim also as its own one-claim interchange, so a bulk file splits per claim, and every segment the reader does not read is named by its id, loop and position, never its value. A 277CA is told from a claim status 277 by its implementation guide, any other kind is named, and an interchange that mixes kinds is refused. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/x12/parse" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postX12Parse(body); ``` With the Python client ```python answer = pw.post_x12_parse(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000071*0*T*:~\nGS*FA*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*71*X*005010X231A1~\nST*999*0001*005010X231A1~\nAK1*HC*42*005010X222A1~\nAK2*837*0001*005010X222A1~\nIK3*NM1*8*2010BA*8~\nCTX*CLM01:PW-1003~\nIK4*9*67*6*W1234#~\nIK5*R*5~\nAK9*R*1*1*0~\nSE*9*0001~\nGE*1*71~\nIEA*1*000000071~" } ``` The answer (200) ```json { "kind": "999", "envelope": { "sender_qualifier": "ZZ", "sender": "SAMPLEPAYER", "receiver_qualifier": "ZZ", "receiver": "SAMPLEBILLER", "date": "261018", "time": "0900", "control_number": "000000071", "usage": "T", "functional_group": "FA", "version": "005010X231A1", "transaction_sets": { "999": 1 }, "segments": 12 }, "acknowledgment": { "ok": true, "groups": [ { "functional": "HC", "control": "42", "version": "005010X222A1", "verdict": "R", "included": 1, "received": 1, "accepted": 0, "codes": [], "sets": [ { "set": "837", "control": "0001", "implementation": "005010X222A1", "verdict": "R", "codes": [ "5" ], "errors": [ { "segment": "NM1", "position": "8", "loop": "2010BA", "code": "8", "claim_reference": "PW-1003", "context": null, "elements": [ { "position": "9", "component": "", "reference": "67", "code": "6", "value": "W1234#" } ] } ] } ] } ] } } ``` | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "270", "271", "276", "277", "277CA", "277RFAI", "278", "835", "837P", "837I", "837D", "999", "997", "TA1", "mixed", "unknown" | The kind of file read. | | `envelope` | Envelope | The interchange envelope, as the file states it. | | `remittance` | object | An 835, read. | | `acknowledgment` | object | A 999, read. | | `claim_acknowledgment` | object | A 277CA, read. | | `interchange_acknowledgment` | object | A TA1, read: each TA1’s verdict (TA104) and note code (TA105), in words. | | `eligibility_request` | object | A 270, read: each request in the JSON shape POST /v1/eligibility/270 takes, and not_read, every segment the reader does not read. | | `eligibility_answer` | object | A 271, read: each transaction, with answered_on and unread. | | `claim_status_inquiry` | object | A 276, read: each question in the JSON shape POST /v1/claim-status/276 takes. | | `claim_status` | object | A 277 claim status answer (005010X212), read. | | `professional_claims` | ClaimFileRead | An 837P, 837I or 837D read back. | | `institutional_claims` | ClaimFileRead | An 837P, 837I or 837D read back. | | `dental_claims` | ClaimFileRead | An 837P, 837I or 837D read back. | | `authorization` | object | A 278 request or response (005010X217), read: each review with its decision, dates and services. | ### POST /v1/sandbox/claims **Run the sandbox payer.** Your claims written as an 837P and answered by the sandbox payer in the same call: a 999 for the file, a 277CA for each claim and an 835 for each claim it adjudicated, every file explained, and claims[] with each claim’s warnings, its repairs and the checks that could not run. Any key may call it, every file is a test file, and nothing reaches a payer. How the member ID ends decides the answer. A member ID ending in RFAI is held for a document: a 277 request for additional information follows. Send the same claims again with the solicited 275 (attachments_275, at most five) and the claim is paid. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/claims" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxClaims(body); ``` With the Python client ```python answer = pw.post_sandbox_claims(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "claims": [ { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Sleep Dentistry", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Sleep Dentistry", "taxonomyCode": "1223S0112X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19600214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-SBX-PAID", "claimChargeAmount": "2900.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "G4733" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "E0486", "procedureModifiers": [ "KX" ], "lineItemChargeAmount": "2900.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Sleep Dentistry", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Sleep Dentistry", "taxonomyCode": "1223S0112X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W12345REJ", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19600214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-SBX-REJ", "claimChargeAmount": "2900.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "G4733" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "E0486", "procedureModifiers": [ "KX" ], "lineItemChargeAmount": "2900.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } } ] } ``` The answer (200) ```json { "usage": "T", "transmitted": false, "control": { "interchange": "000000001", "group": "1", "sets": [ "0001", "0002" ] }, "files": { "claims_837p": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*1500*1*X*005010X222A1~ST*837*0001*005010X222A1~BHT*0019*00*PW-SBX-PAID*20261019*1500*CH~NM1*41*2…", "acknowledgment_999": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*1500*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~I…", "claim_acknowledgment_277ca": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1500*TH~HL*1**20*1~NM…", "remittance_835": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000003*0*T*:~GS*HP*PWSANDBOX*SAMPLEBILLER*20261019*1500*3*X*005010X221A1~ST*835*0001~BPR*I*1392*C*CHK************20261019~TRN*1*PWSANDBOX-000000003…" }, "outcomes": [ { "reference": "PW-SBX-PAID", "outcome": "paid", "charge": 2900, "allowed": 1740, "paid": 1392, "patient_responsibility": 348, "payer_claim_number": "PWS2292711347" }, { "reference": "PW-SBX-REJ", "outcome": "rejected", "charge": 2900, "allowed": null, "paid": null, "patient_responsibility": null, "payer_claim_number": "" } ], "claims": [ { "reference": "PW-SBX-PAID", "warnings": [], "repairs": [], "not_checked": [] }, { "reference": "PW-SBX-REJ", "warnings": [], "repairs": [], "not_checked": [] } ], "explained": { "acknowledgment_999": { "kind": "999", "groups": [ { "control_number": "1", "verdict": "A", "verdict_words": "Accepted", "received": 2, "accepted": 2, "transaction_sets": [ { "control_number": "0001", "verdict": "A", "verdict_words": "Accepted", "fixes": [] }, { "control_number": "0002", "verdict": "A", "verdict_words": "Accepted", "fixes": [] } ] } ] }, "claim_acknowledgment_277ca": { "kind": "277CA", "batch": { "trace": "000000001", "statuses": [ { "category": "A1", "code": "19", "entity": "PR", "date": "20261019", "action": "WQ", "amount": "5800" } ], "accepted": 1, "rejected": 1 }, "claims": [ { "reference": "PW-SBX-PAID", "payer_claim_number": "PWS2292711347", "verdict": "A", "fixes": [ { "category": "A2", "code": "20", "entity": "", "verdict": "A", "where": "", "sentence": "The claim was accepted into the payer's adjudication system: accepted for processing (STC A2:20). Nothing to fix." } ] }, { "reference": "PW-SBX-REJ", "payer_claim_number": "", "verdict": "R", "fixes": [ { "category": "A7", "code": "164", "entity": "IL", "verdict": "R", "where": "the subscriber in your claim data", "sentence": "The claim was rejected for invalid information: the subscriber's contract or member number is missing or invalid (STC A7:164:IL). Fix it on the subscriber in your claim data, then send the claim again." } ] } ] }, "remittance": { "check": { "number": "PWSANDBOX-000000003-1", "date": "20261019", "amount": "1392", "method_flag": "C", "payer": "PAYERWORKS SANDBOX PAYER", "payee_ids": { "npis": [ "1234567893" ], "tins": [ "123456789" ] } }, "checks": [ { "number": "PWSANDBOX-000000003-1", "date": "20261019", "amount": "1392", "method_flag": "C", "payer": "PAYERWORKS SANDBOX PAYER", "payee_ids": { "npis": [ "1234567893" ], "tins": [ "123456789" ] }, "references": [ "PW-SBX-PAID" ], "balance": { "stated": 1392, "claims_net": 1392, "provider_adjustments_net": 0, "variance": 0, "ties": true, "words": "The claims, less the provider-level adjustments, add up to the check." }, "provider_adjustments": [] } ], "claims": [ { "reference": "PW-SBX-PAID", "payer_claim_number": "PWS2292711347", "status_code": "1", "status": "processed as primary", "service_date": "20261001", "billed": 2900, "paid": 1392, "allowed": 1740, "patient_responsibility": 348, "amounts": { "billed": 2900, "paid": 1392, "allowed": 1740, "allowed_basis": "stated", "patient_responsibility": 348, "patient_basis": "stated", "adjustment": 1160, "adjustment_basis": "stated", "write_off": 1160, "below_allowed": 0, "not_paid": 0, "shortfall": 0 }, "adjustments": [ { "group": "CO", "code": "45", "amount": 1160, "label": "Charge exceeds the fee schedule / contracted amount", "category": "Underpayment", "known": true, "label_source": "paraphrase", "patient_billable": false } ], "patient_adjustments": [ { "group": "PR", "code": "2", "amount": 348, "label": "Coinsurance amount", "category": "Other", "known": true, "label_source": "paraphrase", "patient_billable": true } ], "remark_codes": [], "primary_reason": null, "crossover_carrier": "", "service_lines": [ { "procedure_code": "E0486", "modifiers": [ "KX" ], "billed": 2900, "paid": 1392, "allowed": 1740 } ], "check_number": "PWSANDBOX-000000003-1", "index": 0, "claim_filing_indicator": "CI", "verdict": "paid", "verdict_words": "Paid: the payer paid $1,392.00, the patient owes $348.00 (patient responsibility), and $1,160.00 is a contractual adjustment.", "remarks": [], "claim_adjustments": [], "lines": [ { "index": 0, "procedure_code": "E0486", "procedure_qualifier": "HC", "modifiers": [ "KX" ], "billed": 2900, "paid": 1392, "allowed": 1740, "units_paid": 1, "service_date": "2026-10-01", "service_date_end": null, "line_control_number": "", "adjustments": [ { "group": "CO", "code": "45", "amount": 1160, "line": 0 }, { "group": "PR", "code": "2", "amount": 348, "line": 0 } ], "remarks": [] } ] } ], "provider_adjustments": [], "groups": { "CO": { "name": "Contractual obligation", "patient_billable": false, "words": "An adjustment your agreement with the payer, or a regulation, requires. It is generally a write-off for you and is not billed to the patient." }, "OA": { "name": "Other adjustment", "patient_billable": "depends", "words": "Used when no other group applies, such as the effect of an earlier payer's payment. Whether the patient may be billed depends on the reason code beside it and on your agreement with the plan." }, "PI": { "name": "Payer-initiated reduction", "patient_billable": false, "words": "A reduction the payer made itself. It is not a patient responsibility amount, so it is not billed to the patient." }, "PR": { "name": "Patient responsibility", "patient_billable": true, "words": "An amount that may be billed to the patient, such as the deductible, coinsurance or copay." } } } }, "events": [ { "id": "evt_cc264f7536a9315a0c6fe09f", "type": "sandbox.file.acknowledged" }, { "id": "evt_e38518c4523f2dcd24e34de2", "type": "sandbox.claim.acknowledged" } ], "webhook": { "configured": false, "queued": 0 } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of SandboxOutcome | | | `claims` | list of object | Each claim as it was checked, as POST /v1/claims/837p answers it beside its file. | | `explained` | object | Each answer as /v1/acknowledgments/explain, /v1/remittances/explain and /v1/attachments/requests/explain say it. | | `events` | list of EventRef | | | `webhook` | object | Events are queued for your webhook, never delivered inside the call; GET /v1/events says what each delivery did. | ### POST /v1/sandbox/claims/837i **Run the sandbox payer on an 837I.** Your institutional claims checked as POST /v1/claims/837i checks them, written as an 837I and answered by the same sandbox payer as the professional claims, by the same rules: how the member ID ends decides the answer, and the paid amounts and payer claim numbers are made the same way. The answer is a 999 for the file, a 277CA for each claim and an 835 for each claim it adjudicated, and explain_with names the routes that explain them. A member ID ending in RFAI is held for a document, as on POST /v1/sandbox/claims: send the same claims again with the solicited 275 in attachments_275 and the claim is paid. Any key may call it, every file is a test file, nothing is posted to your webhook, and nothing reaches a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/claims/837i" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxClaims837i(body); ``` With the Python client ```python answer = pw.post_sandbox_claims_837i(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Community Hospital", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "organizationName": "Sample Community Hospital", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "282N00000X", "address": { "address1": "200 Hospital Drive", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19700101", "gender": "F", "groupNumber": "GRP1", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "attending": { "lastName": "Doctor", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "207R00000X" }, "claimInformation": { "patientControlNumber": "PW-3001", "claimChargeAmount": "1500.50", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "statementBeginDate": "20261001", "statementEndDate": "20261003", "admissionDateAndHour": "202610010800", "dischargeHour": "1400" }, "claimCodeInformation": { "admissionTypeCode": "1", "admissionSourceCode": "7", "patientStatusCode": "01" }, "principalDiagnosis": { "qualifierCode": "ABK", "principalDiagnosisCode": "J18.9", "presentOnAdmissionIndicator": "Y" }, "admittingDiagnosis": { "qualifierCode": "ABJ", "admittingDiagnosisCode": "R05" }, "otherDiagnosisInformationList": [ [ { "qualifierCode": "ABF", "otherDiagnosisCode": "E119", "presentOnAdmissionIndicator": "Y" } ] ], "serviceLines": [ { "institutionalService": { "serviceLineRevenueCode": "0120", "lineItemChargeAmount": "1000", "measurementUnit": "DA", "serviceUnitCount": "2" }, "serviceDate": "20261001", "serviceDateEnd": "20261002", "lineItemControlNumber": "L1" }, { "institutionalService": { "serviceLineRevenueCode": "0300", "procedureIdentifier": "HC", "procedureCode": "80053", "lineItemChargeAmount": "500.50", "serviceUnitCount": "1" }, "serviceDate": "20261001", "lineItemControlNumber": "L2" } ] } } } ``` The answer (200) ```json { "kind": "837I", "version": "005010X223A2", "usage": "T", "transmitted": false, "control": { "interchange": "000000001", "group": "1", "sets": [ "0001" ] }, "files": { "claims_837i": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*1500*1*X*005010X223A2~ST*837*0001*005010X223A2~BHT*0019*00*PW-3001*20261019*1500*CH~NM1*41*2*Sam…", "acknowledgment_999": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*1500*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X223A2~AK2*837*0001*005010X223A2~I…", "claim_acknowledgment_277ca": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1500*TH~HL*1**20*1~NM…", "remittance_835": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000003*0*T*:~GS*HP*PWSANDBOX*SAMPLEBILLER*20261019*1500*3*X*005010X221A1~ST*835*0001~BPR*I*720.24*C*CHK************20261019~TRN*1*PWSANDBOX-0000000…" }, "outcomes": [ { "reference": "PW-3001", "outcome": "paid", "charge": 1500.5, "allowed": 900.3, "paid": 720.24, "patient_responsibility": 180.06, "payer_claim_number": "PWS1424823247" } ], "claims": [ { "reference": "PW-3001", "warnings": [] } ], "explain_with": { "acknowledgments": "POST /v1/acknowledgments/explain", "remittance": "POST /v1/remittances/explain" } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "837I", "837D" | | | `version` | string | | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of SandboxOutcome | | | `claims` | list of object | | | `explain_with` | object | The routes that explain the files: these runs are not explained inside the answer, and nothing is posted to your webhook. | | `loinc_notice` | string | Only when a claim is held for information. | ### POST /v1/sandbox/claims/837d **Run the sandbox payer on an 837D.** Your dental claims and predeterminations, checked as POST /v1/claims/837d checks them and answered by the same sandbox payer, by the same member ID endings, with a 999, a 277CA and an 835. A predetermination is answered as one: CLP02 25, nothing paid, each line’s would-be payment as an OA 101 adjustment, and the estimate in the outcome. A member ID ending in RFAI is held for a document: send the same claims again with the solicited 275 in attachments_275 and the claim is paid. Any key may call it, every file is a test file, nothing is posted to your webhook, and nothing reaches a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/claims/837d" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxClaims837d(body); ``` With the Python client ```python answer = pw.post_sandbox_claims_837d(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "claims": [ { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Dental", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Dental Plan" }, "billing": { "organizationName": "Sample Dental", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "1223G0001X", "address": { "address1": "3 Oak Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19800202", "gender": "F", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "rendering": { "lastName": "Dentist", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "1223G0001X" }, "claimInformation": { "patientControlNumber": "PW-4001", "claimChargeAmount": "385", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "claimDateInformation": { "serviceDate": "20261005" }, "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "K02.52" } ], "serviceLines": [ { "dentalService": { "procedureCode": "D2392", "lineItemChargeAmount": "210", "procedureCount": 1, "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "30", "toothSurfaceCodes": [ "M", "O" ] } ], "providerControlNumber": "DL1" }, { "dentalService": { "procedureCode": "D2740", "lineItemChargeAmount": "175", "prosthesisCrownOrInlayCode": "I", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "19" } ], "providerControlNumber": "DL2" } ] } }, { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Dental", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Dental Plan" }, "billing": { "organizationName": "Sample Dental", "npi": "1234567893", "employerId": "123456789", "taxonomyCode": "1223G0001X", "address": { "address1": "3 Oak Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "firstName": "Sample", "lastName": "Patient", "dateOfBirth": "19800202", "gender": "F", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "rendering": { "lastName": "Dentist", "firstName": "Sample", "npi": "1245319599", "taxonomyCode": "1223G0001X" }, "claimInformation": { "patientControlNumber": "PW-4002", "claimChargeAmount": "385", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "claimFilingCode": "CI", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "K02.52" } ], "serviceLines": [ { "dentalService": { "procedureCode": "D2392", "lineItemChargeAmount": "210", "procedureCount": 1, "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "30", "toothSurfaceCodes": [ "M", "O" ] } ], "providerControlNumber": "DL1" }, { "dentalService": { "procedureCode": "D2740", "lineItemChargeAmount": "175", "prosthesisCrownOrInlayCode": "I", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } }, "teethInformation": [ { "toothCode": "19" } ], "providerControlNumber": "DL2" } ], "predeterminationOfBenefits": true } } ] } ``` The answer (200) ```json { "kind": "837D", "version": "005010X224A2", "usage": "T", "transmitted": false, "control": { "interchange": "000000001", "group": "1", "sets": [ "0001", "0002" ] }, "files": { "claims_837d": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*1500*1*X*005010X224A2~ST*837*0001*005010X224A2~BHT*0019*00*PW-4001*20261019*1500*CH~NM1*41*2*Sam…", "acknowledgment_999": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*1500*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X224A2~AK2*837*0001*005010X224A2~I…", "claim_acknowledgment_277ca": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1500*TH~HL*1**20*1~NM…", "remittance_835": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000003*0*T*:~GS*HP*PWSANDBOX*SAMPLEBILLER*20261019*1500*3*X*005010X221A1~ST*835*0001~BPR*I*184.8*C*CHK************20261019~TRN*1*PWSANDBOX-00000000…" }, "outcomes": [ { "reference": "PW-4001", "outcome": "paid", "charge": 385, "allowed": 231, "paid": 184.8, "patient_responsibility": 46.2, "payer_claim_number": "PWS3214346392" }, { "reference": "PW-4002", "outcome": "paid", "charge": 385, "allowed": 231, "paid": 0, "patient_responsibility": 46.2, "payer_claim_number": "PWS3264679249", "predetermination": true, "estimated_payment": 184.8 } ], "claims": [ { "reference": "PW-4001", "warnings": [] }, { "reference": "PW-4002", "warnings": [] } ], "explain_with": { "acknowledgments": "POST /v1/acknowledgments/explain", "remittance": "POST /v1/remittances/explain" } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "837I", "837D" | | | `version` | string | | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of SandboxOutcome | | | `claims` | list of object | | | `explain_with` | object | The routes that explain the files: these runs are not explained inside the answer, and nothing is posted to your webhook. | | `loinc_notice` | string | Only when a claim is held for information. | ### POST /v1/sandbox/claims/timeline **Run the sandbox over days.** The claims sandbox spread over simulated time: the 999 after five minutes, a 277CA saying each claim was received after an hour, the payer’s own 277CA after two days and the 835 after fourteen. Send the claims, a seed, the start and the moment to read as of; send a later moment and the cursor you were given to read what came next. Each event is the exact body a webhook sends, so you can post it to your own handler; Payerworks posts none of them. Add format: report for the 277CAs and 835 in the JSON field names most clearinghouse APIs use. Nothing is stored or sent: the same request gives the same answer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/claims/timeline" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxClaimsTimeline(body); ``` With the Python client ```python answer = pw.post_sandbox_claims_timeline(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "claims": [ { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Sleep Dentistry", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Sleep Dentistry", "taxonomyCode": "1223S0112X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19600214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-SBX-PAID", "claimChargeAmount": "2900.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "G4733" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "E0486", "procedureModifiers": [ "KX" ], "lineItemChargeAmount": "2900.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Sleep Dentistry", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Sleep Dentistry", "taxonomyCode": "1223S0112X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W12345REJ", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19600214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-SBX-REJ", "claimChargeAmount": "2900.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "G4733" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "E0486", "procedureModifiers": [ "KX" ], "lineItemChargeAmount": "2900.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } } ], "seed": "demo-1", "start": "2026-10-19T09:00:00Z", "as_of": "2026-10-21T09:00:00Z" } ``` The answer (200) ```json { "usage": "T", "transmitted": false, "stored": false, "run_id": "tl_e5d82a7c305b0437", "start": "2026-10-19T09:00:00.000Z", "as_of": "2026-10-21T09:00:00.000Z", "format": "x12", "schedule": [ { "stage": "file_acknowledged", "kind": "999", "at": "2026-10-19T09:05:00.000Z", "file": true, "events": 1, "order": 1 }, { "stage": "claims_received", "kind": "277CA", "at": "2026-10-19T10:00:00.000Z", "file": true, "events": 2, "order": 2 } ], "items": [ { "sequence": 1, "at": "2026-10-19T09:05:00.000Z", "stage": "file_acknowledged", "file_id": "tl_e5d82a7c305b0437.f1", "event": { "id": "evt_60ed631692398d5d8974da74", "type": "sandbox.file.acknowledged", "created": 1792400700, "mode": "test", "data": { "acknowledgment": "999", "interchange_control_number": "000000001", "group_control_number": "1", "verdict": "A", "verdict_words": "Accepted", "received": 2, "accepted": 2, "transaction_sets": [ { "control_number": "0001", "verdict": "A" }, { "control_number": "0002", "verdict": "A" } ] } } }, { "sequence": 2, "at": "2026-10-19T10:00:00.000Z", "stage": "claims_received", "file_id": "tl_e5d82a7c305b0437.f2", "event": { "id": "evt_5fed61838f3988a48c74df2d", "type": "sandbox.claim.acknowledged", "created": 1792404000, "mode": "test", "data": { "reference": "PW-SBX-PAID", "payer_claim_number": "", "verdict": "A", "fixes": [ "The claim was received, not yet accepted for adjudication: the payer acknowledges receipt (STC A1:19:PR). Nothing to fix." ] } } } ], "files": [ { "id": "tl_e5d82a7c305b0437.f1", "stage": "file_acknowledged", "kind": "999", "at": "2026-10-19T09:05:00.000Z", "x12": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*0905*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*0905*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~I…" }, { "id": "tl_e5d82a7c305b0437.f2", "stage": "claims_received", "kind": "277CA", "at": "2026-10-19T10:00:00.000Z", "x12": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1000*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1000*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1000*TH~HL*1**20*1~NM…" } ], "claims_837p": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*0900*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*0900*1*X*005010X222A1~ST*837*0001*005010X222A1~BHT*0019*00*PW-SBX-PAID*20261019*0900*CH~NM1*41*2…", "control": { "interchange": "000000001", "group": "1", "sets": [ "0001", "0002" ] }, "claims": [ { "reference": "PW-SBX-PAID", "state": "accepted", "payer_claim_number": "PWS2292711347" }, { "reference": "PW-SBX-REJ", "state": "rejected", "payer_claim_number": "" } ], "checked": [ { "reference": "PW-SBX-PAID", "warnings": [], "repairs": [], "not_checked": [] }, { "reference": "PW-SBX-REJ", "warnings": [], "repairs": [], "not_checked": [] } ], "next_cursor": "tl_e5d82a7c305b0437.5", "has_more": false, "next_event_at": "2026-11-02T09:00:00.000Z", "complete": false, "events_total": 6 } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `usage` | the value "T" | | | `transmitted` | the value false | | | `stored` | the value false | | | `run_id` | string | | | `start` | string | | | `as_of` | string | | | `format` | one of "x12", "report", "both" | | | `schedule` | list of object | | | `items` | list of object | | | `files` | list of object | | | `claims_837p` | string | The 837P the run sent (first read only). | | `control` | object | First read only. | | `claims` | list of object | | | `checked` | list of object | | | `next_cursor` | string | Pass as cursor with a later as_of. | | `has_more` | boolean | More events are already due: read again before moving as_of. | | `next_event_at` | string or null | | | `complete` | boolean | | | `events_total` | integer | | ### POST /v1/claims/submit **Send claims to a payer.** Not offered yet: Payerworks is connected to no payer, so this answers 501 submit_not_offered with each gate and whether it is open (sending switched on, a live key, a signed business associate agreement recorded for your account, a trading partner for the payer, and that partner switched on). Nothing is written or sent. Once every gate is open it writes your claims as an 837P exactly as POST /v1/claims/837p does, under the partner’s interchange ids and a control number Payerworks assigns, and hands the file to the partner; transmitted is true only when the partner’s connection accepted the file. Every patientControlNumber begins with your account’s reference prefix, and one already in flight is refused unless it was rejected or the claim is a replacement or a void. The partner’s 999, 277CA and 835 arrive as submission events to the key that sent the file, never to anyone else. Send an Idempotency-Key: a retry under it answers the first answer and sends nothing again. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/submit" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsSubmit(body); ``` With the Python client ```python answer = pw.post_claims_submit(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "60054", "receiver": { "organizationName": "SAMPLE HEALTH PLAN" }, "submitter": { "organizationName": "Sample Dental", "contactInformation": { "name": "BILLING OFFICE", "email": "billing@sample.test" } }, "billing": { "organizationName": "Sample Dental", "npi": "1234567893", "employerId": "12-3456789", "taxonomyCode": "1223S0112X", "address": { "address1": "100 Sample St", "city": "Provo", "state": "UT", "postalCode": "84601-1234" }, "contactInformation": { "name": "BILLING OFFICE", "phoneNumber": "(801) 555-0100" } }, "subscriber": { "memberId": "TESTMEMBER01", "paymentResponsibilityLevelCode": "P", "firstName": "JANE", "lastName": "DOE", "gender": "F", "dateOfBirth": "1960-02-03", "groupNumber": "GRP100", "address": { "address1": "9 Test Ave", "city": "Provo", "state": "UT", "postalCode": "84601" } }, "rendering": { "npi": "1922222223", "firstName": "JOHN", "lastName": "DOE" }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "SDX-1001", "claimChargeAmount": "250.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "G47.33" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R06.83" } ], "serviceLines": [ { "serviceDate": "2026-10-01", "providerControlNumber": "PW-SELF-1-L1", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "procedureModifiers": [ "25" ], "lineItemChargeAmount": "250.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } } ] } } } ``` The answer today (501) ```json { "error": { "code": "submit_not_offered", "message": "Payerworks does not send this claim yet: `gates` names each gate and whether it is open. Nothing was written or sent; write the 837P with POST /v1/claims/837p and send it on the connection you have.", "gates": [ { "gate": "service", "open": false, "sentence": "Sending claims is not switched on for Payerworks yet." }, { "gate": "live_key", "open": null, "sentence": "Not checked: a gate before it is closed." } ], "connection": "Payerworks is connected to no payer.", "transmitted": false } } ``` Every call is answered this way today. The answer once every gate is open is described, field by field, below and in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `submission_id` | string | | | `partner_id` | string | | | `partner_name` | string | | | `usage` | the value "P" | | | `interchange_control_number` | string | ISA13, assigned by Payerworks. | | `group_control_number` | string | GS06. | | `claims` | list of object | | | `transfer_id` | string | The partner connection’s id for the transfer that accepted the file. | | `sent_at` | string | | | `acknowledgments` | string | Where the partner’s answers arrive. | | `transmitted` | the value true | Only once the partner’s connection accepted the file: every other answer is a refusal, with transmitted: false. | | `warnings` | list of FieldProblem | | ### GET /v1/partners **Trading partners.** The trading partners claims go to through POST /v1/claims/submit today, with the payer ids each takes and the transactions it carries, and one sentence about Payerworks’ connections. Today the list is empty: Payerworks is connected to no payer. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/partners" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getPartners(); ``` With the Python client ```python answer = pw.get_partners() ``` The answer (200) ```json { "connection": "Payerworks is connected to no payer.", "partners": [] } ``` | Field | Type | What it is | | --- | --- | --- | | `connection` | string | The one sentence about Payerworks’ connections; today: Payerworks is connected to no payer. | | `partners` | list of object | Empty today. | ## Reading the answer - A validate answer is `valid` and, per claim, `errors`, `warnings`, `repairs` (the formatting Payerworks fixed on a copy: phone punctuation, a ZIP+4 hyphen, letter case, a diagnosis code’s dot) and `not_checked` (each check that could not run, with why). Passing means well formed and valid on the date of service, never that a payer will pay. - Each claim answer carries back your patientControlNumber as `reference`, so the answer matches your claim. - A written file comes back as `x12`, with its interchange, group and transaction set control numbers and `transmitted: false`. ## Troubleshooting - `invalid_claims` (422): At least one claim would be refused, so nothing was written. Each claim’s errors name the field; fix them and send the whole set again. - `duplicate_reference` (422): Two claims carry the same patientControlNumber. Each must be unique in one call, because the answers match back on it. - A value holding a character X12 reserves (* : ^ ~) is refused and named by field, never silently rewritten. - A CPT code is checked for its shape only, and a dental D code or a CPT modifier is listed as not checked: CMS’s HCPCS file carries neither. - `sandbox_test_only` (400): The sandbox answers test interchanges only: leave usage out, or send T. - `invalid_sandbox_attachments` (422): A 275 sent back to the sandbox as attachments_275 could not be read (at most five, each a test 275’s X12 text): nothing was run. - `adjudication_unknown` (422): A correction plan needs to know whether the payer processed the claim: send its payer claim number, or `adjudicated: false`. - `submit_not_offered` (501): A gate on a real send is closed: `gates` names each, with whether it is open. A 501 is never retried by the clients: it will not pass by waiting. Write the 837P with POST /v1/claims/837p and send it on the connection you have. - `reference_prefix_required` (422): Every patientControlNumber you send begins with your account’s reference prefix, which Payerworks records with your business associate agreement. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `cursor_other_run` | 400 | This cursor belongs to another timeline run: the claims, the seed or the start differ from the run that made it. | | `invalid_adjudicated` | 400 | adjudicated is true or false: true says the payer processed the claim even when its number is not at hand. | | `invalid_change` | 400 | change is "replace" (correct a claim) or "void" (cancel it). | | `invalid_correction_claim` | 400 | original and replacement are each one claim in the 837P JSON shape POST /v1/claims/837p takes. | | `invalid_edit_code` | 400 | An edit code is snake_case letters, digits and underscores, at most 64 characters, such as future_date. | | `invalid_edit_filter` | 400 | A filter on GET /v1/edits is snip_type (1 to 7), severity (error or warning), kind (deterministic or code_set) or runs_in (edits, validateClaim, validateSecondaryClaim or correction-plan); the message names the one that is not. | | `invalid_format` | 400 | format is "x12", "report" or "both". | | `invalid_instant` | 400 | start and as_of are moments written in ISO 8601 with a time and a zone (such as 2026-10-19T09:00:00Z), from the year 2000 to 2099, and as_of is not before start. | | `invalid_medicare_mac` | 400 | medicare_mac is true or false. | | `invalid_partner_id` | 400 | partner_id is a partner's id as GET /v1/partners lists it, or left out to send to the one partner taking the claims' payer. | | `invalid_payer_claim_number` | 400 | payer_claim_number is the payer's own number for the claim, at most 50 characters and none of the X12 delimiters (~ * : ^). | | `invalid_row_offset` | 400 | row_offset is a whole number from 0 to 1000000000: the rows of the whole file before the first data row of this piece. | | `invalid_seed` | 400 | seed is your own string for the run, 1 to 64 of A-Z, a-z, 0-9, _ and -: the same seed, claims and start give the same run. | | `invalid_timeline_cursor` | 400 | cursor is the next_cursor an earlier timeline answer gave, unchanged. | | `invalid_timeline_limit` | 400 | limit is a whole number from 1 to 500. | | `no_claim` | 400 | The body has no claim: send `claim` (or `claims`) in the 837P shape the docs describe. | | `no_mrf` | 400 | Send the file's text as `csv` (the tall or wide CSV template) or the file as `json` (an object, or its text). | | `no_rules` | 400 | Send the rules as `rules`, a list of one or more rule objects. | | `no_x12` | 400 | Send the file's text as `x12`. | | `sandbox_test_only` | 400 | A sandbox payer answers test interchanges only: usage is "T" or left out. | | `submit_envelope_assigned` | 400 | On a submission Payerworks sets the interchange: leave out sender_id, receiver_id, the two qualifiers, control_number and usage. | | `too_many_claims` | 400 | At most 100 claims a call. | | `too_many_rules` | 400 | At most 100 rules a call. | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `edit_not_found` | 404 | No edit has this code: GET /v1/edits lists every edit Payerworks runs and the ones it does not. | | `reference_in_flight` | 409 | A claim with this patientControlNumber was already sent to this partner and has not been rejected; a corrected claim (frequency 7 or 8) may reuse it. | | `mrf_too_large` | 413 | The file is longer than 1990000 characters, or holds more than 50000 rows or 50000 items: check it in pieces, the first three rows sent again with each piece and row_offset set. | | `adjudication_unknown` | 422 | Say whether the payer processed the claim: send payer_claim_number (CLP07 on the 835, REF*1K on the 277CA) when it did, or adjudicated: false when it was rejected before processing (a 999 or 277CA rejection). Sending a processed claim again as a new one is a duplicate. | | `claim_type_not_planned` | 422 | claim_type is "professional" or "dental": an institutional claim carries its frequency in the type of bill, which this plan does not cover yet. | | `dental_not_checked_here` | 422 | A dental claim sent as an 837D is read where the API runs (POST /v1/claims/correction-plan), not on this page: send it as JSON here, or call the API. | | `duplicate_reference` | 422 | Two claims carry the same reference (patientControlNumber, CLM01); each must be unique so its answers match back. | | `invalid_claims` | 422 | One or more claims would be refused; nothing was written or run. Each claim's errors are listed. | | `invalid_rules` | 422 | One or more rules are not written in the rule format; nothing was applied. Each rule's problems are listed. | | `invalid_sandbox_attachments` | 422 | A 275 sent back to the sandbox cannot be read; nothing was run. Each problem is listed with its field. | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | | `mrf_not_json` | 422 | `json` is not a JSON document that can be read. | | `not_an_837d` | 422 | With claim_type dental, original_x12 and replacement_x12 are each a one-claim 837D interchange (GS08 005010X224A2). | | `not_an_837p` | 422 | An x12 read as claims (original_x12 and replacement_x12 on POST /v1/claims/correction-plan, x12 on POST /v1/claims/validate) is an 837P interchange (GS08 005010X222A1). | | `not_read_here` | 422 | This file is a kind Payerworks recognizes but does not read here yet; the error names its kind. | | `payer_claim_number_needed` | 422 | The payer processed the claim, so the plan needs its claim number: it is CLP07 on the 835, REF*1K on the 277CA, or the number a claim status answer gives. | | `payer_not_at_partner` | 422 | A claim names a payer this partner does not take claims for; send it in a call of its own to the partner that does. | | `reference_prefix_required` | 422 | Every claim's patientControlNumber must begin with your account's reference prefix, so each answer can be matched to you alone; the error names the prefix and the claims. | | `several_claims_in_file` | 422 | original_x12 and replacement_x12 each hold exactly one claim: split a bulk file with POST /v1/x12/parse first. | | `unrecognized_x12` | 422 | This is not an X12 file Payerworks reads; the message lists the kinds it reads. | | `submit_not_offered` | 501 | Payerworks does not send this claim yet: `gates` names each gate and whether it is open. Nothing was written or sent; write the 837P with POST /v1/claims/837p and send it on the connection you have. | | `timeline_not_offered` | 501 | The sandbox timeline is not offered where this was asked: it is not connected to the claim checks here, so nothing was run. This is not a passing fault, so sending the request again answers the same. | | `submit_not_sent` | 503 | The file was written but the partner's connection did not take it just now, so nothing was sent (`transmitted: false`). Send the call again. | ## Code lists ### The claims sandbox’s member ID endings | Ending | Outcome | What it answers | | --- | --- | --- | | `REJ` | `rejected` | A member ID ending in REJ is rejected by the payer’s front end. The 277CA refuses the claim with STC A7:164:IL (the subscriber’s member number is invalid), and no remittance follows. | | `DENY` | `denied` | A member ID ending in DENY is accepted on the 277CA and then denied on the 835 the way payers send a refusal: claim status 1 (processed), nothing paid, the whole charge adjusted under CO 50, and each line carrying remark N115, so only the adjustments show the refusal. | | `PEND` | `pending` | A member ID ending in PEND is accepted on the 277CA and stays with the payer, so no remittance line comes back yet. | | `DED` | `deductible` | A member ID ending in DED is accepted and paid, but most of what the payer allows is applied to the patient’s deductible (PR 1), so the payer pays only the rest, the patient owes the deductible, and the remainder of the charge is a contractual write-off (CO 45). | | `COPAY` | `copay` | A member ID ending in COPAY is accepted and paid less a fixed copay the patient owes (PR 3), and the remainder of the charge is a contractual write-off (CO 45). | | `D16` | `lacks_information` | A member ID ending in D16 is accepted and then refused on the 835 for missing information: nothing is paid, every line is adjusted under CO 16 with remark M51 (a procedure code is missing or invalid), and the claim status code says processed, so only the adjustments show the refusal. | | `D97` | `bundled` | A member ID ending in D97 is paid except for its last line, which the payer bundles into another service (CO 97, remark M15) and pays nothing for. A claim of one line has that line bundled, so nothing is paid. | | `REV` | `reversed_and_repaid` | A member ID ending in REV comes back as a correction: the 835 first reverses an earlier payment on the claim (claim status 22, every amount negated), then pays the claim again on a higher allowed amount, so the check carries only the difference. | | `PLB` | `provider_adjustment` | A member ID ending in PLB is paid, and its check also carries a provider-level adjustment: the payer recovers an overpayment on an earlier claim (PLB reason WO), so the check is less than the claims it pays. | | `XOVER` | `forwarded` | A member ID ending in XOVER is paid as primary and forwarded to the patient’s secondary payer (claim status 19, the secondary named as the crossover payer, remark MA18), so it is not filed to the secondary by hand. | | `RFAI` | `information_requested` | A member ID ending in RFAI is accepted on the 277CA and then held for a document: the payer sends a 277 request for additional information asking for a progress note by a day thirty days later, and pays nothing until a solicited 275 answers it. Send the same claims again, to the same sandbox route (professional, institutional or dental), with that 275 in attachments_275, and the claim is paid as any other. | | `` | `paid` | Any other member ID is accepted and paid: the payer allows part of the charge, the patient owes coinsurance on what was allowed (PR 2), the rest is a contractual write-off (CO 45), and every service line carries its share to the cent. | ### The file kinds POST /v1/x12/parse names `270`, `271`, `276`, `277`, `277CA`, `277RFAI`, `278`, `835`, `837P`, `837I`, `837D`, `999`, `997`, `TA1`, `mixed`, `unknown` ### The sandbox timeline’s stages | Stage | After | File | Event | | --- | --- | --- | --- | | `file_acknowledged` | 5 minutes | 999 | `sandbox.file.acknowledged` | | `claims_received` | 1 hours | 277CA | `sandbox.claim.acknowledged` | | `payer_acknowledged` | 2 days | 277CA | `sandbox.claim.acknowledged` | | `remittance` | 14 days | 835 | `sandbox.remittance.received` | ### The five gates on a real send Payerworks is connected to no payer. | Gate | What it says while closed | | --- | --- | | `service` | Sending claims is not switched on for Payerworks yet. | | `live_key` | A test key never sends a claim. Run the whole loop with test data on POST /v1/sandbox/claims, or use a live key. | | `baa` | No signed business associate agreement is recorded for this account. Payerworks records one with you before any claim is sent. | | `partner` | No trading partner Payerworks sends to takes claims for this payer yet. | | `partner_switch` | The trading partner for this payer is set up, but sending to it is not switched on yet. | ## Example [claims.mjs](https://payerworks.com/docs/examples/claims.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. claims.mjs ```js // Payerworks example: claims, checked and written as an 837P. Docs: https://payerworks.com/docs/claims // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node claims.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { CLAIM, SENDER_ID, PAYER_ID, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Check the claim the way a payer's front end would. const checked = await pw.postClaimsValidate({ claim: copy(CLAIM) }); // 2. Write it as an 837P. Nothing is written when any claim would be refused. const written = await pw.postClaims837p({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 7, claims: [copy(CLAIM)] }); // 3. Read the file back to the same JSON. const read = await pw.postX12Parse({ x12: written.x12 }); // 4. The failure path: an NPI that fails its check digit and a charge that is not its lines' total. const broken = copy(CLAIM); broken.billing.npi = '1234567890'; broken.claimInformation.claimChargeAmount = '300.00'; const refused = await refusalOf(() => pw.postClaims837p({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 8, claims: [broken] }), { PayerworksError }); // The same claim on validate is answered, with every reason listed. const reasons = (await pw.postClaimsValidate({ claim: broken })).claims[0].errors.map((e) => e.code); // 5. Sending through a trading partner is shut while Payerworks is connected to no payer: the answer // names each of the five gates and whether it is open, and nothing is written or sent. const partners = await pw.getPartners(); const shut = await refusalOf(() => pw.postClaimsSubmit({ claim: copy(CLAIM) }), { PayerworksError }); return { valid: checked.valid, written: { transmitted: written.transmitted, control: written.interchange_control_number }, read_kind: read.kind, refused, reasons, connection: partners.connection, submit: shut, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "valid": true, "written": { "transmitted": false, "control": "000000007" }, "read_kind": "837P", "refused": { "status": 422, "code": "invalid_claims", "request_id": "req_4" }, "reasons": [ "invalid_npi", "charge_does_not_balance" ], "connection": "Payerworks is connected to no payer.", "submit": { "status": 501, "code": "submit_not_offered", "request_id": "req_7" } } ``` # Acknowledgments > What the front end said about your file and each claim in it, as the part of your data to correct, and where each claim stands. Page: https://payerworks.com/docs/acknowledgments · Markdown: https://payerworks.com/docs/acknowledgments.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. A 999 says whether the receiver could read your file; a 277CA says, claim by claim, whether the payer’s front end accepted it. POST /v1/acknowledgments/explain says every rejection in either as the fix: the segment, the loop, the element, and the part of your claim data to correct, matched to your claim reference. POST /v1/claims/track takes the claims and every 999, 277CA and 835 you hold for them, and answers where each claim stands, who said so, what was paid, and a dated timeline of every file. It reads only what you send: nothing is stored and no payer is asked. A payer’s 277 request for additional information may ride with the acknowledgments: a request no 275 answers holds the claim as `information_requested`, with its due day. Send your solicited 275s as `attachments` to see whether they answer it. New here? Start with [A sandbox claim, to its 835](https://payerworks.com/docs/quickstart-sandbox-claim). ## Routes - [POST /v1/acknowledgments/explain](#post-v1-acknowledgments-explain): Explain an acknowledgment - [POST /v1/claims/track](#post-v1-claims-track): Track claims Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/acknowledgments/explain **Explain an acknowledgment.** Every rejection in a 999 or a 277CA said as the fix: the segment, the loop, the element, and the part of your claim data to correct, matched to your claim reference. A TA1 is said as its accept or reject and its note code, in words. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/acknowledgments/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAcknowledgmentsExplain(body); ``` With the Python client ```python answer = pw.post_acknowledgments_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000081*0*T*:~\nGS*HN*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*81*X*005010X214~\nST*277*0001*005010X214~\nBHT*0085*08*B1*20261018*0900*TH~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****46*99999~\nTRN*1*B1~\nHL*2*1*21*1~\nNM1*41*2*SAMPLE SLEEP DENTISTRY*****46*SAMPLEBILLER~\nTRN*2*B1~\nHL*3*2*19*1~\nNM1*85*2*SAMPLE SLEEP DENTISTRY*****XX*1234567893~\nHL*4*3*PT~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nTRN*2*PW-1004~\nSTC*A7:562:85*20261018*U*2900~\nDTP*472*D8*20261001~\nHL*5*3*PT~\nNM1*QC*1*PATIENT*SECOND****MI*W987654321~\nTRN*2*PW-1005~\nSTC*A2:20*20261018*WQ*350~\nREF*1K*PAYER-CN-5~\nDTP*472*D8*20261002~\nSE*22*0001~\nGE*1*81~\nIEA*1*000000081~" } ``` The answer (200) ```json { "kind": "277CA", "batch": { "trace": "B1", "statuses": [], "accepted": null, "rejected": null }, "claims": [ { "reference": "PW-1004", "payer_claim_number": "", "verdict": "R", "fixes": [ { "category": "A7", "code": "562", "entity": "85", "verdict": "R", "where": "the billing provider in your claim data", "sentence": "The claim was rejected for invalid information: the billing provider's NPI is missing or invalid (STC A7:562:85). Fix it on the billing provider in your claim data, then send the claim again." } ] }, { "reference": "PW-1005", "payer_claim_number": "PAYER-CN-5", "verdict": "A", "fixes": [ { "category": "A2", "code": "20", "entity": "", "verdict": "A", "where": "", "sentence": "The claim was accepted into the payer's adjudication system: accepted for processing (STC A2:20). Nothing to fix." } ] } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `kind` | one of "999", "277CA", "TA1" | | | `interchanges` | list of Ta1Read | A TA1: each one, in words. | | `source` | object | A TA1: where the note codes’ words come from, and the day read. | | `groups` | list of object | A 999: each functional group and set. | | `batch` | object | A 277CA: the batch’s own answer. | | `claims` | list of object | A 277CA: each claim. | ### POST /v1/claims/track **Track claims.** Send the claims (as JSON or the 837P) and the 999s, 277CAs and 835s you hold for them. Each claim comes back with where it stands (submitted, received, accepted, rejected, processed, denied or unknown), who said so, what was paid, and a dated timeline of every file. It reads only what you send: nothing is stored and no payer is asked. A payer’s 277 request for additional information may ride with the acknowledgments: a request no 275 answers holds the claim as information requested, with its due day; send your solicited 275s as attachments to see whether they answer it. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/track" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsTrack(body); ``` With the Python client ```python answer = pw.post_claims_track(body) ``` request.json ```json { "x12_837": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*1500*1*X*005010X222A1~ST*837*0001*005010X222A1~BHT*0019*00*PW-SBX-PAID*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W123456789~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-PAID*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0001~GE*1*1~IEA*1*000000001~", "acknowledgments": [ "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*1500*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~IK5*A~AK9*A*1*1*1~SE*6*0001~GE*1*1~IEA*1*000000001~", "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1500*TH~HL*1**20*1~NM1*PR*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~TRN*1*PWSANDBOX2~DTP*050*D8*20261019~DTP*009*D8*20261019~HL*2*1*21*1~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~TRN*2*000000001~STC*A1:19:PR*20261019*WQ*2900~QTY*90*1~AMT*YU*2900~HL*3*2*19*1~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~TRN*1*0~HL*4*3*PT~NM1*QC*1*Patient*Sample****MI*W123456789~TRN*2*PW-SBX-PAID~STC*A2:20*20261019*WQ*2900~REF*1K*PWS2292711347~DTP*472*D8*20261001~SE*23*0001~GE*1*2~IEA*1*000000002~" ], "remittances": [ "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000003*0*T*:~GS*HP*PWSANDBOX*SAMPLEBILLER*20261019*1500*3*X*005010X221A1~ST*835*0001~BPR*I*1392*C*CHK************20261019~TRN*1*PWSANDBOX-000000003-1*1PWSANDBOX~DTM*405*20261019~N1*PR*PAYERWORKS SANDBOX PAYER*XV*PWSANDBOX~REF*2U*PWSANDBOX~N1*PE*Sample Sleep Dentistry*XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*TJ*123456789~LX*1~CLP*PW-SBX-PAID*1*2900*1392*348*CI*PWS2292711347*11*1~NM1*QC*1*Patient*Sample****MI*W123456789~SVC*HC:E0486:KX*2900*1392**1~DTM*472*20261001~CAS*CO*45*1160~CAS*PR*2*348~AMT*B6*1740~SE*19*0001~GE*1*3~IEA*1*000000003~" ] } ``` The answer (200) ```json { "as_of": null, "reads": "Only the files sent in this call: nothing is stored, and no payer is asked.", "claims": [ { "index": 0, "reference": "PW-SBX-PAID", "references": [ "PW-SBX-PAID" ], "state": "processed", "meaning": "Adjudicated: the payer accepted responsibility for the claim. That is not the same as paid in full, or paid at all.", "next_step": "Read the remittance for the amount paid, the patient's share and each adjustment (POST /v1/remittances/explain).", "status_reported_by": "payer", "since": "2026-10-19", "days_in_state": 0, "decided_by": { "timeline_index": 3, "source": "remittances[0]", "kind": "835" }, "paid_total": 1392, "paid_total_cents": 139200, "has_reversals": false, "crossover": null, "payer_claim_numbers": [ "PWS2292711347" ], "submissions": [ { "index": 0, "source": "x12_837[0]", "reference": "PW-SBX-PAID", "frequency_code": "1", "replaces_payer_claim_number": null, "submitted_on": "2026-10-19", "batch_id": "PW-SBX-PAID", "charge": 2900, "service_date": "2026-10-01", "service_date_end": null, "member_id": "W123456789", "payer_id": "99999", "latest": true } ], "lines": { "submitted": [ { "index": 0, "line_control_number": null, "procedure_code": "E0486", "charge": 2900, "service_date": "2026-10-01", "acknowledgment_statuses": [], "payments": [] } ], "remittance_lines_not_matched": [ { "source": "remittances[0]", "payer_claim_number": "PWS2292711347", "clp_status": "1", "procedure_code": "E0486", "billed": 2900, "paid": 1392, "allowed": 1740, "line_control_number": null } ] }, "timeline": [ { "at": "2026-10-19", "source": "x12_837[0]", "kind": "837P", "event": "submitted", "reported_by": null, "words": "Submitted as an original claim.", "submission": 0, "counts_for_state": true, "not_counted_because": null, "matched_by": null, "detail": { "reference": "PW-SBX-PAID", "frequency_code": "1", "replaces_payer_claim_number": null, "batch_id": "PW-SBX-PAID", "charge": 2900 } }, { "at": "2026-10-19", "source": "acknowledgments[0]", "kind": "999", "submission": 0, "counts_for_state": true, "not_counted_because": null, "matched_by": "control_numbers", "event": "file_accepted", "reported_by": "unknown", "words": "The receiver read the transaction set that carried this claim (999 IK5 A). That says the file was taken, not that the claim was accepted: the claim acknowledgment (a 277CA) says that.", "detail": { "group_control_number": "1", "transaction_set_control_number": "0001", "verdict": "A" } } ], "correlation": { "matched_by": [ "control_numbers", "reference" ], "split_payer_claim_numbers": null, "notes": [] } } ], "unmatched": [], "duplicate_remittances": [] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `as_of` | string or null | | | `reads` | string | | | `claims` | list of TrackClaim | | | `unmatched` | list of object | File by file, each item nothing matched, with why. | | `duplicate_remittances` | list of object | An 835 sent twice (the same TRN02 and TRN03): counted once, named here. | | `attachments` | list of object | Each 275 sent, and the claims whose requests it answers. | | `loinc_notice` | string | Only when a 277 request for additional information is among the files. | ## Reading the answer - Each claim in an explain answer has `fixes`, each a `sentence` naming the field of your claim data to correct. - A tracked claim’s `state` is one of the states listed below, with its `meaning`, its `next_step`, who reported it (`status_reported_by`) and the `timeline` of every file that led there. A receipt is never an acceptance, and a claim with no answer among your files is `submitted`, never assumed accepted. ## Troubleshooting - `not_an_acknowledgment` (422): This route reads 999s, 277CAs and TA1s. A claim status 277 (005010X212) is refused here, naming the route that reads it. - `not_read_here` (422): A 997 is recognized and named (`kind: "997"`), but not read. - `mixed_functional_groups` (422): One interchange that holds two kinds of file is refused whole, naming both; split it and send each. - `too_much_to_track` (413): Tracking reads at most what one call can hold; the error names what was counted and the limit. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `ambiguous_claims` | 400 | Send the claims once: as `claims` or as `x12_837`, not both. | | `invalid_track_request` | 400 | A field of the request is not in the shape it takes; each problem is listed with its field. | | `no_claims_to_track` | 400 | Send the claims to track as `claims` (a list) or `x12_837` (an 837P interchange, or a list of them). | | `no_x12` | 400 | Send the file's text as `x12`. | | `too_much_to_track` | 413 | More was sent than one call tracks: the error names what was counted (`unit`) and the most one call takes (`limit`). Send fewer claims or files a call. | | `invalid_tracked_claims` | 422 | One or more claims cannot be tracked; nothing was read. Each problem is listed with its field. | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | | `not_a_tracked_file` | 422 | A file is not a kind its list takes: x12_837 holds 837P claims, acknowledgments holds 999s, 277CAs and 277 requests for additional information, remittances holds 835s, attachments holds solicited 275s. The error names the file and its kind. | | `not_an_acknowledgment` | 422 | This is not a 999, a 277CA or a TA1. | | `not_read_here` | 422 | This file is a kind Payerworks recognizes but does not read here yet; the error names its kind. | ## Code lists ### Where a tracked claim stands | State | What it means | The next step | | --- | --- | --- | | `submitted` | Sent, and no acknowledgment of this submission is among the files you sent. | Wait for the claim acknowledgment (a 277CA) from the receiver or the payer, then track the claim again with it. | | `received` | Received, and not yet accepted for adjudication: a receipt is not an acceptance. | Wait for the payer to accept or reject it. Some payers never send an explicit acceptance, so a claim can stay received; if it does, ask the payer with a claim status inquiry (POST /v1/claim-status/276 writes one). | | `accepted` | Accepted into the payer's adjudication system, and being processed. | Wait for the remittance (an 835). A claim status inquiry says where it stands in the meantime. | | `information_requested` | The payer asked for more information before it decides the claim (a 277 request for additional information), and no 275 among the files you sent answers it. | Send what the payer asked for as a solicited 275 by its due day (POST /v1/attachments/requests/explain lays it out, POST /v1/attachments/275 writes it, POST /v1/attachments/requests/check checks it), then track the claim again with that 275 under attachments. | | `rejected` | Rejected before adjudication: the payer never decided it, and no remittance follows a rejection. | Fix what the acknowledgment names (POST /v1/acknowledgments/explain says each fix) and send the claim again as a new submission. | | `processed` | Adjudicated: the payer accepted responsibility for the claim. That is not the same as paid in full, or paid at all. | Read the remittance for the amount paid, the patient's share and each adjustment (POST /v1/remittances/explain). | | `denied` | Adjudicated and denied: the payer decided it is not responsible for the claim. | Read why and what to do next (POST /v1/denials/next-step): correct it and send a replacement claim, or appeal. | | `unknown` | No single state can be read from the files: the answers are mixed or incomplete, or use codes a claim acknowledgment does not carry. | Read the timeline, and ask the payer with a claim status inquiry before you act. | ## Example [acknowledgments.mjs](https://payerworks.com/docs/examples/acknowledgments.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. acknowledgments.mjs ```js // Payerworks example: acknowledgments, every rejection said as the fix. Docs: https://payerworks.com/docs/acknowledgments // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node acknowledgments.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Two claims through the sandbox payer: one paid, one rejected at its front end (a member ID ending in REJ). const claims = [claimEndingIn(''), claimEndingIn('REJ')]; const run1 = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims }); // 2. Explain the 277CA it answered: each rejection as the part of your claim data to correct. const ack = await pw.postAcknowledgmentsExplain({ x12: run1.files.claim_acknowledgment_277ca }); // A rejected claim's verdict is R, the 277CA's own letter. const rejected = ack.claims.find((c) => c.verdict === 'R'); // 3. Track both claims from the files you hold: where each stands, and who said so. const tracked = await pw.postClaimsTrack({ claims, acknowledgments: [run1.files.acknowledgment_999, run1.files.claim_acknowledgment_277ca], remittances: [run1.files.remittance_835], }); // 4. The failure path: a remittance is not an acknowledgment, and the route says which route reads it. const refused = await refusalOf(() => pw.postAcknowledgmentsExplain({ x12: run1.files.remittance_835 }), { PayerworksError }); return { rejected: { reference: rejected?.reference, fix: rejected?.fixes[0]?.sentence }, states: Object.fromEntries(tracked.claims.map((c) => [c.reference, c.state])), refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "rejected": { "reference": "EX-REJ", "fix": "The claim was rejected for invalid information: the subscriber's contract or member number is missing or invalid (STC A7:164:IL). Fix it on the subscriber in your claim data, then send the claim again." }, "states": { "EX-PAID": "processed", "EX-REJ": "rejected" }, "refused": { "status": 422, "code": "not_an_acknowledgment", "request_id": "req_4" } } ``` # Remittances and denials > What the payer paid and why it did not, said claim by claim, with the next step on every denial. Page: https://payerworks.com/docs/remittances · Markdown: https://payerworks.com/docs/remittances.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. An 835 is the payer’s remittance: what it paid, allowed and adjusted, claim by claim and line by line. POST /v1/remittances/explain reads it check by check, says each claim’s verdict in words, and checks that each check adds up to the claims it pays. POST /v1/denials/next-step says each adjustment as what the payer did, whether the patient may be billed, and the next step, with every deadline and where it comes from. POST /v1/appeals/letter writes an appeal letter from the facts you send, by a fixed template, and never adds a code, an amount or a date you did not give. The routes after it work across several 835s: duplicates, deposits matched to checks, recoupments matched to the claims they take back, and an 835 split by payee or rendering provider. New here? Start with [A sandbox claim, to its 835](https://payerworks.com/docs/quickstart-sandbox-claim). ## Routes - [POST /v1/remittances/explain](#post-v1-remittances-explain): Explain a remittance - [POST /v1/denials/next-step](#post-v1-denials-next-step): The next step on a denial - [POST /v1/appeals/letter](#post-v1-appeals-letter): Write an appeal letter - [POST /v1/appeals/letter/check](#post-v1-appeals-letter-check): Check an edited letter - [GET /v1/codes/carc/{code}](#get-v1-codes-carc-code): An adjustment reason code - [GET /v1/codes/group/{code}](#get-v1-codes-group-code): An adjustment group code - [POST /v1/remittances/duplicates](#post-v1-remittances-duplicates): Find duplicate remittances - [POST /v1/remittances/reassociate](#post-v1-remittances-reassociate): Match deposits to remittances - [POST /v1/remittances/split](#post-v1-remittances-split): Split an 835 by payee or rendering provider - [POST /v1/remittances/recoupments](#post-v1-remittances-recoupments): Match recoupments to the claims they take back - [GET /v1/filing-limits](#get-v1-filing-limits): Published claim filing windows - [GET /v1/filing-limits/{payer}](#get-v1-filing-limits-payer): One payer’s filing window Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/remittances/explain **Explain a remittance.** An 835 claim by claim: what was paid, allowed and written off, the patient’s share, the verdict in words, each service line, every adjustment and remark code in words, and whether each check adds up to the claims it pays. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesExplain(body); ``` With the Python client ```python answer = pw.post_remittances_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~" } ``` The answer (200) ```json { "check": { "number": "EFT-20261018-01", "date": "20261018", "amount": "1260", "method_flag": "C", "payer": "SAMPLE HEALTH PLAN", "payee_ids": { "npis": [ "1234567893" ], "tins": [] } }, "checks": [ { "number": "EFT-20261018-01", "date": "20261018", "amount": "1260", "method_flag": "C", "payer": "SAMPLE HEALTH PLAN", "payee_ids": { "npis": [ "1234567893" ], "tins": [] }, "references": [ "PW-1001", "PW-1002" ], "balance": { "stated": 1260, "claims_net": 1260, "provider_adjustments_net": 0, "variance": 0, "ties": true, "words": "The claims, less the provider-level adjustments, add up to the check." }, "provider_adjustments": [] } ], "claims": [ { "reference": "PW-1001", "payer_claim_number": "PAYER-CN-1", "status_code": "1", "status": "processed as primary", "service_date": "20261001", "billed": 2900, "paid": 1260, "allowed": 1500, "patient_responsibility": 240, "amounts": { "billed": 2900, "paid": 1260, "allowed": 1500, "allowed_basis": "stated", "patient_responsibility": 240, "patient_basis": "stated", "adjustment": 1400, "adjustment_basis": "stated", "write_off": 1400, "below_allowed": 0, "not_paid": 0, "shortfall": 0 }, "adjustments": [ { "group": "CO", "code": "45", "amount": 1400, "label": "Charge exceeds the fee schedule / contracted amount", "category": "Underpayment", "known": true, "label_source": "paraphrase", "patient_billable": false } ], "patient_adjustments": [ { "group": "PR", "code": "2", "amount": 240, "label": "Coinsurance amount", "category": "Other", "known": true, "label_source": "paraphrase", "patient_billable": true } ], "remark_codes": [], "primary_reason": null, "crossover_carrier": "", "service_lines": [ { "procedure_code": "E0486", "modifiers": [], "billed": 2900, "paid": 1260, "allowed": 1500 } ], "check_number": "EFT-20261018-01", "index": 0, "claim_filing_indicator": "12", "verdict": "paid", "verdict_words": "Paid: the payer paid $1,260.00, the patient owes $240.00 (patient responsibility), and $1,400.00 is a contractual adjustment.", "remarks": [], "claim_adjustments": [ { "group": "CO", "code": "45", "amount": 1400 }, { "group": "PR", "code": "2", "amount": 240 } ], "lines": [ { "index": 0, "procedure_code": "E0486", "procedure_qualifier": "HC", "modifiers": [], "billed": 2900, "paid": 1260, "allowed": 1500, "units_paid": null, "service_date": null, "service_date_end": null, "line_control_number": "", "adjustments": [], "remarks": [] } ] }, { "reference": "PW-1002", "payer_claim_number": "PAYER-CN-2", "status_code": "4", "status": "denied", "service_date": "20261002", "billed": 350, "paid": 0, "allowed": null, "patient_responsibility": 0, "amounts": { "billed": 350, "paid": 0, "allowed": null, "allowed_basis": null, "patient_responsibility": 0, "patient_basis": "stated", "adjustment": 350, "adjustment_basis": "stated", "write_off": 0, "below_allowed": 0, "not_paid": 350, "shortfall": null }, "adjustments": [ { "group": "CO", "code": "50", "amount": 350, "label": "Non-covered: not deemed a medical necessity", "category": "Medical Necessity", "known": true, "label_source": "paraphrase", "patient_billable": false } ], "patient_adjustments": [], "remark_codes": [ "N115" ], "primary_reason": { "code": "50", "category": "Medical Necessity", "words": "50, Non-covered: not deemed a medical necessity" }, "crossover_carrier": "", "service_lines": [ { "procedure_code": "95800", "modifiers": [], "billed": 350, "paid": 0, "allowed": null } ], "check_number": "EFT-20261018-01", "index": 1, "claim_filing_indicator": "12", "verdict": "denied", "verdict_words": "Refused: the payer paid nothing. The refusing adjustment is CO-50 (Non-covered: not deemed a medical necessity).", "remarks": [ { "code": "N115", "words": "The decision was based on a Local Coverage Determination (LCD).", "label_source": "paraphrase" } ], "claim_adjustments": [ { "group": "CO", "code": "50", "amount": 350 } ], "lines": [ { "index": 0, "procedure_code": "95800", "procedure_qualifier": "HC", "modifiers": [], "billed": 350, "paid": 0, "allowed": null, "units_paid": null, "service_date": null, "service_date_end": null, "line_control_number": "", "adjustments": [], "remarks": [ { "code": "N115", "words": "The decision was based on a Local Coverage Determination (LCD).", "label_source": "paraphrase" } ] } ] } ], "provider_adjustments": [], "groups": { "CO": { "name": "Contractual obligation", "patient_billable": false, "words": "An adjustment your agreement with the payer, or a regulation, requires. It is generally a write-off for you and is not billed to the patient." }, "OA": { "name": "Other adjustment", "patient_billable": "depends", "words": "Used when no other group applies, such as the effect of an earlier payer's payment. Whether the patient may be billed depends on the reason code beside it and on your agreement with the plan." }, "PI": { "name": "Payer-initiated reduction", "patient_billable": false, "words": "A reduction the payer made itself. It is not a patient responsibility amount, so it is not billed to the patient." }, "PR": { "name": "Patient responsibility", "patient_billable": true, "words": "An amount that may be billed to the patient, such as the deductible, coinsurance or copay." } } } ``` | Field | Type | What it is | | --- | --- | --- | | `check` | Check | | | `checks` | list of Check | One per check: an 835 pays each payee by its own. | | `claims` | list of RemittanceClaim | | | `provider_adjustments` | list of object | | | `groups` | Groups | Each adjustment group code the answer names (CO, OA, PI, PR), said once: its name, whether the patient may be billed, and its words. An adjustment carries its group code only. | ### POST /v1/denials/next-step **The next step on a denial.** An 835, or the claims as JSON: for each claim and each adjustment, what the payer did, whether the patient may be billed, the next step, the corrected claim fields to send, what to attach, and every deadline with where it comes from: a Medicare regulation with its section, the limits you send, or no date at all. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/denials/next-step" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postDenialsNextStep(body); ``` With the Python client ```python answer = pw.post_denials_next_step(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000093*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*93*X*005010X221A1~\nST*835*0001~\nBPR*I*80*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-02*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*XX*1234567893~\nLX*1~\nCLP*PW-2003*1*215*0**12*PAYER-CN-23*11~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nDTM*232*20261001~\nSVC*HC:99213*150*0~\nDTM*472*20261001~\nCAS*CO*16*150~\nLQ*HE*N290~\nSVC*HC:87880*65*0~\nDTM*472*20261001~\nCAS*CO*16*65~\nLQ*HE*N290~\nCLP*PW-2004*1*350*0**MB*PAYER-CN-24*11~\nNM1*QC*1*PATIENT*SECOND****MI*1EG4TE5MK73~\nDTM*232*20261002~\nSVC*HC:99214*350*0~\nDTM*472*20261002~\nCAS*CO*50*350~\nLQ*HE*N115~\nCLP*PW-2005*1*240*80*100*12*PAYER-CN-25*11~\nNM1*QC*1*PATIENT*THIRD****MI*W555555555~\nDTM*232*20261003~\nSVC*HC:99213*240*80~\nDTM*472*20261003~\nCAS*CO*45*60~\nCAS*PR*1*100~\nAMT*B6*180~\nCLP*PW-2006*1*150*0**12*PAYER-CN-26*11~\nNM1*QC*1*PATIENT*FOURTH****MI*W666666666~\nDTM*232*20260101~\nSVC*HC:99213*150*0~\nDTM*472*20260101~\nCAS*CO*29*150~\nSE*39*0001~\nGE*1*93~\nIEA*1*000000093~" } ``` The answer (200) ```json { "source": "x12", "remittance_date": "2026-10-18", "today": "2026-10-19", "claims": [ { "index": 0, "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "status_code": "1", "status": "processed as primary", "claim_filing_indicator": "12", "medicare": null, "service_date": "2026-10-01", "remittance_date": "2026-10-18", "notice_date": "2026-10-18", "notice_date_source": "bpr_16", "billed": 215, "paid": 0, "allowed": null, "patient_responsibility": null, "verdict": "denied", "verdict_words": "Refused: the payer paid nothing. The refusing adjustment is CO-16 (Claim lacks information or has a submission/billing error). The claim status code says processed as primary, but the adjustments refused the money.", "patient": { "may_bill": 0, "words": "Nothing on this remittance is the patient's to pay. The $215.00 under group CO (contractual) may not be billed to the patient." }, "adjustments": [ { "group": "CO", "code": "16", "amount": 150, "label": "Claim lacks information or has a submission/billing error", "category": "Missing Information", "known": true, "label_source": "paraphrase", "patient_billable": false, "line": 0, "remedy": { "key": "correct", "action": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "is_letter": false, "letter_kind": null } }, { "group": "CO", "code": "16", "amount": 65, "label": "Claim lacks information or has a submission/billing error", "category": "Missing Information", "known": true, "label_source": "paraphrase", "patient_billable": false, "line": 1, "remedy": { "key": "correct", "action": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "is_letter": false, "letter_kind": null } } ], "remarks": [ { "code": "N290", "words": "The rendering provider's primary identifier is missing, incomplete or invalid.", "label_source": "paraphrase" }, { "code": "N290", "words": "The rendering provider's primary identifier is missing, incomplete or invalid.", "label_source": "paraphrase" } ], "next_step": { "key": "correct", "action": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "is_letter": false, "letter_kind": null }, "corrected_claim": { "frequency_code": "7", "void_frequency_code": "8", "claim_control_number": "PAYER-CN-23", "fields": { "claimInformation.claimFrequencyCode": "7", "claimInformation.claimSupplementalInformation.claimControlNumber": "PAYER-CN-23" }, "words": "Send a replacement claim through POST /v1/claims/837p with claimInformation.claimFrequencyCode \"7\" and claimInformation.claimSupplementalInformation.claimControlNumber \"PAYER-CN-23\" (the payer's claim number); it is written as CLM05-3 7 and…" }, "send": [], "deadlines": [ { "kind": "timely_filing", "date": null, "date_us": null, "days_left": null, "window": null, "counted_from": "the date of service", "source": { "kind": "none" }, "words": "The payer's filing limit is stated in its contract or provider manual. Send facts.filing_limit_days to have the date computed." } ] }, { "index": 1, "reference": "PW-2004", "payer_claim_number": "PAYER-CN-24", "status_code": "1", "status": "processed as primary", "claim_filing_indicator": "MB", "medicare": "fee_for_service", "service_date": "2026-10-02", "remittance_date": "2026-10-18", "notice_date": "2026-10-18", "notice_date_source": "bpr_16", "billed": 350, "paid": 0, "allowed": null, "patient_responsibility": null, "verdict": "denied", "verdict_words": "Refused: the payer paid nothing. The refusing adjustment is CO-50 (Non-covered: not deemed a medical necessity). The claim status code says processed as primary, but the adjustments refused the money.", "patient": { "may_bill": 0, "words": "Nothing on this remittance is the patient's to pay. The $350.00 under group CO (contractual) may not be billed to the patient." }, "adjustments": [ { "group": "CO", "code": "50", "amount": 350, "label": "Non-covered: not deemed a medical necessity", "category": "Medical Necessity", "known": true, "label_source": "paraphrase", "patient_billable": false, "line": 0, "remedy": { "key": "necessity", "action": "appeal", "label": "Appeal with the clinical records", "words": "The payer says the service was not medically necessary. Appeal with the clinical records that support it, the ones the payer's coverage policy asks for.", "is_letter": true, "letter_kind": "medical_necessity" } } ], "remarks": [ { "code": "N115", "words": "The decision was based on a Local Coverage Determination (LCD).", "label_source": "paraphrase" } ], "next_step": { "key": "necessity", "action": "appeal", "label": "Appeal with the clinical records", "words": "The payer says the service was not medically necessary. Appeal with the clinical records that support it, the ones the payer's coverage policy asks for.", "is_letter": true, "letter_kind": "medical_necessity", "letter_facts": { "payer_name": "SAMPLE HEALTH PLAN", "patient_name": "SECOND PATIENT", "payer_claim_number": "PAYER-CN-24", "patient_control_number": "PW-2004", "service_date": "2026-10-02", "billed_amount": 350, "remittance_date": "2026-10-18", "adjustment_codes": [ "CO-50" ], "remark_codes": [ "N115" ], "claim_filing_indicator": "MB" }, "letter_facts_missing": [ "member_id", "provider_name" ] }, "corrected_claim": null, "send": [], "deadlines": [ { "kind": "redetermination", "date": "2027-02-20", "date_us": "02/20/2027", "days_left": 124, "window": { "days": 120, "presumed_receipt_days": 5 }, "counted_from": "receipt of the initial determination", "counted_from_date": "2026-10-18", "counted_from_date_source": "bpr_16", "source": { "kind": "regulation", "citation": "42 CFR 405.942(a)", "url": "https://www.ecfr.gov/current/title-42/section-405.942", "verified_on": "10/19/2026", "presumed_receipt_citation": "42 CFR 405.942(a)(1)" }, "words": "A redetermination (the first level of appeal) is due within 120 days of receiving the initial determination, and receipt is presumed 5 days after the notice's date: 02/20/2027, counted from the payment date (BPR16), as the remittance advice…" }, { "kind": "reconsideration", "date": null, "date_us": null, "days_left": null, "window": { "days": 180, "presumed_receipt_days": 5 }, "counted_from": "receipt of the redetermination decision", "source": { "kind": "regulation", "citation": "42 CFR 405.962(a)", "url": "https://www.ecfr.gov/current/title-42/section-405.962", "verified_on": "10/19/2026", "presumed_receipt_citation": "42 CFR 405.962(a)(1)" }, "words": "A later level: 180 days from receipt of the redetermination decision, receipt presumed 5 days after that decision's date (42 CFR 405.962(a); 42 CFR 405.962(a)(1)). Its date is known once that decision is." } ] } ], "groups": { "CO": { "name": "Contractual obligation", "patient_billable": false, "words": "An adjustment your agreement with the payer, or a regulation, requires. It is generally a write-off for you and is not billed to the patient." }, "OA": { "name": "Other adjustment", "patient_billable": "depends", "words": "Used when no other group applies, such as the effect of an earlier payer's payment. Whether the patient may be billed depends on the reason code beside it and on your agreement with the plan." }, "PI": { "name": "Payer-initiated reduction", "patient_billable": false, "words": "A reduction the payer made itself. It is not a patient responsibility amount, so it is not billed to the patient." }, "PR": { "name": "Patient responsibility", "patient_billable": true, "words": "An amount that may be billed to the patient, such as the deductible, coinsurance or copay." } }, "deadline_rule": "A deadline is computed only from a regulation it cites (Medicare fee-for-service, by its filing indicator MA or MB), or from the limits you send as facts; otherwise its date is null and its words say the payer's contract or provider manual …" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `source` | one of "x12", "claims" | | | `remittance_date` | string or null | | | `today` | string | | | `claims` | list of NextStepClaim | | | `groups` | Groups | Each adjustment group code the answer names (CO, OA, PI, PR), said once: its name, whether the patient may be billed, and its words. An adjustment carries its group code only. | | `deadline_rule` | string | | ### POST /v1/appeals/letter **Write an appeal letter.** An appeal letter written from the facts you send, by a fixed template for its kind. It never adds a code, an amount or a date you did not give. A fact the kind needs and you left out is named, and nothing is written. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/appeals/letter" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAppealsLetter(body); ``` With the Python client ```python answer = pw.post_appeals_letter(body) ``` request.json ```json { "kind": "timely_filing", "facts": { "payer_name": "Sample Health Plan", "patient_name": "Sample Patient", "member_id": "W666666666", "payer_claim_number": "PAYER-CN-26", "service_date": "2026-01-01", "provider_name": "Sample Family Medicine", "provider_npi": "1234567893", "signer_name": "Sam Sample", "original_filing_date": "2026-01-15", "proof_reference": "the clearinghouse acceptance report dated 01/16/2026", "filing_limit_days": 90 } } ``` The answer (200) ```json { "kind": "timely_filing", "subject": "Request for reconsideration: timely filing denial", "letter": "10/19/2026\n\nSample Health Plan\n\nRe: Request for reconsideration: timely filing denial\nPatient: Sample Patient\nMember ID: W666666666\nClaim number: PAYER-CN-26\nDate of service: 01/01/2026\n\nTo the appeals department:\n\nThis claim was denied as …", "facts_used": [ { "field": "payer_name", "label": "Payer", "value": "Sample Health Plan" }, { "field": "patient_name", "label": "Patient", "value": "Sample Patient" } ], "enclosures": [], "guard": { "passed": true }, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | string | | | `subject` | string | | | `letter` | string | The letter, as plain text. | | `facts_used` | list of FactUsed | | | `enclosures` | list of string | | | `guard` | LetterGuard | | | `transmitted` | the value false | Payerworks wrote the letter and sent it nowhere. | ### POST /v1/appeals/letter/check **Check an edited letter.** A letter you edited, held to the facts by the same rule: every code, amount and date it states, however it is written (a dollar sign or the word dollars, a written or numeric date, CO-50 or CARC 50, N115 or N 115), must be one the facts hold, and a placeholder left in is found. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/appeals/letter/check" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAppealsLetterCheck(body); ``` With the Python client ```python answer = pw.post_appeals_letter_check(body) ``` request.json ```json { "letter": "We ask that you reverse the denial of the claim for Sample Patient (CO-29), billed at $150.00.", "facts": { "patient_name": "Sample Patient", "adjustment_codes": [ "CO-29" ] } } ``` The answer (200) ```json { "guard": { "passed": false, "reason": "unknown_amount", "value": "$150.00", "words": "The letter states $150.00, which no fact holds. Add the amount to the facts, or take it out of the letter.", "findings": [ { "reason": "unknown_amount", "value": "$150.00", "words": "The letter states $150.00, which no fact holds. Add the amount to the facts, or take it out of the letter." } ] }, "facts_used": [ { "field": "patient_name", "label": "Patient", "value": "Sample Patient" }, { "field": "adjustment_codes", "label": "Adjustment codes", "value": "CO-29" } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `guard` | LetterGuard | | | `facts_used` | list of FactUsed | | ### GET /v1/codes/carc/{code} **An adjustment reason code.** A claim adjustment reason code in words, with its category, sent bare (45) or with its group (CO-45). A remark code sent here is named as one. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/carc/CO-16" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesCarcByCode("CO-16"); ``` With the Python client ```python answer = pw.get_codes_carc_by_code("CO-16") ``` The answer (200) ```json { "code": "16", "label": "Claim lacks information or has a submission/billing error", "category": "Missing Information", "known": true, "label_source": "paraphrase", "stopped_on": null, "group": { "code": "CO", "known": true, "name": "Contractual obligation", "words": "An adjustment your agreement with the payer, or a regulation, requires. It is generally a write-off for you and is not billed to the patient.", "patient_billable": false }, "source": { "list": "X12 claim adjustment reason codes", "url": "https://x12.org/codes/claim-adjustment-reason-codes", "read_on": "10/19/2026" } } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `label` | string | | | `category` | string | | | `known` | boolean | | | `label_source` | the value "paraphrase" | | | `stopped_on` | string or null | | | `group` | object or null | | | `source` | object | | ### GET /v1/codes/group/{code} **An adjustment group code.** A claim adjustment group code (CO, PR, OA, PI or CR) in words, and whether the patient may be billed an amount under it. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/group/PR" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getCodesGroupByCode("PR"); ``` With the Python client ```python answer = pw.get_codes_group_by_code("PR") ``` The answer (200) ```json { "code": "PR", "known": true, "name": "Patient responsibility", "words": "An amount that may be billed to the patient, such as the deductible, coinsurance or copay.", "patient_billable": true, "patient_billable_words": "The patient may be billed this amount.", "label_source": "paraphrase", "source": { "list": "X12 claim adjustment group codes", "url": "https://x12.org/codes/claim-adjustment-group-codes", "read_on": "10/19/2026" } } ``` | Field | Type | What it is | | --- | --- | --- | | `code` | string | | | `known` | boolean | | | `name` | string | | | `words` | string | | | `patient_billable` | boolean or string or null | true, false, "depends" or null. | | `patient_billable_words` | string | | | `label_source` | the value "paraphrase" | | | `source` | object | | | `valid_codes` | list of string | | ### POST /v1/remittances/duplicates **Find duplicate remittances.** Send several 835s and get back every check that is one payment sent more than once: the same trace number (TRN02) from the same originator (TRN03), never the trace alone. Each duplicate says whether the copies are the same remittance or differ in content, and a check with no trace is listed apart. Nothing is stored. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/duplicates" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesDuplicates(body); ``` With the Python client ```python answer = pw.post_remittances_duplicates(body) ``` request.json ```json { "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~", "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~" ] } ``` The answer (200) ```json { "files": 2, "checks": [ { "file": 0, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2, "duplicate_of": null }, { "file": 1, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2, "duplicate_of": { "file": 0, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2 } } ], "duplicates": [ { "trace": "EFT-20261018-01", "originator": "1234567890", "matched_on": [ "TRN02", "TRN03" ], "identical": true, "copies": [ { "file": 0, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2 }, { "file": 1, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2 } ], "words": "2 checks carry the same trace (TRN02 EFT-20261018-01) from the same originator (TRN03 1234567890), and their content is the same: one remittance sent twice. Post it once." } ], "no_trace": [], "words": "1 payment is in these files more than once, matched by trace (TRN02) and originator (TRN03).", "reads": "Only the files sent in this call: nothing is stored, and no payer or bank is asked." } ``` | Field | Type | What it is | | --- | --- | --- | | `files` | integer | | | `checks` | list of RemitCheck | Each check, with duplicate_of naming the first copy. | | `duplicates` | list of object | | | `no_trace` | list of object | | | `words` | string | | | `reads` | string | | ### POST /v1/remittances/reassociate **Match deposits to remittances.** Send 835s and the deposits from your bank (the amount and effective date, and the trace number or description when your bank gives them). Each deposit is matched to one check by its trace number first, then by a trace in the description, then by amount and date only when exactly one check and one deposit fit, and every match says what agreed and what did not. Unmatched deposits and checks are listed. No bank description is repeated back. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/reassociate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesReassociate(body); ``` With the Python client ```python answer = pw.post_remittances_reassociate(body) ``` request.json ```json { "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~", "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000093*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*93*X*005010X221A1~\nST*835*0001~\nBPR*I*80*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-02*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*XX*1234567893~\nLX*1~\nCLP*PW-2003*1*215*0**12*PAYER-CN-23*11~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nDTM*232*20261001~\nSVC*HC:99213*150*0~\nDTM*472*20261001~\nCAS*CO*16*150~\nLQ*HE*N290~\nSVC*HC:87880*65*0~\nDTM*472*20261001~\nCAS*CO*16*65~\nLQ*HE*N290~\nCLP*PW-2004*1*350*0**MB*PAYER-CN-24*11~\nNM1*QC*1*PATIENT*SECOND****MI*1EG4TE5MK73~\nDTM*232*20261002~\nSVC*HC:99214*350*0~\nDTM*472*20261002~\nCAS*CO*50*350~\nLQ*HE*N115~\nCLP*PW-2005*1*240*80*100*12*PAYER-CN-25*11~\nNM1*QC*1*PATIENT*THIRD****MI*W555555555~\nDTM*232*20261003~\nSVC*HC:99213*240*80~\nDTM*472*20261003~\nCAS*CO*45*60~\nCAS*PR*1*100~\nAMT*B6*180~\nCLP*PW-2006*1*150*0**12*PAYER-CN-26*11~\nNM1*QC*1*PATIENT*FOURTH****MI*W666666666~\nDTM*232*20260101~\nSVC*HC:99213*150*0~\nDTM*472*20260101~\nCAS*CO*29*150~\nSE*39*0001~\nGE*1*93~\nIEA*1*000000093~" ], "deposits": [ { "id": "bank-1", "amount": 1260, "effective_date": "2026-10-19", "trace_number": "EFT-20261018-01", "originator": "1234567890" }, { "id": "bank-2", "amount": 80, "effective_date": "2026-10-19", "description": "SAMPLE HEALTH PLAN HCCLAIMPMT EFT-20261018-02" } ] } ``` The answer (200) ```json { "matches": [ { "deposit": { "index": 0, "id": "bank-1", "amount": 1260, "effective_date": "2026-10-19" }, "check": { "file": 0, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2 }, "matched_by": "trace", "matched_on": [ "TRN02", "TRN03" ], "agrees": { "amount": true, "date": false, "originator": true, "weekdays_from_payment_date": 1 }, "words": "Matched by the trace number (TRN02): check EFT-20261018-01 of 10/18/2026 (file 1), agreeing on TRN02, TRN03, BPR02. But the date differs: the deposit settled 10/19/2026 and the check's payment date (BPR16) is 10/18/2026, 1 weekday apart." } ], "unmatched_deposits": [ { "deposit": { "index": 1, "id": "bank-2", "amount": 80, "effective_date": "2026-10-19" }, "words": "Not matched: the deposit carries no trace number, its description names none of the checks' traces, and no single check has its amount on its date." } ], "unmatched_checks": [ { "check": { "file": 1, "check": 0, "trace": "EFT-20261018-02", "originator": "1234567890", "amount": 80, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 4 }, "words": "Unmatched: no deposit among those sent matches this check. CAQH CORE 370 has the plan release the 835 within 3 business days of the EFT's effective entry date either way, so the money may not have settled yet." } ], "counts": { "checks": 2, "deposits": 2, "matched": 1 }, "words": "1 of 2 deposits matched to a check; 1 deposit and 1 check unmatched.", "how": "Matched by trace number first (TRN02, with TRN03 when the deposit names its originator), then by a trace printed in the bank's description (at least 6 characters, as a whole word), then by amount and date only where neither carries a trace …", "sources": [ { "name": "CMS, EFT and ERA: Payment Remittance Reassociation Basics (June 2022)", "url": "https://www.cms.gov/files/document/eft-and-era-payment-remittance-reassociation-basics.pdf", "read_on": "10/19/2026" }, { "name": "CAQH CORE, EFT and ERA Operating Rules Overview", "url": "https://www.caqh.org/sites/default/files/core/phase-iii/policy-rules/EFT-ERA_Rules_Overview.pdf", "read_on": "10/19/2026" } ], "reads": "Only the files and deposits sent in this call: nothing is stored, no bank description is repeated back, and no payer or bank is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `matches` | list of object | | | `unmatched_deposits` | list of object | | | `unmatched_checks` | list of object | | | `counts` | object | | | `words` | string | | | `how` | string | | | `sources` | list of object | | | `reads` | string | | ### POST /v1/remittances/split **Split an 835 by payee or rendering provider.** Send an 835 paid to one tax ID and get one file per payee (the provider the payer grouped the claims under) or per rendering NPI. Each file’s payment total is recomputed from its own claims and provider-level adjustments and read back to check it balances, the parts add up to the original check, and each keeps its trace. An adjustment that names no single group gets a file of its own, never a guessed one, and a value holding a delimiter is refused, never rewritten. Send the interchange control number you track for the first file (each further file takes the next): the original’s own number is never reused. The files are yours to load; nothing is sent. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/split" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesSplit(body); ``` With the Python client ```python answer = pw.post_remittances_split(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000094*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*94*X*005010X221A1~\nST*835*0001~\nBPR*I*330*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-03*1234567890~\nDTM*405*20261018~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*FI*123456789~\nLX*1~\nTS3*1234567893*11*20261231*1*240~\nCLP*PW-2101*1*240*180*60*12*PAYER-CN-31*11~\nCAS*PR*2*60~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nNM1*82*1*SAMPLE*SAM****XX*1234567893~\nSVC*HC:99213*240*180~\nDTM*472*20261001~\nLX*2~\nTS3*1999999992*11*20261231*1*200~\nCLP*PW-2102*1*200*170*30*12*PAYER-CN-32*11~\nCAS*PR*2*30~\nNM1*QC*1*PATIENT*SECOND****MI*W987654321~\nNM1*82*1*SAMPLE*SARA****XX*1999999992~\nSVC*HC:99214*200*170~\nDTM*472*20261002~\nPLB*123456789*20261231*WO:PAYER-CN-32 PW-2102*20~\nSE*24*0001~\nGE*1*94~\nIEA*1*000000094~", "by": "payee", "control_number": 1 } ``` The answer (200) ```json { "by": "payee", "transmitted": false, "files": [ { "group": { "by": "payee", "key": "1234567893", "stated": true }, "claims": 1, "checks": [ { "trace": "EFT-20261018-03", "original_amount": 330, "amount": 180, "claims_paid": 180, "provider_adjustments": 0 } ], "ties": true, "isa_control": "000000001", "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*HP*SAMPLEPAYER*SAMPLEBILLER*20261019*1500*1*X*005010X221A1~ST*835*0001~BPR*I*180*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*99…", "words": "The claims the payer grouped under provider 1234567893." }, { "group": { "by": "payee", "key": "1999999992", "stated": true }, "claims": 1, "checks": [ { "trace": "EFT-20261018-03", "original_amount": 330, "amount": 150, "claims_paid": 170, "provider_adjustments": 20 } ], "ties": true, "isa_control": "000000002", "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HP*SAMPLEPAYER*SAMPLEBILLER*20261019*1500*2*X*005010X221A1~ST*835*0001~BPR*I*150*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*99…", "words": "The claims the payer grouped under provider 1999999992." } ], "checks": [ { "trace": "EFT-20261018-03", "original_amount": 330, "parts_total": 330, "parts_add_up": true, "original_ties": true, "claims_kept": true, "provider_adjustments": [ { "code": "WO", "reference": "PAYER-CN-32 PW-2102", "amount": 20, "provider": "123456789", "group": "1999999992", "assigned_by": "its reference (PLB03-2) names a claim in this group" } ] } ], "balanced": true, "how": "In each file, each check's BPR02 is recomputed as its claims' payments (CLP04) less its provider-level adjustments (PLB), BPR03 C, or D with the amount unsigned when that is negative. The LX numbers are renumbered, a TS3 whose loop is cut s…", "notes": [], "words": "Split into 2 files by payee; every file ties and the parts add up to the original." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `by` | one of "payee", "rendering_npi" | | | `transmitted` | the value false | Payerworks wrote the files and sent them nowhere. | | `files` | list of object | | | `checks` | list of object | | | `balanced` | boolean | | | `how` | string | | | `notes` | list of string | | | `words` | string | | ### POST /v1/remittances/recoupments **Match recoupments to the claims they take back.** Send a set of 835s. Every provider-level adjustment is named, and each takeback is matched to the paid claim its reference names in the files you sent: by payer claim number, then patient control number, then amount when only one earlier payment at the same payer fits. Unmatched takebacks are listed, with what each payer took back and what the files show still carried forward. It says what was taken and from which claim, never whether it should have been. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/recoupments" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesRecoupments(body); ``` With the Python client ```python answer = pw.post_remittances_recoupments(body) ``` request.json ```json { "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~", "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000095*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*95*X*005010X221A1~\nST*835*0001~\nBPR*I*50*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261102~\nTRN*1*EFT-20261102-01*1234567890~\nDTM*405*20261102~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1006*1*150*150*0*12*PAYER-CN-6*11~\nNM1*QC*1*PATIENT*FIFTH****MI*W777777777~\nSVC*HC:99213*150*150~\nDTM*472*20261020~\nPLB*1234567893*20261231*WO:PAYER-CN-1 PW-1001*100~\nSE*13*0001~\nGE*1*95~\nIEA*1*000000095~" ] } ``` The answer (200) ```json { "adjustments": [ { "check": { "file": 1, "check": 0, "trace": "EFT-20261102-01", "originator": "1234567890", "amount": 50, "date": "2026-11-02", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 1 }, "provider_identifier": "1234567893", "code": "WO", "label": "Overpayment recovery", "kind": "claim_recoupment", "known": true, "label_source": "paraphrase", "amount": 100, "effect": "withheld", "reference": "PAYER-CN-1 PW-1001", "takeback": true, "match": { "matched_by": "payer_claim_number", "matched_on": "CLP07", "token": "PAYER-CN-1", "claim": { "reference": "PW-1001", "payer_claim_number": "PAYER-CN-1", "status_code": "1", "paid": 1260, "billed": 2900, "service_date": "2026-10-01", "check": { "file": 0, "check": 0, "trace": "EFT-20261018-01", "originator": "1234567890", "amount": 1260, "date": "2026-10-18", "method": "ACH", "payer": "SAMPLE HEALTH PLAN", "claims": 2 } }, "reversed_in_files": null }, "matched": true, "ambiguous": false, "words": "WO (overpayment recovery): $100.00 withheld from check EFT-20261102-01 of 11/02/2026 (file 2), matched to claim PW-1001 paid $1,260.00 on check EFT-20261018-01 of 10/18/2026 (file 1), by the payer claim number PAYER-CN-1 (CLP07) in its refe…" } ], "reversals": [], "payers": [ { "payer": "SAMPLE HEALTH PLAN", "originator": "1234567890", "checks": 2, "adjustments": 1, "taken_back": 100, "matched_taken_back": 100, "unmatched_taken_back": 0, "returned": 0, "reversed_on_claims": 0, "other_withheld": 0, "other_added": 0, "forwarding_balance_net": 0, "open_balance": 0, "words": "SAMPLE HEALTH PLAN: $100.00 taken back from claims by provider-level adjustment, $100.00 of it matched to a claim in these files and $0.00 not." } ], "counts": { "checks": 2, "adjustments": 1, "takebacks": 1, "matched": 1, "unmatched": 0, "reversals": 0 }, "words": "1 takeback by provider-level adjustment, 1 matched to a claim in these files; 0 claim reversals.", "how": "Each provider-level adjustment is named by X12's list of PLB reason codes and matched to the one claim payment its reference (PLB03-2) names at the same payer: the payer claim control number (CLP07) first, then the patient control number (C…", "source": { "list": "X12 provider adjustment reason codes (code list 967)", "url": "https://x12.org/codes/provider-adjustment-reason-codes", "read_on": "10/19/2026" }, "reads": "Only the files sent in this call: nothing is stored, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `adjustments` | list of object | | | `reversals` | list of object | | | `payers` | list of object | | | `counts` | object | | | `words` | string | | | `how` | string | | | `source` | object | | | `reads` | string | | ### GET /v1/filing-limits **Published claim filing windows.** The claim filing window each payer in Payerworks’ table publishes, read on its own pages on 09/10/2026, and Medicare fee-for-service’s from 42 CFR 424.44. Search with q, and add date_of_service to count the last day where the payer states one figure; a payer whose pages leave the window to its agreement says so, with no day. Your agreement with the payer governs over a published window, and nothing here is a payer’s answer about a claim. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/filing-limits?q=select&date_of_service=2026-03-15" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getFilingLimits({ query: {"q":"select","date_of_service":"2026-03-15"} }); ``` With the Python client ```python answer = pw.get_filing_limits(query={"q": "select", "date_of_service": "2026-03-15"}) ``` The answer (200) ```json { "total": 1, "data": [ { "slug": "select-health", "payer": "Select Health", "applies_to": null, "answer": "window", "answer_words": "A published window", "words": "1 year from the date of service.", "window": { "years": 1 }, "date_of_service": "2026-03-15", "last_day": "2027-03-15", "last_day_words": "File by 03/15/2027.", "source": { "url": null, "read_on": "09/10/2026", "words": "Read on the payer's own pages on 09/10/2026; the page's address was not kept." } } ], "note": "A payer's own agreement with you, or the plan's document, governs over a published window; a secondary claim's window may differ. Nothing here is a payer's answer about a claim." } ``` | Field | Type | What it is | | --- | --- | --- | | `total` | integer | | | `data` | list of FilingLimit | | | `note` | string | | ### GET /v1/filing-limits/{payer} **One payer’s filing window.** One payer by its slug from GET /v1/filing-limits, or by its name, with every plan line it publishes a window for; add date_of_service to count the last day. A payer with no window on record is 404 no_filing_limit_on_record, never a window. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/filing-limits/select-health?date_of_service=2026-03-15" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getFilingLimitsByPayer("select-health", { query: {"date_of_service":"2026-03-15"} }); ``` With the Python client ```python answer = pw.get_filing_limits_by_payer("select-health", query={"date_of_service": "2026-03-15"}) ``` The answer (200) ```json { "payer": "Select Health", "data": [ { "slug": "select-health", "payer": "Select Health", "applies_to": null, "answer": "window", "answer_words": "A published window", "words": "1 year from the date of service.", "window": { "years": 1 }, "date_of_service": "2026-03-15", "last_day": "2027-03-15", "last_day_words": "File by 03/15/2027.", "source": { "url": null, "read_on": "09/10/2026", "words": "Read on the payer's own pages on 09/10/2026; the page's address was not kept." } } ], "note": "A payer's own agreement with you, or the plan's document, governs over a published window; a secondary claim's window may differ. Nothing here is a payer's answer about a claim." } ``` | Field | Type | What it is | | --- | --- | --- | | `payer` | string | | | `data` | list of FilingLimit | | | `note` | string | | ## Reading the answer - A claim’s verdict comes from the money and the refusing codes, never from CLP02 alone: a refusal often arrives as CLP02 1 with nothing paid. - `primary_reason` is `null` when nothing refused money. A contractual write-off (CO-45) on a paid claim is not why a claim was refused. - A deadline cites its source or has no date: Medicare’s own windows from the regulation for Medicare claims, else the limits you send, else `null`. - A file with several checks says each check apart, and no single number or total for the file. ## Troubleshooting - `not_an_835` (422): The file is not an 835. A 277CA or a 999 goes to POST /v1/acknowledgments/explain. - `is_a_remark_code` (400): N115 and other remark codes (RARCs) are not adjustment reason codes; the CARC lookup refuses them and says so. - `missing_facts` (422): An appeal letter needs facts that were not sent; the answer lists them, and nothing is written. - `letter_refused` (422): A fact held a placeholder in brackets, which the letter guard refuses. - `no_filing_limit_on_record` (404): No published filing window is on record for that payer: ask the payer or read your agreement. A window is never guessed. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `deposits_required` | 400 | Send the bank's deposits as deposits: a list of { amount, effective_date, trace_number?, originator?, description?, id? }. | | `invalid_code` | 400 | An adjustment reason code is one to four digits, or a letter and digits (such as 45, A1 or B13), optionally after its group. | | `invalid_date_of_service` | 400 | date_of_service is a day, YYYY-MM-DD. | | `invalid_group_code` | 400 | A claim adjustment group code is two letters, such as CO or PR. | | `invalid_letter` | 400 | Send the letter's text as letter, at most 20000 characters. | | `invalid_payer` | 400 | Name the payer by its slug from GET /v1/filing-limits, or by its name. | | `invalid_split_by` | 400 | by is "payee" (the 2000 loop's provider, TS301) or "rendering_npi" (each claim's rendering provider, NM1*82). | | `is_a_remark_code` | 400 | This is a remittance advice remark code (RARC), not a claim adjustment reason code (CARC). | | `no_remittance` | 400 | Send an 835 as x12, or the claims as a list in claims. | | `no_remittances` | 400 | Send the 835s as remittances: a list of X12 files, each the file's text. | | `no_x12` | 400 | Send the file's text as `x12`. | | `too_many_claims` | 400 | At most 100 claims a call. | | `two_inputs` | 400 | Send either x12 (an 835) or claims (a list), not both. | | `unknown_letter_kind` | 400 | kind is one of the letter kinds the answer lists, with the facts each requires. | | `no_filing_limit_on_record` | 404 | No published filing window is on record for this payer. Ask the payer, or read your agreement with it; GET /v1/filing-limits lists every payer that has one. | | `too_many_remittances` | 413 | More was sent than one call reads across its files (at most 50 files, 1000 checks, 5000 claims, 5000 provider-level adjustments and 2000 deposits): the error names the unit and the limit. Send fewer in each call. | | `invalid_claim_facts` | 422 | One or more claims or facts are not usable; nothing was decided. Each problem is listed with its field. | | `invalid_deposits` | 422 | One or more deposits are not usable; nothing was matched. Each problem is listed with its field. | | `invalid_letter_facts` | 422 | One or more facts are not usable; nothing was written. Each is listed with its field. | | `letter_refused` | 422 | The letter was not written: a fact holds a placeholder in brackets, which the guard refuses. | | `missing_facts` | 422 | The letter needs facts that were not sent; the answer lists them. | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | | `not_an_835` | 422 | This is not an 835 remittance. | | `value_holds_a_delimiter` | 422 | A value in the file holds one of the characters Payerworks writes as a delimiter (* : ^ ~), so it cannot be copied unchanged; nothing was written. | ## Code lists Any one code in words: [GET /v1/codes/carc/{code}](#get-v1-codes-carc-code), [GET /v1/codes/group/{code}](#get-v1-codes-group-code). ### A remittance claim’s verdict `paid`, `partially_paid`, `denied`, `applied_to_patient`, `nothing_payable`, `reversal`, `reversal_and_repaid`, `forwarded`, `pending` ### The next steps on a denial | Step | What to do | In words | | --- | --- | --- | | `correct` | Correct the claim and send a replacement | The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this. | | `attach` | Send what the payer asked for | The remark codes say what the payer is missing. Send it, on a replacement claim with a PWK segment naming the attachment, or enclosed with an appeal, as the payer's instructions say. | | `dispute` | Check the payment against your contract | The payer allowed less than your charge. That is normal under a contract; it is an underpayment only if the allowed amount is below your contracted rate for this service. If it is, ask the payer to reprocess the claim at the contracted amount. | | `timely` | Appeal with proof of timely filing | The payer says the claim was filed after its limit. If you filed in time, appeal with your proof of filing, such as the acceptance acknowledgment (999 or 277CA) for the original claim and its date. A replacement claim cannot cure a filing limit. | | `duplicate` | Find the original claim before sending anything | The payer already holds a claim for this service. Find the original claim and its payment first: sending again makes more duplicates. Appeal only if this is a different service, and say what makes it different. | | `discovery` | Find the coverage active on the date of service | The payer says the patient was not covered on the date of service. Check eligibility for that date, find the plan that was active, and file the claim to it. | | `cob` | Bill the payers in the right order | The payer says another plan pays first. Confirm the order of benefits with the patient and the plans, file to the primary, then file to this payer as secondary with the primary's payment. If the patient has no other coverage, tell the payer so it can correct its record. Payerworks does not write a secondary claim yet. | | `auth` | Appeal with the authorization | The payer says an authorization was missing or did not cover the service. If you had one, appeal with its number. If you did not, ask the payer whether it grants one after the service. | | `necessity` | Appeal with the clinical records | The payer says the service was not medically necessary. Appeal with the clinical records that support it, the ones the payer's coverage policy asks for. | | `non_covered` | Check the benefit, then appeal or bill the patient | The payer calls the service not covered. Check the patient's benefits for it: if the plan covers it, appeal with that evidence. If it does not, whether the patient may be billed depends on the group code (PR yes, CO no) and on any notice the patient signed in advance. | | `recoupment` | Review the takeback | The payer took back money it had paid on this claim. Read the reason it gives; if you disagree, dispute the recoupment in writing and ask for the payment to be reinstated. | | `contract_cut` | Read the remittance before deciding | The payer reduced the charge to its contracted amount and paid nothing more. That can be the deductible, a coverage denial or an underpayment, and only the remittance's patient responsibility, group codes and remarks say which. Read them, then bill the patient, appeal, or dispute the allowed amount. | | `decide` | Read the denial and decide | The code alone does not decide the remedy. Read the remark codes and the payer's notes, then correct the claim, appeal it, or write it off. | | `post_payment` | Post the payment | The payer paid the claim. Post the payment and the contractual write-off; bill the patient only the patient responsibility. | | `bill_patient` | Bill the patient | The payer processed the claim and put the allowed amount on the patient (deductible, coinsurance or copay). Nothing was refused, so there is nothing to appeal: bill the patient. | | `post_correction` | Post the reversal, then the new payment | The payer reversed its earlier payment and paid the claim again. Post the reversal against the original payment, then post the new payment. | | `await_new_decision` | Wait for the new decision | The payer withdrew an earlier decision that paid nothing. No money moves; a new decision usually follows on a later remittance. | | `forwarded` | Wait for the next payer | The payer processed the claim and sent it on to the next payer itself. Do not file it to that payer by hand; wait for its own remittance. | | `wait` | Wait, or ask the payer | The payer has not finished this claim. Wait for its decision, or ask the payer where it stands (a claim status request, a 276). | | `new_claim` | Send a new claim | The payer says the claim could not be processed (remark MA130), so it has no appeal rights. Send a NEW claim (frequency code 1) with the information completed or corrected. | | `resend` | Fix the rejection and send it again | The claim was rejected at the payer's front end and never entered adjudication. Fix what the rejection names and send it again as a new claim (frequency code 1). | | `post_no_payment` | Post the adjudication: nothing is payable | The payer finished the claim and nothing is payable: the contractual or prior-payer adjustments account for the whole charge. Post it with its write-off. Check the allowed amount against your contract only if you believe it is wrong. | | `paid_short` | Ask the payer to pay its own allowed amount | The payer paid less than its own allowed amount less the patient's share, and gave no reason beyond the contractual adjustment. Ask it to pay the rest, or to say why it did not. The underpayment letter states it from allowed_amount, paid_amount and patient_responsibility_amount; no contracted rate is needed. | | `necessity_patient` | Appeal, or bill the patient | The payer refused the service as not medically necessary and put it on the patient (group PR), which a payer does when the patient signed a notice in advance that the plan might not pay. Two doors are open: appeal with the clinical records if the service was necessary, or bill the patient under that notice. | | `timely_medicare` | Ask for an exception to the time limit | Medicare does not treat a finding that a claim was filed late as an initial determination, so it cannot be appealed (42 CFR 405.926(n)). The only route is an exception to the filing limit under 42 CFR 424.44(b), such as a late filing caused by an error of CMS or its contractor, asked of your Medicare contractor with the evidence. If the claim was in fact filed in time, ask the contractor to correct its error. | | `duplicate_medicare` | Ask for a reopening, not an appeal | Medicare processes a denial of a claim as a duplicate as a clerical error, by reopening rather than by redetermination (42 CFR 405.980(a)(3)). Find the original claim first; if this is a different service, ask your Medicare contractor to reopen the claim and say what makes it different. | ### The appeal letter kinds | Kind | Subject | Facts it needs | | --- | --- | --- | | `timely_filing` | Request for reconsideration: timely filing denial | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `original_filing_date` | | `authorization_on_file` | Request for reconsideration: authorization on file | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `authorization_number` | | `records_enclosed` | Records enclosed for review of this claim | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `enclosures` | | `underpayment` | Request for reconsideration: payment below the amount due | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `allowed_amount`, `paid_amount` | | `not_a_duplicate` | Request for reconsideration: not a duplicate claim | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `distinguishing_facts` | | `other_coverage` | Request for reconsideration: coordination of benefits | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `other_coverage_status` | | `medical_necessity` | Request for reconsideration: medical necessity | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `clinical_summary`, `enclosures` | | `recoupment` | Dispute of a recoupment | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `recoupment_amount`, `recoupment_date` | | `reconsideration` | Request for reconsideration of the appeal decision | `payer_name`, `patient_name`, `member_id`, `payer_claim_number`, `service_date`, `provider_name`, `signer_name`, `prior_appeal_date`, `reconsideration_reason` | ## Example [remittances.mjs](https://payerworks.com/docs/examples/remittances.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. remittances.mjs ```js // Payerworks example: remittances and denials, explained claim by claim. Docs: https://payerworks.com/docs/remittances // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node remittances.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. A paid claim and a denied one (a member ID ending in DENY), answered by the sandbox payer with an 835. const sandbox = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claimEndingIn(''), claimEndingIn('DENY')] }); const the835 = sandbox.files.remittance_835; // 2. The 835 claim by claim: paid, allowed, the patient's share and the verdict in words. const explained = await pw.postRemittancesExplain({ x12: the835 }); // 3. Each denial as the next step, with every deadline and its source. const next = await pw.postDenialsNextStep({ x12: the835 }); // 4. One adjustment reason code in words. const carc = await pw.getCodesCarcByCode('50'); // 5. The failure path: N115 is a remark code, not an adjustment reason, and the lookup says so. const refused = await refusalOf(() => pw.getCodesCarcByCode('N115'), { PayerworksError }); return { verdicts: Object.fromEntries(explained.claims.map((c) => [c.reference, c.verdict])), denied_claims: next.claims.filter((c) => c.reference === 'EX-DENY').length, carc_50: carc.label, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "verdicts": { "EX-PAID": "paid", "EX-DENY": "denied" }, "denied_claims": 1, "carc_50": "Non-covered: not deemed a medical necessity", "refused": { "status": 400, "code": "is_a_remark_code", "request_id": "req_5" } } ``` # Analytics from your own files > Figures from the files you send, each with how many it rests on. Payerworks has no data of anyone else’s to compare you with. Page: https://payerworks.com/docs/analytics · Markdown: https://payerworks.com/docs/analytics.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Four routes count what your own files show: your denials and payments, payments against the fee schedule you send, a claim against your own past denials, and your open denials ranked. Each reads only what you send in the call, and nothing is stored. ## Routes - [POST /v1/analytics/summary](#post-v1-analytics-summary): Summarize your claims - [POST /v1/remittances/variance](#post-v1-remittances-variance): Check payments against your fee schedule - [POST /v1/claims/risk](#post-v1-claims-risk): Check a claim against your past denials - [POST /v1/denials/prioritize](#post-v1-denials-prioritize): Rank your open denials Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/analytics/summary **Summarize your claims.** Send the claims with the 999s, 277CAs and 835s you hold for them (the same body as /v1/claims/track), or your 835s alone. You get first-pass acceptance, the rejection reasons ranked, the denial rate by payer and by group code, the dollars refused, the days from sending a claim to its remittance by payer (the median and quartiles), reversals, and what the refusals come down to by reason. Every figure says how many it counts, and a rate or a median is read as a pattern only from five; below that the answer says there is not enough history. Send month (YYYY-MM) for one calendar month: the current month counts to today. It reads only your files: there is no benchmark, and nothing is stored. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/analytics/summary" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAnalyticsSummary(body); ``` With the Python client ```python answer = pw.post_analytics_summary(body) ``` request.json ```json { "x12_837": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HC*SAMPLEBILLER*PWSANDBOX*20261019*1500*1*X*005010X222A1~ST*837*0001*005010X222A1~BHT*0019*00*PW-SBX-PAID*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W123456789~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-PAID*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0001~ST*837*0002*005010X222A1~BHT*0019*00*PW-SBX-DENY*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W12345DENY~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-DENY*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0002~ST*837*0003*005010X222A1~BHT*0019*00*PW-SBX-REJ*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W12345REJ~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-REJ*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0003~ST*837*0004*005010X222A1~BHT*0019*00*PW-SBX-D16*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W12345D16~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-D16*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0004~ST*837*0005*005010X222A1~BHT*0019*00*PW-SBX-PEND*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W12345PEND~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-PEND*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0005~ST*837*0006*005010X222A1~BHT*0019*00*PW-SBX-DED*20261019*1500*CH~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~PER*IC*Billing Office*TE*5555550100~NM1*40*2*Sample Health Plan*****46*PWSANDBOX~HL*1**20*1~PRV*BI*PXC*1223S0112X~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*EI*123456789~HL*2*1*22*0~SBR*P*18*******CI~NM1*IL*1*Patient*Sample****MI*W12345DED~N3*9 Elm Street~N4*Springfield*IL*62701~DMG*D8*19600214*F~NM1*PR*2*Sample Health Plan*****PI*99999~CLM*PW-SBX-DED*2900***11:B:1*Y*A*Y*Y~HI*ABK:G4733~LX*1~SV1*HC:E0486:KX*2900*UN*1***1~DTP*472*D8*20261001~SE*24*0006~GE*6*1~IEA*1*000000001~", "acknowledgments": [ "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000001*0*T*:~GS*FA*PWSANDBOX*SAMPLEBILLER*20261019*1500*1*X*005010X231A1~ST*999*0001*005010X231A1~AK1*HC*1*005010X222A1~AK2*837*0001*005010X222A1~IK5*A~AK2*837*0002*005010X222A1~IK5*A~AK2*837*0003*005010X222A1~IK5*A~AK2*837*0004*005010X222A1~IK5*A~AK2*837*0005*005010X222A1~IK5*A~AK2*837*0006*005010X222A1~IK5*A~AK9*A*6*6*6~SE*16*0001~GE*1*1~IEA*1*000000001~", "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000002*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*2*X*005010X214~ST*277*0001*005010X214~BHT*0085*08*PWSANDBOX2*20261019*1500*TH~HL*1**20*1~NM1*PR*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~TRN*1*PWSANDBOX2~DTP*050*D8*20261019~DTP*009*D8*20261019~HL*2*1*21*1~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~TRN*2*000000001~STC*A1:19:PR*20261019*WQ*17400~QTY*90*5~QTY*AA*1~AMT*YU*14500~AMT*YY*2900~HL*3*2*19*1~NM1*85*2*Sample Sleep Dentistry*****XX*1234567893~TRN*1*0~HL*4*3*PT~NM1*QC*1*Patient*Sample****MI*W123456789~TRN*2*PW-SBX-PAID~STC*A2:20*20261019*WQ*2900~REF*1K*PWS2292711347~DTP*472*D8*20261001~HL*5*3*PT~NM1*QC*1*Patient*Sample****MI*W12345DENY~TRN*2*PW-SBX-DENY~STC*A2:20*20261019*WQ*2900~REF*1K*PWS0752053091~DTP*472*D8*20261001~HL*6*3*PT~NM1*QC*1*Patient*Sample****MI*W12345REJ~TRN*2*PW-SBX-REJ~STC*A7:164:IL*20261019*U*2900~DTP*472*D8*20261001~HL*7*3*PT~NM1*QC*1*Patient*Sample****MI*W12345D16~TRN*2*PW-SBX-D16~STC*A2:20*20261019*WQ*2900~REF*1K*PWS1120978316~DTP*472*D8*20261001~HL*8*3*PT~NM1*QC*1*Patient*Sample****MI*W12345PEND~TRN*2*PW-SBX-PEND~STC*A2:20*20261019*WQ*2900~REF*1K*PWS0688343368~DTP*472*D8*20261001~HL*9*3*PT~NM1*QC*1*Patient*Sample****MI*W12345DED~TRN*2*PW-SBX-DED~STC*A2:20*20261019*WQ*2900~REF*1K*PWS0903628598~DTP*472*D8*20261001~SE*54*0001~GE*1*2~IEA*1*000000002~" ], "remittances": [ "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000003*0*T*:~GS*HP*PWSANDBOX*SAMPLEBILLER*20261019*1500*3*X*005010X221A1~ST*835*0001~BPR*I*1740*C*CHK************20261019~TRN*1*PWSANDBOX-000000003-1*1PWSANDBOX~DTM*405*20261019~N1*PR*PAYERWORKS SANDBOX PAYER*XV*PWSANDBOX~REF*2U*PWSANDBOX~N1*PE*Sample Sleep Dentistry*XX*1234567893~N3*100 Main Street~N4*Springfield*IL*627011234~REF*TJ*123456789~LX*1~CLP*PW-SBX-PAID*1*2900*1392*348*CI*PWS2292711347*11*1~NM1*QC*1*Patient*Sample****MI*W123456789~SVC*HC:E0486:KX*2900*1392**1~DTM*472*20261001~CAS*CO*45*1160~CAS*PR*2*348~AMT*B6*1740~CLP*PW-SBX-DENY*1*2900*0*0*CI*PWS0752053091*11*1~NM1*QC*1*Patient*Sample****MI*W12345DENY~SVC*HC:E0486:KX*2900*0**1~DTM*472*20261001~CAS*CO*50*2900~AMT*B6*0~LQ*HE*N115~CLP*PW-SBX-D16*1*2900*0*0*CI*PWS1120978316*11*1~NM1*QC*1*Patient*Sample****MI*W12345D16~SVC*HC:E0486:KX*2900*0**1~DTM*472*20261001~CAS*CO*16*2900~AMT*B6*0~LQ*HE*M51~CLP*PW-SBX-DED*1*2900*348*1392*CI*PWS0903628598*11*1~NM1*QC*1*Patient*Sample****MI*W12345DED~SVC*HC:E0486:KX*2900*348**1~DTM*472*20261001~CAS*CO*45*1160~CAS*PR*1*1392~AMT*B6*1740~SE*40*0001~GE*1*3~IEA*1*000000003~" ], "month": "2026-10" } ``` The answer (200) ```json { "unit": "claims", "period": { "month": "2026-10", "from": "2026-10-01", "through": "2026-10-19", "month_to_date": true, "basis": "Each claim is counted in the month of the day it was first sent (its 837's date), else its date of service; a claim payment, in the month of its date of service.", "words": "The calendar month to date, through 10/19/2026." }, "claims": { "n": 6, "sent": 6, "outside_the_month": 0, "undated_left_out": 0, "states": { "processed": 2, "denied": 2, "rejected": 1, "accepted": 1 } }, "first_pass": { "n": 6, "accepted": 5, "rejected": 1, "received_only": 0, "not_acknowledged": 0, "acceptance_rate": 0.833, "enough_history": true, "words": "Of the 6 claims whose first submission's first acknowledgment accepted or rejected it, 5 were accepted. 0 were only received at the first acknowledgment, and 0 have no acknowledgment of their first submission among the files sent: neither i…" }, "rejections": { "claims_rejected": 1, "reasons": [ { "kind": "277CA", "category": "A7", "code": "164", "entity": "IL", "claims": 1, "words": "The claim was rejected for invalid information: the subscriber's contract or member number is missing or invalid (STC A7:164:IL). Fix it on the subscriber in your claim data, then send the claim again." } ], "words": "Each reason counts the claims it rejected at least once, on any submission; a claim rejected for two reasons is counted under both." }, "denials": { "unit": "claims", "decided": 4, "denied": 2, "denial_rate": 0.5, "enough_history": false, "words": "Not enough history: 4 are fewer than the 5 this figure is read as a pattern from.", "partly_refused": 0, "dollars_refused": 5800, "dollars_refused_cents": 580000, "by_payer": [ { "payer_id": "99999", "payer_id_source": "claim", "decided": 4, "denied": 2, "denial_rate": 0.5, "dollars_refused": 5800, "enough_history": false, "words": "Not enough history: 4 are fewer than the 5 this figure is read as a pattern from." } ], "by_group": { "n": 2, "words": "Of the 2 claims with money refused, how many had a refusing adjustment under each group code (one may have several).", "groups": [ { "group": "CO", "name": "Contractual obligation", "known": true, "count": 2, "share": 1, "dollars_refused": 5800 } ] }, "reasons": [ { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "known": true, "label_source": "paraphrase", "root_cause": "missing_information", "count": 1, "share": 0.5, "dollars_refused": 2900 }, { "group": "CO", "code": "50", "label": "Non-covered: not deemed a medical necessity", "known": true, "label_source": "paraphrase", "root_cause": "medical_necessity", "count": 1, "share": 0.5, "dollars_refused": 2900 } ], "root_cause": { "n": 2, "words": "Each of the 2 claims with money refused is placed by the reason that refused the most money on it; the dollars count every refusing adjustment by its own reason.", "categories": [ { "category": "medical_necessity", "words": "The payer judged the service not medically necessary, unproven or experimental, or the records not to support its level, frequency or diagnosis.", "count": 1, "share": 0.5, "dollars_refused": 2900 }, { "category": "missing_information", "words": "Information or an attachment the payer needed was missing, wrong or not documented.", "count": 1, "share": 0.5, "dollars_refused": 2900 } ], "source": { "list": "X12 claim adjustment reason codes", "url": "https://x12.org/codes/claim-adjustment-reason-codes", "read_on": "10/19/2026", "grouping": "Payerworks' own grouping of each code by its published words; the codes' words are X12's and are not copied here." } } }, "days_to_remittance": { "words": "Days from the day the claim's latest submission was sent (its 837's date) to the first remittance that decided it. A claim with no submission date is not counted.", "all_payers": { "n": 4, "median": 0, "p25": 0, "p75": 0, "min": 0, "max": 0, "enough_history": false }, "by_payer": [ { "payer_id": "99999", "payer_id_source": "claim", "n": 4, "median": 0, "p25": 0, "p75": 0, "min": 0, "max": 0, "enough_history": false, "words": "Not enough history: 4 are fewer than the 5 this figure is read as a pattern from." } ], "not_counted": 0 }, "reversals": { "claims_with_reversals": 0, "reversals": 0, "taken_back": 0, "taken_back_of_payments_not_sent": 0, "words": "A reversal takes back an earlier payment. Money taken back from a payment among the files sent is counted in taken_back; from a payment not among them, apart." }, "files_not_tied": { "items": 0, "words": "Every acknowledgment and remittance item was tied to a claim sent." }, "floor": { "min_n": 5, "words": "A rate or a median is read as a pattern only from 5. Every figure counts only the files sent: Payerworks holds no other history, and no figure here is a benchmark." }, "reads": "Only the files sent in this call: nothing is stored, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `unit` | one of "claims", "claim payments" | | | `period` | object | The month counted, or every claim in the files. | | `claims` | object | | | `claim_payments` | object | | | `first_pass` | object | First-pass acceptance: n, the rate (null below the floor’s count), and what was left out of it. | | `rejections` | object | | | `denials` | object | The denial rate with its n, by payer, by group code and by reason, and the dollars refused. | | `days_to_remittance` | object | Days from sending a claim to its remittance, by payer: n, the median and the quartiles. | | `reversals` | object | | | `floor` | object | | | `today` | string | | | `reads` | string | | ### POST /v1/remittances/variance **Check payments against your fee schedule.** Send your 835s and your fee schedule (payer_id, code, modifier, allowed, effective_from, effective_to). Each service line is judged against the rate in force on its date of service: the amount expected, the payer’s allowed amount, the difference, and the adjustment codes the payer gave. A line with no rate says so and is never read as zero, and a line the payer refused is a denial, not an underpayment. Add medicare (a state) to compare with Medicare’s October 2026 DMEPOS fee schedule where the API runs, labeled as Medicare’s rate and never as your contract. An underpaid line carries the facts for POST /v1/appeals/letter. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/variance" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesVariance(body); ``` With the Python client ```python answer = pw.post_remittances_variance(body) ``` request.json ```json { "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000093*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*93*X*005010X221A1~\nST*835*0001~\nBPR*I*80*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-02*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*XX*1234567893~\nLX*1~\nCLP*PW-2003*1*215*0**12*PAYER-CN-23*11~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nDTM*232*20261001~\nSVC*HC:99213*150*0~\nDTM*472*20261001~\nCAS*CO*16*150~\nLQ*HE*N290~\nSVC*HC:87880*65*0~\nDTM*472*20261001~\nCAS*CO*16*65~\nLQ*HE*N290~\nCLP*PW-2004*1*350*0**MB*PAYER-CN-24*11~\nNM1*QC*1*PATIENT*SECOND****MI*1EG4TE5MK73~\nDTM*232*20261002~\nSVC*HC:99214*350*0~\nDTM*472*20261002~\nCAS*CO*50*350~\nLQ*HE*N115~\nCLP*PW-2005*1*240*80*100*12*PAYER-CN-25*11~\nNM1*QC*1*PATIENT*THIRD****MI*W555555555~\nDTM*232*20261003~\nSVC*HC:99213*240*80~\nDTM*472*20261003~\nCAS*CO*45*60~\nCAS*PR*1*100~\nAMT*B6*180~\nCLP*PW-2006*1*150*0**12*PAYER-CN-26*11~\nNM1*QC*1*PATIENT*FOURTH****MI*W666666666~\nDTM*232*20260101~\nSVC*HC:99213*150*0~\nDTM*472*20260101~\nCAS*CO*29*150~\nSE*39*0001~\nGE*1*93~\nIEA*1*000000093~" ], "fee_schedule": [ { "payer_id": "99999", "code": "99213", "allowed": 210, "effective_from": "2026-01-01" }, { "payer_id": "99999", "code": "99214", "allowed": 260, "effective_from": "2026-01-01" } ] } ``` The answer (200) ```json { "lines": [ { "source": "remittances[0]", "claim_index": 0, "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "payer_id": "99999", "line": 0, "procedure_code": "99213", "modifiers": [], "service_date": "2026-10-01", "units_paid": null, "billed": 150, "paid": 0, "allowed": null, "expected_allowed": null, "variance": null, "status": "refused", "words": "The payer refused money on this line: it is a denial, not a pricing question. POST /v1/denials/next-step says what to do.", "rate": null, "adjustments": [ { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "amount": 150 } ] }, { "source": "remittances[0]", "claim_index": 0, "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "payer_id": "99999", "line": 1, "procedure_code": "87880", "modifiers": [], "service_date": "2026-10-01", "units_paid": null, "billed": 65, "paid": 0, "allowed": null, "expected_allowed": null, "variance": null, "status": "refused", "words": "The payer refused money on this line: it is a denial, not a pricing question. POST /v1/denials/next-step says what to do.", "rate": null, "adjustments": [ { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "amount": 65 } ] } ], "totals": { "lines": 5, "judged": 1, "as_expected": 0, "underpaid": 1, "underpaid_dollars": 30, "above_expected": 0, "above_expected_dollars": 0, "no_rate": 0, "refused": 4, "no_allowed_stated": 0, "reversals_and_predeterminations_left_out": 0, "repeats_left_out": 0, "words": "1 of 5 lines were judged against your fee schedule; 1 was allowed below your rate, by $30.00 in all. A line with no rate is not judged and is never read as zero." }, "by_payer": [ { "payer_id": "99999", "payer_id_source": "N1*PR", "lines": 5, "judged": 1, "underpaid": 1, "underpaid_dollars": 30, "above_expected": 0, "above_expected_dollars": 0, "no_rate": 0 } ], "rule": "Each line is judged by the row in force on its date of service: a row with effective_from applies from that day, effective_to ends it, and of several rows the newest in force wins. A row naming a modifier the line carries is chosen over the…", "letters": "An underpaid line carries the facts POST /v1/appeals/letter (kind underpayment) can be sent, from the remittance and your own rate; add the patient, member ID, provider and signer.", "reads": "Only the files and fee schedule sent in this call: nothing is stored, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `lines` | list of object | | | `totals` | object | | | `by_payer` | list of object | | | `medicare` | object | | | `rule` | string | | | `letters` | string | | | `reads` | string | | ### POST /v1/claims/risk **Check a claim against your past denials.** Send a claim (as /v1/claims/validate takes it) and your past 835s. For each line, every past refusal of the same payer, procedure code and modifiers, with its reason and remark codes, on how many claims, out of how many decided claims with a line of that shape, and the fix. A reason is called a pattern only from two refused claims: a claim counts once however many of its lines were refused, and a remittance sent twice counts once. Send the past claims too and the diagnosis is compared. It reads only your files and never says how a payer will decide. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/risk" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsRisk(body); ``` With the Python client ```python answer = pw.post_claims_risk(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000093*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*93*X*005010X221A1~\nST*835*0001~\nBPR*I*80*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-02*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*XX*1234567893~\nLX*1~\nCLP*PW-2003*1*215*0**12*PAYER-CN-23*11~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nDTM*232*20261001~\nSVC*HC:99213*150*0~\nDTM*472*20261001~\nCAS*CO*16*150~\nLQ*HE*N290~\nSVC*HC:87880*65*0~\nDTM*472*20261001~\nCAS*CO*16*65~\nLQ*HE*N290~\nCLP*PW-2004*1*350*0**MB*PAYER-CN-24*11~\nNM1*QC*1*PATIENT*SECOND****MI*1EG4TE5MK73~\nDTM*232*20261002~\nSVC*HC:99214*350*0~\nDTM*472*20261002~\nCAS*CO*50*350~\nLQ*HE*N115~\nCLP*PW-2005*1*240*80*100*12*PAYER-CN-25*11~\nNM1*QC*1*PATIENT*THIRD****MI*W555555555~\nDTM*232*20261003~\nSVC*HC:99213*240*80~\nDTM*472*20261003~\nCAS*CO*45*60~\nCAS*PR*1*100~\nAMT*B6*180~\nCLP*PW-2006*1*150*0**12*PAYER-CN-26*11~\nNM1*QC*1*PATIENT*FOURTH****MI*W666666666~\nDTM*232*20260101~\nSVC*HC:99213*150*0~\nDTM*472*20260101~\nCAS*CO*29*150~\nSE*39*0001~\nGE*1*93~\nIEA*1*000000093~" ] } ``` The answer (200) ```json { "reference": "PW-2001", "payer_ids_compared": [ "99999" ], "history": { "claim_payments_read": 4, "decisions_read": 4, "for_this_payer": 4, "not_decisions": 0, "repeats_set_aside": 0, "payer_ids_in_files": [ { "payer_id": "99999", "claim_payments": 4 } ], "diagnosis": "No past claims were sent, so diagnoses are not compared: an 835 does not carry them. Send past_claims to compare them too." }, "lines": [ { "index": 0, "procedure_code": "99213", "modifiers": [], "diagnoses": [ "J029", "R509" ], "verdict": "refused_before", "words": "Refused before, not enough history for a pattern: this payer refused 99213 with no modifier on 2 of 3 decided claims with a line of this shape, and no reason came up on 2 claims or more.", "same_shape": { "refused": 2, "paid": 1, "other": 0, "decided": 3, "n": 3, "refusals": [ { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "known": true, "label_source": "paraphrase", "root_cause": "missing_information", "root_cause_mapped": true, "times": 1, "of_decided": 3, "refused": 150, "remark_codes": [ { "code": "N290", "times": 1 } ], "pattern": false, "fix": { "key": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "letter_kind": null }, "latest": { "remittance_date": "2026-10-18", "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "source": "remittances[0]" } }, { "group": "CO", "code": "29", "label": "Time limit for filing has expired (timely filing)", "known": true, "label_source": "paraphrase", "root_cause": "timely_filing", "root_cause_mapped": true, "times": 1, "of_decided": 3, "refused": 150, "remark_codes": [], "pattern": false, "fix": { "key": "timely", "label": "Appeal with proof of timely filing", "words": "The payer says the claim was filed after its limit. If you filed in time, appeal with your proof of filing, such as the acceptance acknowledgment (999 or 277CA) for the original claim and its date. A replacement claim cannot cure a filing l…", "letter_kind": "timely_filing" }, "latest": { "remittance_date": "2026-10-18", "reference": "PW-2006", "payer_claim_number": "PAYER-CN-26", "source": "remittances[0]" } } ] }, "same_diagnosis": null, "same_code_other_modifiers": [] }, { "index": 1, "procedure_code": "87880", "modifiers": [], "diagnoses": [ "J029" ], "verdict": "refused_before", "words": "Refused before, not enough history for a pattern: this payer refused 87880 with no modifier on 1 of 1 decided claim with a line of this shape, and no reason came up on 2 claims or more.", "same_shape": { "refused": 1, "paid": 0, "other": 0, "decided": 1, "n": 1, "refusals": [ { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "known": true, "label_source": "paraphrase", "root_cause": "missing_information", "root_cause_mapped": true, "times": 1, "of_decided": 1, "refused": 65, "remark_codes": [ { "code": "N290", "times": 1 } ], "pattern": false, "fix": { "key": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "letter_kind": null }, "latest": { "remittance_date": "2026-10-18", "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "source": "remittances[0]" } } ] }, "same_diagnosis": null, "same_code_other_modifiers": [] } ], "floor": { "pattern_min_denials": 2, "words": "A reason is called a pattern only when this payer refused a line of the same shape under it on at least 2 claims in the files sent, each claim counted once however many of its lines it refused, and a remittance sent twice counted once. Ever…" }, "root_cause_source": { "list": "X12 claim adjustment reason codes", "url": "https://x12.org/codes/claim-adjustment-reason-codes", "read_on": "10/19/2026", "grouping": "Payerworks' own grouping of each code by its published words; the codes' words are X12's and are not copied here." }, "reads": "Only the files sent in this call: nothing is stored, no payer is asked, and nothing here says how a payer will decide." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `reference` | string or null | | | `payer_ids_compared` | list of string | | | `history` | object | | | `lines` | list of object | | | `floor` | object | | | `root_cause_source` | object | | | `reads` | string | | ### POST /v1/denials/prioritize **Rank your open denials.** Send the 835s holding your open denials, and if you have them, your appeal outcomes (payer_id, carc, outcome). Denials due in 14 days or fewer come first, soonest first; then each by its dollars, or its dollars times your own overturn rate for that payer and code once you have five decided appeals there. A passed deadline comes last. Without outcomes it ranks by deadline and dollars, and says so. No rate here is anyone’s but yours. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/denials/prioritize" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postDenialsPrioritize(body); ``` With the Python client ```python answer = pw.post_denials_prioritize(body) ``` request.json ```json { "remittances": [ "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000093*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*93*X*005010X221A1~\nST*835*0001~\nBPR*I*80*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-02*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE FAMILY MEDICINE*XX*1234567893~\nLX*1~\nCLP*PW-2003*1*215*0**12*PAYER-CN-23*11~\nNM1*QC*1*PATIENT*SAMPLE****MI*W123456789~\nDTM*232*20261001~\nSVC*HC:99213*150*0~\nDTM*472*20261001~\nCAS*CO*16*150~\nLQ*HE*N290~\nSVC*HC:87880*65*0~\nDTM*472*20261001~\nCAS*CO*16*65~\nLQ*HE*N290~\nCLP*PW-2004*1*350*0**MB*PAYER-CN-24*11~\nNM1*QC*1*PATIENT*SECOND****MI*1EG4TE5MK73~\nDTM*232*20261002~\nSVC*HC:99214*350*0~\nDTM*472*20261002~\nCAS*CO*50*350~\nLQ*HE*N115~\nCLP*PW-2005*1*240*80*100*12*PAYER-CN-25*11~\nNM1*QC*1*PATIENT*THIRD****MI*W555555555~\nDTM*232*20261003~\nSVC*HC:99213*240*80~\nDTM*472*20261003~\nCAS*CO*45*60~\nCAS*PR*1*100~\nAMT*B6*180~\nCLP*PW-2006*1*150*0**12*PAYER-CN-26*11~\nNM1*QC*1*PATIENT*FOURTH****MI*W666666666~\nDTM*232*20260101~\nSVC*HC:99213*150*0~\nDTM*472*20260101~\nCAS*CO*29*150~\nSE*39*0001~\nGE*1*93~\nIEA*1*000000093~" ], "facts": { "filing_limit_days": 180, "appeal_limit_days": 180 }, "today": "2026-10-19" } ``` The answer (200) ```json { "today": "2026-10-19", "denials": [ { "source": "remittances[0]", "index": 1, "reference": "PW-2004", "payer_claim_number": "PAYER-CN-24", "payer_id": "99999", "payer_name": "SAMPLE HEALTH PLAN", "service_date": "2026-10-02", "remittance_date": "2026-10-18", "verdict": "denied", "lead": { "group": "CO", "code": "50", "label": "Non-covered: not deemed a medical necessity", "known": true, "root_cause": "medical_necessity" }, "at_stake": 350, "at_stake_cents": 35000, "deadline": { "kind": "redetermination", "date": "2027-02-20", "date_us": "02/20/2027", "days_left": 124, "source": { "kind": "regulation", "citation": "42 CFR 405.942(a)", "url": "https://www.ecfr.gov/current/title-42/section-405.942", "verified_on": "10/19/2026", "presumed_receipt_citation": "42 CFR 405.942(a)(1)" }, "passed": false }, "tier": "open", "overturn": null, "expected_recovery": null, "expected_recovery_cents": null, "ranked_by": "dollars", "next_step": { "key": "necessity", "action": "appeal", "label": "Appeal with the clinical records", "words": "The payer says the service was not medically necessary. Appeal with the clinical records that support it, the ones the payer's coverage policy asks for.", "is_letter": true, "letter_kind": "medical_necessity", "letter_facts": { "payer_name": "SAMPLE HEALTH PLAN", "patient_name": "SECOND PATIENT", "payer_claim_number": "PAYER-CN-24", "patient_control_number": "PW-2004", "service_date": "2026-10-02", "billed_amount": 350, "remittance_date": "2026-10-18", "adjustment_codes": [ "CO-50" ], "remark_codes": [ "N115" ], "claim_filing_indicator": "MB" }, "letter_facts_missing": [ "member_id", "provider_name" ] }, "rank": 1 }, { "source": "remittances[0]", "index": 0, "reference": "PW-2003", "payer_claim_number": "PAYER-CN-23", "payer_id": "99999", "payer_name": "SAMPLE HEALTH PLAN", "service_date": "2026-10-01", "remittance_date": "2026-10-18", "verdict": "denied", "lead": { "group": "CO", "code": "16", "label": "Claim lacks information or has a submission/billing error", "known": true, "root_cause": "missing_information" }, "at_stake": 215, "at_stake_cents": 21500, "deadline": { "kind": "timely_filing", "date": "2027-03-30", "date_us": "03/30/2027", "days_left": 162, "source": { "kind": "caller", "field": "filing_limit_days", "value": 180 }, "passed": false }, "tier": "open", "overturn": null, "expected_recovery": null, "expected_recovery_cents": null, "ranked_by": "dollars", "next_step": { "key": "correct", "action": "correct", "label": "Correct the claim and send a replacement", "words": "The payer says something on the claim was missing or wrong. Fix it and send a replacement claim (frequency code 7) that carries the payer's claim number. An appeal is not the route for this.", "is_letter": false, "letter_kind": null }, "rank": 2 } ], "counts": { "open": 3, "due_soon": 0, "passed": 0, "no_deadline_known": 0, "closed_left_out": 0, "repeats_left_out": 0, "ranked_by_expected_recovery": 0, "ranked_by_dollars": 3, "outcomes_read": 0 }, "ranking": "Deadline first: a denial due in 14 days or fewer comes first, soonest first, and one whose deadline has passed comes last. Then by dollars at stake only: no appeal outcomes were sent, so no overturn rate is used. Send outcomes ({ payer_id, …", "deadline_rule": "Every deadline cites its source, as POST /v1/denials/next-step does.", "floor": { "overturn_min_decided": 5, "levels": [ "payer_and_code", "payer_and_root_cause" ] }, "letters": "POST /v1/appeals/letter writes the letter each next step names (letter_kind).", "reads": "Only the files and outcomes sent in this call: nothing is stored, no payer is asked, and no rate here is anyone's but yours." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `today` | string | | | `denials` | list of object | | | `counts` | object | | | `ranking` | string | | | `deadline_rule` | string | | | `floor` | object | | | `letters` | string | | | `reads` | string | | ## Reading the answer - Every figure carries its `n`: how many claims or lines it rests on. Below a floor, the answer says there is not enough history rather than calling anything a pattern. - There is no outside benchmark: Payerworks holds no one else’s data to compare yours with, so no answer says how others do. ## Troubleshooting - `too_much_to_analyze` (413): More was sent than one call reads; the error names what was counted and the limit. Send it in parts. - `invalid_fee_schedule` (422): A fee schedule row is not usable; each problem is listed with its field, and nothing is judged. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_analytics_request` | 400 | A field of the request is not in the shape this route takes; each problem is listed with its field. | | `no_claim_to_check` | 400 | Send the claim to check as claim, in the 837P JSON shape POST /v1/claims/validate takes. | | `no_remittances` | 400 | Send the 835s as remittances: a list of X12 files, each the file's text. | | `nothing_to_summarize` | 400 | Send the claims (claims or x12_837) with their acknowledgments and remittances, or the remittances alone. | | `too_much_to_analyze` | 413 | More was sent than one call reads: the error names what was counted (`unit`) and the most one call takes (`limit`). Send fewer files a call. | | `invalid_fee_schedule` | 422 | One or more fee schedule rows are not usable; nothing was judged. Each problem is listed with its field. | | `invalid_outcomes` | 422 | One or more appeal outcomes are not usable; nothing was ranked. Each problem is listed with its field. | | `invalid_risk_claim` | 422 | The claim cannot be compared: each problem is listed with its field. Nothing was read. | | `not_an_835` | 422 | This is not an 835 remittance. | ## Example [analytics.mjs](https://payerworks.com/docs/examples/analytics.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. analytics.mjs ```js // Payerworks example: analytics from your own claims and 835s. Docs: https://payerworks.com/docs/analytics // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node analytics.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. A small book through the sandbox payer, so there are 835s to count. const sandbox = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claimEndingIn(''), claimEndingIn('DENY'), claimEndingIn('PEND')] }); // 2. Count what your own remittances show. Every figure carries how many it rests on. const summary = await pw.postAnalyticsSummary({ remittances: [sandbox.files.remittance_835] }); // 3. The failure path: nothing to count is refused, never answered as zero. const refused = await refusalOf(() => pw.postAnalyticsSummary({}), { PayerworksError }); // Two decisions are fewer than the floor, so the denial rate is said with enough_history: false. const { decided, denied, enough_history: enoughHistory } = summary.denials; return { denials: { decided, denied, enough_history: enoughHistory }, floor: summary.floor.min_n, refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "denials": { "decided": 2, "denied": 1, "enough_history": false }, "floor": 5, "refused": { "status": 400, "code": "nothing_to_summarize", "request_id": "req_3" } } ``` # Documents and attachments > The paper side of the same claims and answers: PDFs drawn from what you send, a 275 for attachments, and two picture reads behind their switches. Page: https://payerworks.com/docs/documents · Markdown: https://payerworks.com/docs/documents.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. The paper side of the same claims and answers. A claim is drawn on the CMS-1500 after the same checks POST /v1/claims/837p makes; a remittance and an eligibility answer are drawn as the explain routes read them; Medicare’s redetermination and reconsideration forms are CMS’s own files, filled in. Each PDF comes back in the answer as base64 with its SHA-256, and Payerworks keeps no copy. POST /v1/attachments/275 writes a claim’s attachments as an X12 275 under the control number the claim’s PWK segment announced. A payer that wants more before it decides a claim sends a 277 request for additional information. POST /v1/attachments/requests/explain says it claim by claim (each LOINC code it asks for, about which claim and line, and by when) and lays out the solicited 275 that answers it; POST /v1/attachments/275 writes that 275 from those values, and POST /v1/attachments/requests/check holds it to the request before you send it. In the claims sandbox, a member ID ending in RFAI is held for a document, and the same claims sent again with the 275 are paid. The two picture reads (a paper EOB into a draft 835, a card photo into the payer and member ID) are built and switched off until Payerworks turns them on. New here? Start with [A first 275](https://payerworks.com/docs/quickstart-first-275). ## Routes - [POST /v1/claims/cms1500](#post-v1-claims-cms1500): Draw claims on the CMS-1500 - [POST /v1/remittances/pdf](#post-v1-remittances-pdf): Print a remittance - [POST /v1/eligibility/pdf](#post-v1-eligibility-pdf): Print an eligibility answer - [POST /v1/appeals/form](#post-v1-appeals-form): Fill a Medicare appeal form - [POST /v1/attachments/275](#post-v1-attachments-275): Write a 275 attachment - [POST /v1/attachments/requests/explain](#post-v1-attachments-requests-explain): Read a request for more information - [POST /v1/attachments/requests/check](#post-v1-attachments-requests-check): Check a 275 against its request - [POST /v1/remittances/from-image](#post-v1-remittances-from-image): Read a paper EOB into a draft 835 - [POST /v1/payers/from-card](#post-v1-payers-from-card): Look up a payer from a card photo Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/claims/cms1500 **Draw claims on the CMS-1500.** Up to one hundred claims in the 837P’s JSON, checked by the same claim checks as POST /v1/claims/837p (your own rules are not applied here) and drawn on the CMS-1500 (02/12): six service lines a form, as many forms as a claim needs (at most 200 a call), each form’s Box 28 the total of its own lines. form: full puts CMS’s own form under the data; data_only prints the data alone, to print onto pre-printed red forms (print one on plain paper and hold it against a form first). Send signed_on to date Boxes 12 and 31, which are otherwise left undated, and payer_address for the carrier block. The answer is your PDF as base64; nothing is sent or kept. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/claims/cms1500" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postClaimsCms1500(body); ``` With the Python client ```python answer = pw.post_claims_cms1500(body) ``` request.json ```json { "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, "form": "full", "signed_on": "2026-10-19", "payer_address": { "address1": "PO Box 100", "city": "Springfield", "state": "IL", "postalCode": "62701" } } ``` The answer (200) ```json { "document": "cms1500", "content_type": "application/pdf", "filename": "cms1500-20261019.pdf", "pages": 1, "bytes": 198311, "sha256": "ddf6013e3b086ddb5b6ae2e5c3bcf5484015ffa022ab78ce1ae9409099b96703", "pdf_base64": "JVBERi0xLjcKJYGBgYEKCjUgMCBvYmoKPDwKL1R5cGUgL1hPYmplY3QKL1N1YnR5...", "notice": "Your document: Payerworks drew it from what you sent. Payerworks sent nothing to any payer and kept no copy.", "form": "full", "forms": [ { "claim": 0, "reference": "PW-2001", "page": 1, "lines": [ 1, 2 ] } ], "shrunk": [], "claims": [ { "index": 0, "reference": "PW-2001", "errors": [], "warnings": [], "repairs": [], "not_checked": [] } ], "dated": "Boxes 12 and 31 are dated 10/19/2026.", "sources": [ { "name": "CMS, Medicare Claims Processing Manual, chapter 26 (Item 24F and Item 28)", "url": "https://www.cms.gov/regulations-and-guidance/guidance/manuals/downloads/clm104c26pdf.pdf", "read_on": "10/19/2026" } ], "stored": false, "transmitted": false } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | string | Which document: cms1500, remittance, eligibility, cms-20027 or cms-20033. | | `content_type` | the value "application/pdf" | | | `filename` | string | A name to save it under. | | `pages` | integer | | | `bytes` | integer | The PDF’s size. | | `sha256` | string | The PDF’s SHA-256, in hex. | | `pdf_base64` | string | The PDF, base64. The examples here show only its start. | | `notice` | string | Printed on every page Payerworks draws and in the file’s subject: the document is yours, nothing was sent and no copy was kept. | | `stored` | the value false | | | `transmitted` | the value false | Payerworks drew the document and sent it nowhere. | | `form` | one of "full", "data_only" | | | `forms` | list of object | Each form, the claim it draws and its lines: Box 28 is the total of its own lines. | | `shrunk` | list of string | Each value set smaller to fit its box, never cut. | | `claims` | list of ClaimChecks | Each claim as POST /v1/claims/837p checks it: the form draws the repaired claim. | | `dated` | string | | | `sources` | list of object | | ### POST /v1/remittances/pdf **Print a remittance.** An 835 laid out the way Medicare’s Standard Paper Remittance is read: each check, each claim and its service lines with what was billed, allowed, applied to the deductible and coinsurance, adjusted and paid, the check’s totals and provider-level adjustments, and a glossary of every code on the file. Up to 500 claims and 3,000 service lines a call. The answer is your PDF as base64. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/pdf" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesPdf(body); ``` With the Python client ```python answer = pw.post_remittances_pdf(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*12*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~" } ``` The answer (200) ```json { "document": "remittance", "content_type": "application/pdf", "filename": "remittance-20261019.pdf", "pages": 1, "bytes": 5142, "sha256": "be50686577c0787609da31857e494a6a3e1f8c94e290bfd62d528709f26bb0d0", "pdf_base64": "JVBERi0xLjcKJYGBgYEKCjYgMCBvYmoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUK...", "notice": "Your document: Payerworks drew it from what you sent. Payerworks sent nothing to any payer and kept no copy.", "checks": 1, "claims": 2, "service_lines": 2, "sources": [ { "name": "CMS, Remittance Advice Resources and FAQs (MLN booklet ICN 905367)", "url": "https://www.cms.gov/Outreach-and-Education/Medicare-Learning-Network-MLN/MLNProducts/Downloads/ICN905367TextOnly.pdf", "read_on": "10/19/2026" }, { "name": "CMS, Medicare Claims Processing Manual, chapter 22, section 50 (Standard Paper Remittance)", "url": "https://www.cms.gov/regulations-and-guidance/guidance/manuals/downloads/clm104c22.pdf", "read_on": "10/19/2026" } ], "stored": false, "transmitted": false } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | string | Which document: cms1500, remittance, eligibility, cms-20027 or cms-20033. | | `content_type` | the value "application/pdf" | | | `filename` | string | A name to save it under. | | `pages` | integer | | | `bytes` | integer | The PDF’s size. | | `sha256` | string | The PDF’s SHA-256, in hex. | | `pdf_base64` | string | The PDF, base64. The examples here show only its start. | | `notice` | string | Printed on every page Payerworks draws and in the file’s subject: the document is yours, nothing was sent and no copy was kept. | | `stored` | the value false | | | `transmitted` | the value false | Payerworks drew the document and sent it nowhere. | | `checks` | integer | | | `claims` | integer | | | `service_lines` | integer | | | `sources` | list of object | | ### POST /v1/eligibility/pdf **Print an eligibility answer.** A 271, as POST /v1/eligibility/explain takes it, as a summary per patient: what was asked and what the answer echoes back, whether the coverage is active, the plan’s figures in and out of network, each service type asked, and every benefit line by plan and then service type. Up to 50 patients and 3,000 benefit lines a call, and 3,000 rows as drawn: a benefit line is listed under each service type it names, so a line naming ten service types is ten rows (and each of its messages one more under each). The answer is your PDF as base64. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/pdf" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEligibilityPdf(body); ``` With the Python client ```python answer = pw.post_eligibility_pdf(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000091*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*91*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0001*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0001*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nREF*6P*G7700*SAMPLE EMPLOYER GROUP~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*G*IND*30***23*6000*****Y~\nEB*G*IND*30***29*4100*****Y~\nEB*C*FAM*30***23*3000*****Y~\nEB*C*FAM*30***29*1800*****Y~\nEB*A*IND*30*****.2****Y~\nEB*C*IND*30***23*3000*****N~\nEB*C*IND*30***29*3000*****N~\nEB*A*IND*30*****.4****N~\nEB*B*IND*98****25*****Y~\nEB*A*IND*98*****.4****N~\nEB*1*IND*62********Y~\nSE*27*0001~\nGE*1*91~\nIEA*1*000000091~", "asked": { "service_type_codes": [ "30", "98", "62" ], "date_of_service": "2026-10-20" } } ``` The answer (200) ```json { "document": "eligibility", "content_type": "application/pdf", "filename": "eligibility-20261019.pdf", "pages": 1, "bytes": 6999, "sha256": "03927a87f1174d778f9953402d7ee60106b93ae3d09e7501325ffb0776444961", "pdf_base64": "JVBERi0xLjcKJYGBgYEKCjYgMCBvYmoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUK...", "notice": "Your document: Payerworks drew it from what you sent. Payerworks sent nothing to any payer and kept no copy.", "patients": 1, "benefit_lines": 14, "stored": false, "transmitted": false } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | string | Which document: cms1500, remittance, eligibility, cms-20027 or cms-20033. | | `content_type` | the value "application/pdf" | | | `filename` | string | A name to save it under. | | `pages` | integer | | | `bytes` | integer | The PDF’s size. | | `sha256` | string | The PDF’s SHA-256, in hex. | | `pdf_base64` | string | The PDF, base64. The examples here show only its start. | | `notice` | string | Printed on every page Payerworks draws and in the file’s subject: the document is yours, nothing was sent and no copy was kept. | | `stored` | the value false | | | `transmitted` | the value false | Payerworks drew the document and sent it nowhere. | | `patients` | integer | | | `benefit_lines` | integer | | ### POST /v1/appeals/form **Fill a Medicare appeal form.** CMS’s own redetermination request (CMS-20027) or reconsideration request (CMS-20033), the file CMS publishes, filled from the claim and the 835 that denied it: the beneficiary, the Medicare number, the codes of the lines the payer denied (a line it paid is never listed) and their dates of service, the notice date, and why you disagree in your words. Original Medicare only. Nothing is signed, dated or chosen for you, the deadline comes with its regulation, and the form is yours to review and send. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/appeals/form" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAppealsForm(body); ``` With the Python client ```python answer = pw.post_appeals_form(body) ``` request.json ```json { "form": "redetermination", "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000001*0*T*:~\nGS*HP*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*1*X*005010X221A1~\nST*835*0001~\nBPR*I*1260*C*ACH*CCP*01*999999999*DA*123456*1234567890**01*999999999*DA*654321*20261018~\nTRN*1*EFT-20261018-01*1234567890~\nDTM*405*20261016~\nN1*PR*SAMPLE HEALTH PLAN*PI*99999~\nN1*PE*SAMPLE SLEEP DENTISTRY*XX*1234567893~\nLX*1~\nCLP*PW-1001*1*2900*1260*240*12*PAYER-CN-1*11~\nCAS*CO*45*1400~\nCAS*PR*2*240~\nNM1*QC*1*PATIENT*SAMPLE~\nDTM*472*20261001~\nSVC*HC:E0486*2900*1260~\nAMT*B6*1500~\nCLP*PW-1002*4*350*0*0*MB*PAYER-CN-2*11~\nCAS*CO*50*350~\nNM1*QC*1*PATIENT*SECOND~\nDTM*472*20261002~\nSVC*HC:95800*350*0~\nLQ*HE*N115~\nSE*20*0001~\nGE*1*1~\nIEA*1*000000001~", "reference": "PW-1002", "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "1EG4TE5MK73", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-1002", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ] } }, "reason": "The study was ordered for a documented condition; the order and the visit note are enclosed.", "evidence": true } ``` The answer (200) ```json { "document": "cms-20027", "content_type": "application/pdf", "filename": "cms-20027-PW-1002.pdf", "pages": 1, "bytes": 55228, "sha256": "634e37b62abac326693c63c4d431f8618c41ee9bca5c9a1a997d05a38e3f27ee", "pdf_base64": "JVBERi0xLjcKJYGBgYEKCjEgMCBvYmoKPDwKL0JCb3ggWyAwIDAgNy42ODAwMiA3...", "notice": "Your document: Payerworks drew it from what you sent. Payerworks sent nothing to any payer and kept no copy.", "form": { "id": "cms-20027", "form_number": "CMS-20027", "revision": "01/20", "level": 1, "name": "Medicare Redetermination Request Form (1st level of appeal)", "source_url": "https://www.cms.gov/medicare/cms-forms/cms-forms/downloads/cms20027.pdf", "sha256": "bc80ed47453ddabf3628106fbdf6151c13951be697a2098d548fa7f7be92f09c", "retrieved_on": "10/19/2026", "fillable": true }, "claim": { "reference": "PW-1002", "index": 1, "payer_claim_number": "PAYER-CN-2", "claim_filing_indicator": "MB", "verdict": "denied", "verdict_words": "Refused: the payer paid nothing. The refusing adjustment is CO-50 (Non-covered: not deemed a medical necessity).", "lines_appealed": [ 0 ] }, "filled": { "beneficiary_name": "Sample Patient", "medicare_number": "1EG4TE5MK73", "item_or_service": "95800", "date_received": "10/02/2026", "notice_date": "10/16/2026", "contractor": "SAMPLE HEALTH PLAN", "reason": "The study was ordered for a documented condition; the order and the visit note are enclosed.", "appellant_name": "Sample Family Medicine", "appellant_street": "100 Main Street", "appellant_city": "Springfield", "appellant_state": "IL", "appellant_zip": "62701-1234", "appellant_phone": "(555) 555-0100", "evidence": "I have evidence to submit", "appellant_type": "Person appealing - Provider/Supplier" }, "filled_from": { "beneficiary": "claim (the patient)", "item_or_service": "the 835's lines for this claim payment (procedure codes only)", "notice_date": "the 835: the remittance advice's date (DTM*405)", "contractor": "the 835's payer name (N1*PR); CMS marks the box not required", "appellant": "claim (its billing provider, as the provider or supplier)" }, "deadline": { "date": "2027-02-18", "days": 120, "presumed_receipt_days": 5, "counted_from": "receipt of the initial determination", "citation": "42 CFR 405.942(a)", "url": "https://www.ecfr.gov/current/title-42/section-405.942", "late": false, "today": "2026-10-19" }, "where_to_send": "Send it to the Medicare contractor named on the notice, at the address the notice gives, with a copy of the notice and your evidence. Nothing is signed here: the form has no signature box, and the person appealing reviews it before it goes.", "warnings": [], "stored": false, "transmitted": false } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | string | Which document: cms1500, remittance, eligibility, cms-20027 or cms-20033. | | `content_type` | the value "application/pdf" | | | `filename` | string | A name to save it under. | | `pages` | integer | | | `bytes` | integer | The PDF’s size. | | `sha256` | string | The PDF’s SHA-256, in hex. | | `pdf_base64` | string | The PDF, base64. The examples here show only its start. | | `notice` | string | Printed on every page Payerworks draws and in the file’s subject: the document is yours, nothing was sent and no copy was kept. | | `stored` | the value false | | | `transmitted` | the value false | Payerworks drew the document and sent it nowhere. | | `form` | object | | | `claim` | object | | | `filled` | object | Each field typed into the form, by name. | | `filled_from` | object | Where each filled value came from. | | `deadline` | object or null | | | `where_to_send` | string | | | `warnings` | list of FieldProblem | | ### POST /v1/attachments/275 **Write a 275 attachment.** A claim’s attachments as an X12 275 (005010X210): each file (PDF, TIFF, JPEG, PNG or a CDA document, up to ten and 1.4 MB in all) base64 in its own BIN, under the control number the claim’s PWK segment announced with transmission EL, so the payer can match the two; a control number the claim does not announce is refused. A control number is 2 to 50 letters, digits, dots, dashes or underscores, the same in the claim’s PWK06 and the 275’s TRN02. Written as public payer companion guides show the 275, not from the licensed implementation guide, and the answer says so: check your payer’s own. A test key writes test files only. Names an attachment’s LOINC document type when you give one. A solicited 275 answers a payer’s request: send `request` with the request’s trace, and each attachment may echo its own requested code (stc01) and name its service line (line_control_number); POST /v1/attachments/requests/explain gives every value from the request. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/attachments/275" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAttachments275(body); ``` With the Python client ```python answer = pw.post_attachments_275(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 7, "claim": { "tradingPartnerServiceId": "99999", "submitter": { "organizationName": "Sample Family Medicine", "contactInformation": { "name": "Billing Office", "phoneNumber": "5555550100" } }, "receiver": { "organizationName": "Sample Health Plan" }, "billing": { "npi": "1234567893", "employerId": "123456789", "organizationName": "Sample Family Medicine", "taxonomyCode": "207Q00000X", "address": { "address1": "100 Main Street", "city": "Springfield", "state": "IL", "postalCode": "627011234" } }, "subscriber": { "memberId": "W123456789", "paymentResponsibilityLevelCode": "P", "firstName": "Sample", "lastName": "Patient", "gender": "F", "dateOfBirth": "19800214", "address": { "address1": "9 Elm Street", "city": "Springfield", "state": "IL", "postalCode": "62701" } }, "claimInformation": { "claimFilingCode": "CI", "patientControlNumber": "PW-2001", "claimChargeAmount": "215.00", "placeOfServiceCode": "11", "claimFrequencyCode": "1", "signatureIndicator": "Y", "planParticipationCode": "A", "benefitsAssignmentCertificationIndicator": "Y", "releaseInformationCode": "Y", "healthCareCodeInformation": [ { "diagnosisTypeCode": "ABK", "diagnosisCode": "J029" }, { "diagnosisTypeCode": "ABF", "diagnosisCode": "R509" } ], "serviceLines": [ { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "99213", "lineItemChargeAmount": "150.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1", "2" ] } } }, { "serviceDate": "20261001", "professionalService": { "procedureIdentifier": "HC", "procedureCode": "87880", "lineItemChargeAmount": "65.00", "measurementUnit": "UN", "serviceUnitCount": "1", "compositeDiagnosisCodePointers": { "diagnosisCodePointers": [ "1" ] } } } ], "claimSupplementalInformation": { "reportInformations": [ { "attachmentReportTypeCode": "09", "attachmentTransmissionCode": "EL", "attachmentControlNumber": "PWATT0001" } ] } } }, "attachments": [ { "control_number": "PWATT0001", "content_type": "application/pdf", "content_base64": "JVBERi0xLjQKJSBhbiBpbnZlbnRlZCBhdHRhY2htZW50CjEgMCBvYmo8PD4+ZW5kb2JqCnRyYWlsZXI8PD4+CiUlRU9GCg==", "filename": "progress note.pdf", "loinc": "11506-3" } ] } ``` The answer (200) ```json { "version": "005010X210", "usage": "T", "solicited": false, "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000007*0*T*:~GS*PI*SAMPLEBILLER*99999*20261019*1500*7*X*005010X210~ST*275*0001*005010X210~BGN*02*PW-2001*20261019~NM1*PR*2*Sample Health Plan*****P…", "interchange_control_number": "000000007", "group_control_number": "7", "transaction_set_control_number": "0001", "bgn02": "PW-2001", "segments": 23, "attachments": [ { "control_number": "PWATT0001", "trn02": "PWATT0001", "content_type": "application/pdf", "format_code": "IA", "filename": "progress_note.pdf", "bytes": 70, "loinc": { "code": "11506-3", "valid": true, "known": true, "long_common_name": "Progress Note", "short_name": "Prog Note", "label_source": "loinc", "notice": "This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http:/…" } } ], "warnings": [], "shape": { "verified": false, "words": "Written as the public companion guides of the VA, Texas Medicaid and CMS's esMD profile show a 275; the ASC X12 TR3 for 005010X210 was not read. Check the payer's own companion guide (some ask for CAT02 MB for every file, and some take an u…" }, "limits": { "attachments": 10, "decoded_bytes": 1400000 }, "loinc_notice": "This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http:/…", "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | the value "005010X210" | | | `usage` | one of "T", "P" | | | `solicited` | boolean | | | `x12` | string | The whole interchange. | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_number` | string | | | `bgn02` | string | | | `segments` | integer | | | `attachments` | list of object | | | `warnings` | list of FieldProblem | | | `shape` | object | | | `limits` | object | | | `loinc_notice` | string | LOINC’s notice, carried with any answer that names a LOINC code. | | `transmitted` | the value false | Payerworks wrote the file and sent it nowhere. | ### POST /v1/attachments/requests/explain **Read a request for more information.** A payer’s 277 request for additional information (005010X213) said claim by claim: what it asks for (each LOINC code it names, in LOINC’s own words, or said as a code Payerworks has no words for), about which claim and which service line, by when against your own today, and the solicited 275 that answers it: the payer’s trace, the claim’s REF*EJ, the submitter id, the payer’s response contact, and a body POST /v1/attachments/275 takes. Read as public sources show the request, not from the licensed implementation guide, and the answer says so. Nothing is stored or sent. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/attachments/requests/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAttachmentsRequestsExplain(body); ``` With the Python client ```python answer = pw.post_attachments_requests_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000004*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*4*X*005010X213~ST*277*0001*005010X213~BHT*0010*08*PWSANDBOX4*20261019*1500~HL*1**20*1~NM1*PR*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~HL*2*1*21*1~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~HL*3*2*19*1~NM1*1P*2*Sample Sleep Dentistry*****XX*1234567893~HL*4*3*PT~NM1*QC*1*Patient*Sample****MI*W12345RFAI~TRN*1*PWR3503656727~STC*R4:11506-3::LOI*20261019~REF*1K*PWS1325701407~REF*EJ*PW-SBX-RFAI~DTP*472*D8*20261001~DTP*106*D8*20261118~SE*17*0001~GE*1*4~IEA*1*000000004~", "today": "2026-10-19" } ``` The answer (200) ```json { "kind": "277RFAI", "version": "005010X213", "usage": "T", "created": "2026-10-19", "reference": "PWSANDBOX4", "today": "2026-10-19", "claims": [ { "index": 0, "trace": "PWR3503656727", "trace_type": "1", "payer": { "name": "PAYERWORKS SANDBOX PAYER", "id": "PWSANDBOX" }, "receiver": { "name": "Sample Sleep Dentistry", "id": "SAMPLEBILLER", "id_qualifier": "46" }, "provider": { "entity": "1P", "name": "Sample Sleep Dentistry", "first_name": null, "id_qualifier": "XX", "id": "1234567893" }, "patient": { "entity": "QC", "last_name": "Patient", "first_name": "Sample", "id_qualifier": "MI", "id": "W12345RFAI" }, "payer_claim_number": "PWS1325701407", "patient_control_number": "PW-SBX-RFAI", "service_date": "2026-10-01", "service_date_end": null, "references": [ { "qualifier": "1K", "value": "PWS1325701407", "words": "the payer's claim control number" }, { "qualifier": "EJ", "value": "PW-SBX-RFAI", "words": "the patient control number (the claim's CLM01)" } ], "dates": [ { "qualifier": "472", "format": "D8", "value": "20261001", "words": "the date of service" }, { "qualifier": "106", "format": "D8", "value": "20261118", "words": "the day the payer needs the information by" } ], "due": { "date": "2026-11-18", "source": "DTP*106", "days_left": 30, "overdue": false, "words": "Due by 11/18/2026, in 30 days." }, "requests": [ { "level": "claim", "line_index": null, "element": "STC01", "written": "R4:11506-3::LOI", "category": { "code": "R4", "words": "held for supporting documentation", "known": true }, "loinc": { "code": "11506-3", "valid": true, "known": true, "long_common_name": "Progress Note", "short_name": "Prog Note", "label_source": "loinc", "notice": "This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http:/…" }, "code": "11506-3", "code_is_loinc": true, "entity": null, "code_list_qualifier": "LOI", "effective_date": "2026-10-19", "words": "The payer asks for the Progress Note (LOINC 11506-3) about the claim: the claim is held for supporting documentation." } ], "lines": [], "response_contact": null, "payer_contact": null, "response_275": { "bgn01": "11", "trn01": "2", "trn02": "PWR3503656727", "ref_ej": "PW-SBX-RFAI", "submitter_id": "SAMPLEBILLER", "payer": { "name": "PAYERWORKS SANDBOX PAYER", "id": "PWSANDBOX" }, "payer_contact": null, "provider": { "entity": "1P", "name": "Sample Sleep Dentistry", "first_name": null, "id_qualifier": "XX", "id": "1234567893" }, "patient": { "last_name": "Patient", "first_name": "Sample", "member_id": "W12345RFAI" }, "request": { "trace": "PWR3503656727", "stc01": "R4:11506-3::LOI", "patient_control_number": "PW-SBX-RFAI", "submitter_id": "SAMPLEBILLER" }, "attachments": [ { "control_number": "ATT1", "stc01": "R4:11506-3::LOI", "loinc": "11506-3", "document": "Progress Note", "content_type": null, "content_base64": null } ], "sender_id": "SAMPLEBILLER", "receiver_id": "PWSANDBOX", "cannot_answer": [], "words": "Send a solicited 275 (BGN01 11) under the payer's trace PWR3503656727 (TRN01 2, the same TRN02 on every attachment), as submitter SAMPLEBILLER, with REF*EJ PW-SBX-RFAI, and one attachment for each code asked, each echoing the request's STC …" }, "words": "One request about the claim PW-SBX-RFAI. Due by 11/18/2026, in 30 days." } ], "requests": 1, "shape": { "verified": false, "words": "Read as public sources show a 277 request for additional information (005010X213): X12's interpretation 2045 (the LOINC code in STC01, STC10 and STC11), HL7's Da Vinci attachment mapping (the payer's trace in the claim's TRN, the deadline a…" }, "loinc_notice": "This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http:/…", "today_source": "today", "reads": "Only the file sent in this call: nothing is stored, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "277RFAI" | | | `version` | string | | | `usage` | string or null | | | `created` | string or null | YYYY-MM-DD, or empty when not stated. | | `reference` | string or null | | | `today` | string | YYYY-MM-DD, or empty when not stated. | | `claims` | list of object | | | `requests` | integer | | | `shape` | object | | | `loinc_notice` | string | | | `today_source` | string | | | `reads` | string | | ### POST /v1/attachments/requests/check **Check a 275 against its request.** A solicited 275 (or up to ten) held to the 277 request it answers before you send it: BGN01 11, the payer’s trace on every attachment, the request’s REF*EJ and submitter id, every code asked for answered by an attachment echoing its STC, each file’s BIN length, and the due day. Errors and warnings per 275 set, and per claim whether every code is answered: a set with an error of its own answers no code, what is missing is read across every 275 sent, and a line’s request is answered only by an attachment carrying that line’s REF*FJ. Nothing is stored or sent. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/attachments/requests/check" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAttachmentsRequestsCheck(body); ``` With the Python client ```python answer = pw.post_attachments_requests_check(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SAMPLEBILLER *261019*1500*^*00501*000000004*0*T*:~GS*HN*PWSANDBOX*SAMPLEBILLER*20261019*1500*4*X*005010X213~ST*277*0001*005010X213~BHT*0010*08*PWSANDBOX4*20261019*1500~HL*1**20*1~NM1*PR*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~HL*2*1*21*1~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~HL*3*2*19*1~NM1*1P*2*Sample Sleep Dentistry*****XX*1234567893~HL*4*3*PT~NM1*QC*1*Patient*Sample****MI*W12345RFAI~TRN*1*PWR3503656727~STC*R4:11506-3::LOI*20261019~REF*1K*PWS1325701407~REF*EJ*PW-SBX-RFAI~DTP*472*D8*20261001~DTP*106*D8*20261118~SE*17*0001~GE*1*4~IEA*1*000000004~", "x12_275": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*PWSANDBOX *261019*1500*^*00501*000000008*0*T*:~GS*PI*SAMPLEBILLER*PWSANDBOX*20261019*1500*8*X*005010X210~ST*275*0001*005010X210~BGN*11*PW-SBX-RFAI*20261019~NM1*PR*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~NM1*41*2*Sample Sleep Dentistry*****46*SAMPLEBILLER~NM1*1P*2*Sample Sleep Dentistry*****XX*1234567893~PRV*BI*PXC*1223S0112X~NX1*1P~N3*100 Main Street~N4*Springfield*IL*627011234~NM1*QC*1*Patient*Sample****MI*W12345RFAI~REF*EJ*PW-SBX-RFAI~DTP*472*D8*20261001~LX*1~TRN*2*PWR3503656727~STC*R4:11506-3::LOI~DTP*368*D8*20261019~CAT*AE*IA~EFI*05~BIN*246*Mime-Version: 1.0\r\nContent-Type: application/pdf\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=progress_note.pdf\r\n\r\nJVBERi0xLjQKJSBhbiBpbnZlbnRlZCBhdHRhY2htZW50CjEgMCBvYmo8PD4+ZW5kb2JqCnRyYWls\r\nZXI8PD4+CiUlRU9GCg==~SE*20*0001~GE*1*8~IEA*1*000000008~", "today": "2026-10-19" } ``` The answer (200) ```json { "kind": "277RFAI", "today": "2026-10-19", "today_source": "today", "answers_every_request": true, "claims": [ { "index": 0, "trace": "PWR3503656727", "patient_control_number": "PW-SBX-RFAI", "asked": [ "11506-3" ], "answered": [ "11506-3" ], "missing": [], "missing_requests": [], "answered_by": [ { "file": 0, "set": 0, "answers": true } ], "complete": true, "words": "Every code asked for is answered." } ], "files": [ { "file": "x12_275", "sets": [ { "index": 0, "st_control": "0001", "trace": "PWR3503656727", "claim_index": 0, "answers": true, "errors": [], "warnings": [] } ] } ], "shape": { "verified": false, "words": "Read as public sources show a 277 request for additional information (005010X213): X12's interpretation 2045 (the LOINC code in STC01, STC10 and STC11), HL7's Da Vinci attachment mapping (the payer's trace in the claim's TRN, the deadline a…" }, "loinc_notice": "This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http:/…", "reads": "Only the files sent in this call: nothing is stored, nothing is sent, and no payer is asked." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "277RFAI" | | | `today` | string | YYYY-MM-DD, or empty when not stated. | | `today_source` | string | | | `answers_every_request` | boolean | | | `claims` | list of object | | | `files` | list of object | | | `shape` | object | | | `reads` | string | | | `loinc_notice` | string | LOINC’s notice, whenever the answer names a LOINC code. | ### POST /v1/remittances/from-image **Read a paper EOB into a draft 835.** A photo, scan or PDF of a payer’s paper explanation of benefits (two pages at most, 1.4 MB), read by a model into a DRAFT: every figure, date and code with the reader’s confidence and where on the page it was read, the payments checked by arithmetic against the check total the page states, and an 835 written as a test file. It is never an ERA and nothing is posted: check every field against the paper. Off until Payerworks turns it on; test keys and invented documents only for now, at most 100 reads a UTC day for an account’s keys together; a call refused for the route being off, a live key or no file is not counted. A read has 25 seconds; past them the answer is 504 model_read_timed_out: send one page, or a crop of the part you need. Nothing is kept. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/remittances/from-image" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postRemittancesFromImage(body); ``` With the Python client ```python answer = pw.post_remittances_from_image(body) ``` request.json ```json { "file_base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAoCAAAAABrpaM1AAAAJElEQVR4nGOYSSFgGDVg1IBRA0YNGDVg1IBRA0YNGDVgOBoAAFl9+kzmtZbEAAAAAElFTkSuQmCC" } ``` The answer (200) ```json { "object": "remittance_draft", "draft": true, "needs_review": true, "is_era": false, "posted": false, "words": "A draft read from a picture of a paper explanation of benefits by a model. It is not an ERA, it was not sent by the payer, and nothing has been posted: check every field against the paper before you use it.", "confidence_words": "confidence is the reader's own estimate from 0 to 1, not a measured accuracy.", "region_words": "region is where on the page the reader placed the value: page (1 is the first) and a box as fractions of the page's width and height from its top left. It is approximate, and null where the reader read a PDF's text rather than its picture.", "document": { "media_type": "image/png", "pages": 1, "read_as": "picture" }, "payer": { "name": { "value": "SAMPLE HEALTH PLAN", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } }, "payee": { "name": { "value": "Sample Family Medicine", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "npi": { "value": "1234567893", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "tax_id": { "value": "12-3456789", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } }, "check": { "number": { "value": "CHK10001", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "date": { "value": "2026-10-01", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "total": { "value": 150, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "method": { "value": "Check", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } }, "claims": [ { "patient_name": { "value": "PATIENT, SAMPLE", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "member_id": { "value": "W123456789", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "patient_control_number": { "value": "PW-2001", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "payer_claim_number": { "value": "PCN000123", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "status": { "value": "processed_as_primary", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "billed": { "value": 215, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "allowed": { "value": 150, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "paid": { "value": 120, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "patient_responsibility": { "value": 30, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "service_from": { "value": null, "confidence": null, "region": null, "not_printed": true }, "service_to": { "value": null, "confidence": null, "region": null, "not_printed": true }, "adjustments": [], "lines": [ { "date": { "value": "2026-10-01", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "procedure_code": { "value": "99213", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "modifiers": { "value": null, "confidence": null, "region": null, "not_printed": true }, "units": { "value": 1, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "billed": { "value": 150, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "allowed": { "value": 150, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "paid": { "value": 120, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "adjustments": [ { "group": { "value": "PR", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "reason": { "value": "2", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "amount": { "value": 30, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } } ], "remarks": { "value": null, "confidence": null, "region": null, "not_printed": true } }, { "date": { "value": "2026-10-01", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "procedure_code": { "value": "87880", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "modifiers": { "value": null, "confidence": null, "region": null, "not_printed": true }, "units": { "value": 1, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "billed": { "value": 65, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "allowed": { "value": 0, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "paid": { "value": 0, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "adjustments": [ { "group": { "value": "CO", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "reason": { "value": "97", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "amount": { "value": 65, "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } } ], "remarks": { "value": "N130", "confidence": 0.62, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } } ] } ], "provider_adjustments": [], "totals": { "check_total_stated": 150, "claims_paid": 120, "provider_adjustments": 0, "check_total_computed": 120, "matches": false, "words": "The claims as read do NOT add up to the check total the page states." }, "warnings": [ { "code": "check_total_mismatch", "path": "check.total", "message": "The page states a check total of $150.00, and the claims as read come to $120.00: a difference of $30.00. Something was misread or not read: check every payment against the paper.", "stated": 150, "computed": 120 } ], "review": [ "claims[0].lines[1].remarks", "check.total" ], "x12": { "version": "005010X221A1", "usage": "T", "text": "ISA*00* *00* *ZZ*PWDRAFT *ZZ*PWDRAFT *261019*1500*^*00501*000000001*0*T*:~GS*HP*PWDRAFT*PWDRAFT*20261019*1500*1*X*005010X221A1~ST*835*0001~BPR*I*150*C*CHK************20261001~TRN*1*CHK10001~DTM*405*20261001~N…", "reads_back": true, "not_read": [ { "path": "payer", "element": "TRN03" }, { "path": "claims[0]", "element": "CLP06" } ], "words": "A test interchange (ISA15 T) whatever your key: a draft to check against the paper, never a payer's file. Elements the page did not give are left empty and listed in not_read." }, "mode": "test", "stored": false, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "remittance_draft" | | | `draft` | the value true | | | `needs_review` | the value true | | | `is_era` | the value false | Never an ERA: the payer did not send it. | | `posted` | the value false | Nothing was posted. | | `words` | string | | | `confidence_words` | string | | | `region_words` | string | | | `document` | object | | | `payer` | object | | | `payee` | object | | | `check` | object | | | `claims` | list of object | At most 25 claims and 100 service lines. | | `provider_adjustments` | list of object | | | `totals` | object | The arithmetic, never the model’s: the claims’ payments less the provider adjustments against the check total the page states. | | `warnings` | list of object | | | `review` | list of string | Every path to check first: a value read below 0.8 confidence, a value not read as its kind, and every path a warning names. | | `x12` | object | | | `mode` | one of "test", "live" | | | `stored` | the value false | | | `transmitted` | the value false | | ### POST /v1/payers/from-card **Look up a payer from a card photo.** A photo of an insurance card’s front (and back, if you have it), read twice by a model for the payer’s name, the member ID and its shape, the group number and the plan type the card itself states, and the payer looked up in the Payerworks payer directory. A match means the directory holds published facts about that payer, never that a claim can be sent to it; an electronic payer ID printed on the card is shown only where the payer publishes it. Off until Payerworks turns it on; test keys and invented cards only for now, at most 100 reads a UTC day for an account’s keys together; a call refused for the route being off, a live key or no file is not counted. A read has 25 seconds; past them the answer is 504 model_read_timed_out: send one page, or a crop of the part you need. Nothing is kept. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/payers/from-card" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postPayersFromCard(body); ``` With the Python client ```python answer = pw.post_payers_from_card(body) ``` request.json ```json { "front_base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAoCAAAAABrpaM1AAAAJElEQVR4nGOYSSFgGDVg1IBRA0YNGDVg1IBRA0YNGDVgOBoAAFl9+kzmtZbEAAAAAElFTkSuQmCC" } ``` The answer (200) ```json { "object": "card_lookup", "needs_review": true, "words": "Read from a photo of an insurance card by a model, and matched by name against the Payerworks payer directory. A match says the directory holds published facts about that payer, never that a claim can be sent to it.", "confidence_words": "confidence is the reader's own estimate from 0 to 1, not a measured accuracy.", "region_words": "region is where the reader placed the value: page 1 is the front photo, 2 the back; a box as fractions of that photo from its top left. It is approximate.", "files": [ { "field": "front_base64", "media_type": "image/png", "pages": 1 } ], "payer_name": { "value": "Aetna", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "plan_name": { "value": null, "confidence": null, "region": null, "not_printed": true }, "member_id": { "value": "W123456789", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] }, "reads": "agreed", "other_readings": [], "shape": { "length": 10, "pattern": "A999999999", "letters": 1, "digits": 9, "leading_letters": "W", "mbi_format": false, "words": "It does not have the form of a Medicare Beneficiary Identifier." } }, "group_number": { "value": "0123456", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } }, "plan_type": { "stated": "ppo", "words": { "value": "PPO", "confidence": 0.97, "region": { "page": 1, "box": [ 0.1, 0.2 ] } } }, "printed_payer_id": { "printed": true, "published": true, "value": "60054", "words": "The card prints this electronic payer ID, and a directory row lists it as an ID the payer publishes (the row says where and when it was read)." }, "directory": { "available": true, "matched": true, "matches": [ { "id": "pwd_c28e50f685f059ec", "name": "Aetna", "kind": "organization", "states": [], "nationwide": false, "payer_id": null, "payer_id_words": "This payer publishes more than one claims payer ID; each is listed with what it is for.", "published_payer_ids": [ { "payer_id": "60054", "for": "Commercial and Medicare plan claims and encounters", "transactions": [ "claims" ], "source": { "url": "https://www.aetna.com/document-library/healthcare-professionals/assets/documents/eligibility-benefits-claims-information.pdf", "label": "Aetna: where to go for eligibility, benefits and claims", "read_on": "2026-10-19" } }, { "payer_id": "60055", "for": "Bulk encounter files (Aetna asks to be contacted first)", "transactions": [ "encounters" ], "source": { "url": "https://www.aetna.com/document-library/healthcare-professionals/assets/documents/eligibility-benefits-claims-information.pdf", "label": "Aetna: where to go for eligibility, benefits and claims", "read_on": "2026-10-19" } } ], "match": { "by": "payer_id", "exact": true, "payer_id": "60054" }, "connection": "none", "transmits": false }, { "id": "pwd_65862b816a773b41", "name": "Aetna", "kind": "organization", "states": [ "OH" ], "nationwide": false, "payer_id": null, "payer_id_words": "No payer ID published by the payer that Payerworks has read.", "published_payer_ids": [], "match": { "by": "name", "exact": true }, "connection": "none", "transmits": false } ], "words": "Matched in the Payerworks directory: the directory holds published facts about a payer by this name. It is not a list of payers Payerworks can send to, and it does not say a claim can be sent.", "scope": "Payers and plan programs from public federal files; not a list of payers Payerworks can send to; payer IDs only where the payer publishes one.", "payer_ids_are": "A payer ID is shown only where the payer or its Medicare contractor publishes it, with the page and the day it was read. Clearinghouses can route the same payer under other IDs, and Payerworks is connected to no payer yet." }, "warnings": [], "mode": "test", "stored": false, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "card_lookup" | | | `needs_review` | the value true | | | `words` | string | | | `confidence_words` | string | | | `region_words` | string | | | `files` | list of object | | | `payer_name` | ReadField | | | `plan_name` | ReadField | | | `member_id` | ReadField | | | `group_number` | ReadField | | | `plan_type` | object | | | `printed_payer_id` | object | | | `directory` | object | | | `warnings` | list of object | | | `mode` | one of "test", "live" | | | `stored` | the value false | | | `transmitted` | the value false | | ## Reading the answer - A PDF answer is `pdf_base64`, `sha256` and `bytes`, with `stored: false` and `transmitted: false`. Decode the base64 to get the file. - A 275 answer is the `x12` with `shape.verified: false`: it is written as public payer companion guides show the 275, not from the licensed implementation guide. Check your payer’s own guide. - A request for more information: each claim’s `requests` name what is asked (`known: false` where Payerworks has no words for a code), `due` says by when against your `today`, and `response_275` holds every value the solicited 275 must carry: the payer’s trace on every attachment (`trn02`), the request’s REF*EJ and the submitter id. The request is read as public sources show it (`shape.verified: false`). - A check answers `answers_every_request`, and per claim the codes `asked`, `answered` and `missing` and each request not yet answered (`missing_requests`, a line’s request naming its line), with each 275 set’s errors and warnings by field. A set with an error of its own answers no code, and what is missing is read across every 275 sent, so an answer split across two files counts. - A model read is a draft a person checks: every value with the reader’s confidence and where it was read, totals checked by arithmetic, and `is_era: false`. It has 25 seconds. ## Troubleshooting - `model_read_not_offered` (501): The picture reads are off on this host until Payerworks turns them on. A 501 is never retried by the clients: it will not pass by waiting. - `invalid_attachments` (422): Each attachment is refused with its field, for example a control number the claim does not announce with transmission EL. - `not_medicare_fee_for_service` (422): The appeal forms are Original Medicare’s; the 835 must say the claim is Medicare fee-for-service. - `documents_unavailable` (503): The part that draws PDFs was not loaded: retry in a moment. - `not_an_information_request` (422): The file is not a 277 request for additional information (005010X213): the error names what it is, and the route that reads it. A claim status 277 goes to POST /v1/claim-status/explain. - `no_attachment_response` (400): The check needs the solicited 275 too, as x12_275: the file’s text, or a list of them. - A 275 written under the claim’s own number, not the payer’s trace, goes unmatched and the claim is decided without it: copy `trn02`, the REF*EJ and the submitter id from `response_275`. - `model_read_timed_out` (504): A picture read did not finish in its 25 seconds: send one page, or a crop of the part you need. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_document_option` | 400 | An option of this document is not one the route takes: the error names it and what it takes. | | `invalid_image_base64` | 400 | The file is not base64: send the file's bytes base64 encoded (A-Z, a-z, 0-9, + and /, with = padding), with no data: prefix. | | `invalid_information_request_call` | 400 | A field of the request is not in the shape this route takes; each problem is listed with its field. | | `no_attachment_response` | 400 | Send the solicited 275 to check as x12_275: the file's text, or a list of them. | | `no_image` | 400 | Send the file as base64: file_base64 for a paper EOB, front_base64 (and back_base64 if you have it) for an insurance card. | | `no_information_request` | 400 | Send the payer's 277 request for additional information as x12 (the file's text). | | `no_remittance_file` | 400 | Send the 835 as x12 (the file's text). | | `too_many_forms` | 400 | These claims need more CMS-1500 forms than one call draws (200, at six service lines a form): send fewer claims in each call. | | `unknown_appeal_form` | 400 | form is redetermination (CMS-20027) or reconsideration (CMS-20033). | | `model_read_test_keys_only` | 403 | This route answers test keys only, with invented documents, until an agreement covering the model is in place for live data. Use a test key. | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `image_too_large` | 413 | The file is larger than one call reads: at most 1,400,000 bytes in all, before base64. Send a smaller copy (a lower resolution photo, or fewer pages). | | `too_many_pages` | 413 | The PDF holds more pages than one call reads: the error names the most (limit). Send fewer pages in each call. | | `claim_not_on_remittance` | 422 | The 835 holds no claim payment with this reference (CLP01); send the reference the denying claim payment carries. | | `invalid_appeal_form_input` | 422 | One or more fields cannot be typed into the form; nothing was filled. Each problem is listed with its field. | | `invalid_attachments` | 422 | The 275 cannot be written as asked; nothing was written. Each problem is listed with its field. | | `invalid_claims` | 422 | One or more claims would be refused; nothing was written or run. Each claim's errors are listed. | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | | `no_requested_claims` | 422 | This request holds no claim (no TRN in a patient loop), so there is nothing to say or check. | | `not_an_835` | 422 | This is not an 835 remittance. | | `not_an_attachment_response` | 422 | A file sent as x12_275 is not a 275 this route can read: the error names the file and why. | | `not_an_eob` | 422 | The reader did not see a payer's explanation of benefits or remittance advice in this file, so no draft was written. | | `not_an_information_request` | 422 | This file is not a 277 request for additional information (005010X213): the error names what it is, and the route that reads it. | | `not_an_insurance_card` | 422 | The reader did not see a health insurance card in this file, so nothing was looked up. | | `not_medicare_fee_for_service` | 422 | These are Original Medicare's forms, and the 835 says this claim is not Medicare fee-for-service (CLP06 is not MA or MB): use the plan's own appeal, or POST /v1/appeals/letter. | | `unreadable_image` | 422 | The file could not be read as a picture or a PDF; the error says why (reason) and what to send instead. | | `model_read_daily_limit` | 429 | This account's keys have made the most model-read calls an account may make on this route today (100 a UTC day, its keys together). Try again tomorrow. | | `model_read_not_offered` | 501 | Reading a document with a model is not offered on this host: Payerworks turns it on route by route. This is not a passing fault, so sending the request again answers the same. | | `model_read_failed` | 502 | The reader gave no usable answer for this file, so nothing is answered from it. Send a clearer copy, or try again. | | `documents_unavailable` | 503 | The PDF could not be drawn just now: the part of the API that draws it is not loaded. Retry in a moment. The engine run on its own, in a browser or a test, draws no PDF; the hosted Payerworks API, at the base URL shown with your key, does. | | `form_asset_changed` | 503 | The form file on our side is not the one CMS published that we pinned, so nothing was filled. This is ours to fix. | | `model_read_unavailable` | 503 | The document reader could not be loaded just now, so nothing was read. Retry in a moment. The engine run on its own, in a browser or a test, has no reader; the hosted Payerworks API does. | | `model_read_timed_out` | 504 | The read did not finish inside one call's time. Send less: one page, or a crop of the part you need. | ## Code lists ### LOINC This material contains content from LOINC (http://loinc.org). LOINC is copyright © Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee and is available at no cost under the license at http://loinc.org/license. LOINC® is a registered United States trademark of Regenstrief Institute, Inc. ## Example [documents.mjs](https://payerworks.com/docs/examples/documents.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. documents.mjs ```js // Payerworks example: documents, a CMS-1500 drawn and a 275 written. Docs: https://payerworks.com/docs/documents // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node documents.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { CLAIM, ELIGIBILITY_QUESTION, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The claim drawn on the CMS-1500, after the same checks the 837P route makes. The PDF comes back as base64. const form = await pw.postClaimsCms1500({ claim: copy(CLAIM), form: 'full', signed_on: '2026-10-20' }); const pdf = Buffer.from(form.pdf_base64, 'base64'); // 2. An eligibility answer printed: the sandbox's 271, as POST /v1/eligibility/explain reads it. const sandbox = await pw.postSandboxEligibility({ request: copy(ELIGIBILITY_QUESTION) }); const summary = await pw.postEligibilityPdf({ x12: sandbox.files.eligibility_271 }); // 3. The failure path: reading a paper EOB with a model is built and switched off until Payerworks // turns it on. A 501 will not pass by waiting, so the clients never retry it. const off = await refusalOf(() => pw.postRemittancesFromImage({ file_base64: 'iVBORw0KGgo=' }), { PayerworksError }); return { cms1500: { starts: pdf.subarray(0, 5).toString('latin1'), sha256: form.sha256.length, stored: form.stored, transmitted: form.transmitted }, eligibility_pdf: summary.pdf_base64.startsWith('JVBERi0'), off, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "cms1500": { "starts": "%PDF-", "sha256": 64, "stored": false, "transmitted": false }, "eligibility_pdf": true, "off": { "status": 501, "code": "model_read_not_offered", "request_id": "req_4" } } ``` # Prior authorization > Ask for an authorization from JSON, and read the payer’s response as what was certified and what to do next. Page: https://payerworks.com/docs/prior-authorization · Markdown: https://payerworks.com/docs/prior-authorization.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Write the review request as a 278 from JSON, send it the way the payer takes it, and give Payerworks the response that comes back to read. The authorization sandbox answers from the same payer as the eligibility sandbox. POST /v1/authorizations/required checks codes against Medicare’s published prior authorization lists. ## Routes - [POST /v1/authorizations/278](#post-v1-authorizations-278): Write a 278 - [POST /v1/authorizations/explain](#post-v1-authorizations-explain): Explain a 278 response - [POST /v1/sandbox/authorizations](#post-v1-sandbox-authorizations): Run the prior authorization sandbox - [POST /v1/authorizations/required](#post-v1-authorizations-required): Does Medicare require it Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/authorizations/278 **Write a 278.** Up to one hundred prior authorization or referral requests, from JSON (the kind of review, the service type, each procedure code with its units and dates, the requester, the servicing provider, the patient and the diagnoses), written as one 005010X217 interchange for you to send on your own connection, or built in the shape of the Da Vinci PAS request bundle with output set to fhir. Each request is checked field by field first, and nothing is written if any would be refused. A test key writes test files only. Whether a payer takes a 278 at all is the payer’s choice: many take prior authorizations only on a portal, by fax or by phone. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/authorizations/278" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAuthorizations278(body); ``` With the Python client ```python answer = pw.post_authorizations_278(body) ``` request.json ```json { "sender_id": "SAMPLEBILLER", "receiver_id": "99999", "control_number": 31, "request": { "controlNumber": "AUTH-0001", "tradingPartnerServiceId": "99999", "requester": { "organizationName": "Sample Family Medicine", "npi": "1234567893" }, "subscriber": { "memberId": "W123456789", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "1980-02-14", "gender": "F" }, "review": { "serviceTypeCode": "12", "placeOfServiceCode": "12", "diagnoses": [ "M17.11" ] }, "services": [ { "procedureCode": "L1833", "units": 4, "serviceDate": "2026-11-02" } ] } } ``` The answer (200) ```json { "version": "005010X217", "usage": "T", "x12": "ISA*00* *00* *ZZ*SAMPLEBILLER *ZZ*99999 *261019*1500*^*00501*000000031*0*T*:~GS*HI*SAMPLEBILLER*99999*20261019*1500*31*X*005010X217~ST*278*0001*005010X217~BHT*0007*13*AUTH-0001*20261019*1500~HL*1**20*1~NM1*X3*2*…", "interchange_control_number": "000000031", "group_control_number": "31", "transaction_set_control_numbers": [ "0001" ], "segments": 22, "requests": [ { "index": 0, "trace": "AUTH-0001", "transaction_set_control_number": "0001", "warnings": [] } ], "payer_choice": "Payerworks sends nothing. Whether a payer accepts an X12 278 or a FHIR prior authorization request at all is the payer's choice: many take them only on a portal, by fax or by phone, and its companion guide says what it requires.", "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `version` | string | | | `usage` | one of "T", "P" | | | `x12` | string | | | `interchange_control_number` | string | | | `group_control_number` | string | | | `transaction_set_control_numbers` | list of string | | | `segments` | integer | | | `fhir` | object | With output fhir or both: the implementation guide named, a note that the shape is not validated, and each request’s bundle. | | `requests` | list of object | | | `payer_choice` | string | That Payerworks sends nothing, and whether a payer takes a 278 at all is the payer’s choice. | | `transmitted` | the value false | | ### POST /v1/authorizations/explain **Explain a 278 response.** A payer’s 278 response, as X12, as Payerworks’ own JSON read of one, or as a PAS response, said per review: approved, approved in part, approved with changes, not approved, pended, waiting on more information, canceled, no review needed, or not reviewed at all, with the reason in words, the certification and reference numbers, the approved dates and units against what you asked, the claim field the number goes in, and the next step. A request the payer never reviewed is never called a denial. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/authorizations/explain" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAuthorizationsExplain(body); ``` With the Python client ```python answer = pw.post_authorizations_explain(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SUBMITTER *261019*1500*^*00501*000000002*0*T*:~GS*HI*PWSANDBOX*SUBMITTER*20261019*1500*2*X*005010X217~ST*278*0001*005010X217~BHT*0007*11*AUTH-0001*20261019*1500*18~HL*1**20*1~NM1*X3*2*PAYERWORKS SANDBOX PAYER*****PI*PWSANDBOX~HL*2*1*21*1~NM1*1P*2*Sample Family Medicine*****XX*1234567893~HL*3*2*22*1~NM1*IL*1*PATIENT*SAMPLE****MI*W12345PARTIAL~DMG*D8*19800214*F~HL*4*3*EV*1~TRN*2*AUTH-0001*9SUBMITTER~UM*HS*I*12*12:B~HCR*A2*PWA033425780*10~REF*NT*PWR951115437~DTP*AAH*RD8*20261102-20270130~DTP*102*D8*20261019~DTP*007*RD8*20261102-20270130~DTP*036*D8*20270130~HI*ABK:M1711~HL*5*4*SS*0~TRN*2*852346626*9SUBMITTER~HCR*A2*PWA033425780*10~DTP*472*D8*20261102~SV1*HC:L1833**UN*2~SE*25*0001~GE*1*2~IEA*1*000000002~", "asked": { "controlNumber": "AUTH-0001", "tradingPartnerServiceId": "PWSANDBOX", "requester": { "organizationName": "Sample Family Medicine", "npi": "1234567893" }, "subscriber": { "memberId": "W12345PARTIAL", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "1980-02-14", "gender": "F" }, "review": { "serviceTypeCode": "12", "placeOfServiceCode": "12", "diagnoses": [ "M17.11" ] }, "services": [ { "procedureCode": "L1833", "units": 4, "serviceDate": "2026-11-02" } ] } } ``` The answer (200) ```json { "kind": "278", "source": "x12", "envelope": { "sender_qualifier": "ZZ", "sender": "PWSANDBOX", "receiver_qualifier": "ZZ", "receiver": "SUBMITTER", "date": "261019", "time": "1500", "control_number": "000000002", "usage": "T", "functional_group": "HI", "version": "005010X217", "transaction_sets": { "278": 1 }, "segments": 28 }, "reviews": [ { "index": 0, "trace": "AUTH-0001", "purpose": "response", "payer": { "name": "PAYERWORKS SANDBOX PAYER", "id": "PWSANDBOX", "entity": "X3" }, "requester": { "name": "Sample Family Medicine", "id_qualifier": "XX", "id": "1234567893", "entity": "1P" }, "subscriber": { "name": "SAMPLE PATIENT", "member_id": "W12345PARTIAL", "birth_date": "1980-02-14", "gender": "F" }, "dependent": null, "request_category": { "code": "HS", "words": "health services review: outpatient services", "known": true }, "service_type": { "code": "12", "words": "durable medical equipment, purchased", "known": true, "verified_in_005010": true, "later_words": "" }, "verdict": "partially_approved", "words": "Approved in part, certification number PWA033425780: fewer units than asked on one service.", "next_step": "Bill only what was approved, with PWA033425780 as priorAuthorizationNumber (2300 REF*G1). For what was not, ask the payer to reconsider (a 278 with certificationType N and the administrative reference number, where the payer takes one) or f…", "certification_number": "PWA033425780", "administrative_reference_number": "PWR951115437", "previous_authorization_number": "", "decision": { "code": "A2", "words": "certified in part", "verdict": "partially_approved", "known": true, "certification_number": "PWA033425780", "second_surgical_opinion": "" }, "reason": { "code": "10", "words": "the delivery pattern asked (units, days, visits, weeks, hours or months)", "kind": "denial", "known": true }, "rejections": [], "dates": { "event": { "begin": "2026-11-02", "end": "2027-01-30" }, "admission": "", "certification": { "issued": "2026-10-19", "effective": { "begin": "2026-11-02", "end": "2027-01-30" }, "expires": "2027-01-30" } }, "services": [ { "index": 0, "trace": "852346626", "procedure": { "qualifier": "HC", "code": "L1833", "modifiers": [] }, "verdict": "partially_approved", "decision": { "code": "A2", "words": "certified in part", "verdict": "partially_approved", "known": true, "certification_number": "PWA033425780" }, "reason": { "code": "10", "words": "the delivery pattern asked (units, days, visits, weeks, hours or months)", "known": true }, "units": 2, "units_asked": 4, "units_cut": 2, "dates": { "begin": "2026-11-02", "end": "2026-11-02" }, "dates_asked": { "begin": "2026-11-02", "end": "2026-11-02" }, "dates_changed": false, "quantities": [], "administrative_reference_number": "", "messages": [] } ], "quantities": [], "messages": [], "claim": { "prior_authorization_number": "PWA033425780", "field": "claimInformation.claimSupplementalInformation.priorAuthorizationNumber", "segment": "2300 REF*G1", "route": "POST /v1/claims/837p" }, "notes": [ "From 01/01/2026, CMS-0057-F (89 FR 8758, 02/08/2024) requires Medicare Advantage organizations, Medicaid and CHIP programs and plans, and QHP issuers on the federal exchanges to give a specific reason for a denied prior authorization (drugs…" ], "label_source": "paraphrase" } ], "label_source": "paraphrase" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "278" | | | `source` | one of "x12", "json", "fhir" | | | `envelope` | Envelope or null | | | `reviews` | list of AuthorizationReview | | | `answer` | object | Only with ?include=answer: the 278 read to JSON. | | `label_source` | the value "paraphrase" | | ### POST /v1/sandbox/authorizations **Run the prior authorization sandbox.** The round trip on invented data: your requests written as a 278 and the sandbox payer’s 278 response, chosen by how each member ID ends, read back and explained in the same call. Any key may call it, every file is a test file, and nothing reaches a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/authorizations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSandboxAuthorizations(body); ``` With the Python client ```python answer = pw.post_sandbox_authorizations(body) ``` request.json ```json { "request": { "controlNumber": "AUTH-0001", "tradingPartnerServiceId": "PWSANDBOX", "requester": { "organizationName": "Sample Family Medicine", "npi": "1234567893" }, "subscriber": { "memberId": "W12345PARTIAL", "firstName": "SAMPLE", "lastName": "PATIENT", "dateOfBirth": "1980-02-14", "gender": "F" }, "review": { "serviceTypeCode": "12", "placeOfServiceCode": "12", "diagnoses": [ "M17.11" ] }, "services": [ { "procedureCode": "L1833", "units": 4, "serviceDate": "2026-11-02" } ] } } ``` The answer (200) ```json { "usage": "T", "transmitted": false, "control": { "interchange_request": "000000001", "interchange_response": "000000002", "sets": [ "0001" ] }, "files": { "authorization_request_278": "ISA*00* *00* *ZZ*SUBMITTER *ZZ*PWSANDBOX *261019*1500*^*00501*000000001*0*T*:~GS*HI*SUBMITTER*PWSANDBOX*20261019*1500*1*X*005010X217~ST*278*0001*005010X217~BHT*0007*13*AUTH-0001*20261019*1500~HL*1**20*1~NM1*X3*2*…", "authorization_response_278": "ISA*00* *00* *ZZ*PWSANDBOX *ZZ*SUBMITTER *261019*1500*^*00501*000000002*0*T*:~GS*HI*PWSANDBOX*SUBMITTER*20261019*1500*2*X*005010X217~ST*278*0001*005010X217~BHT*0007*11*AUTH-0001*20261019*1500*18~HL*1**20*1~NM1*X3…" }, "outcomes": [ { "index": 0, "trace": "AUTH-0001", "outcome": "partially_approved", "trigger": "PARTIAL" } ], "requests": [ { "index": 0, "trace": "AUTH-0001", "warnings": [] } ], "explained": [ { "index": 0, "trace": "AUTH-0001", "purpose": "response", "payer": { "name": "PAYERWORKS SANDBOX PAYER", "id": "PWSANDBOX", "entity": "X3" }, "requester": { "name": "Sample Family Medicine", "id_qualifier": "XX", "id": "1234567893", "entity": "1P" }, "subscriber": { "name": "SAMPLE PATIENT", "member_id": "W12345PARTIAL", "birth_date": "1980-02-14", "gender": "F" }, "dependent": null, "request_category": { "code": "HS", "words": "health services review: outpatient services", "known": true }, "service_type": { "code": "12", "words": "durable medical equipment, purchased", "known": true, "verified_in_005010": true, "later_words": "" }, "verdict": "partially_approved", "words": "Approved in part, certification number PWA033425780: fewer units than asked on one service.", "next_step": "Bill only what was approved, with PWA033425780 as priorAuthorizationNumber (2300 REF*G1). For what was not, ask the payer to reconsider (a 278 with certificationType N and the administrative reference number, where the payer takes one) or f…", "certification_number": "PWA033425780", "administrative_reference_number": "PWR951115437", "previous_authorization_number": "", "decision": { "code": "A2", "words": "certified in part", "verdict": "partially_approved", "known": true, "certification_number": "PWA033425780", "second_surgical_opinion": "" }, "reason": { "code": "10", "words": "the delivery pattern asked (units, days, visits, weeks, hours or months)", "kind": "denial", "known": true }, "rejections": [], "dates": { "event": { "begin": "2026-11-02", "end": "2027-01-30" }, "admission": "", "certification": { "issued": "2026-10-19", "effective": { "begin": "2026-11-02", "end": "2027-01-30" }, "expires": "2027-01-30" } }, "services": [ { "index": 0, "trace": "852346626", "procedure": { "qualifier": "HC", "code": "L1833", "modifiers": [] }, "verdict": "partially_approved", "decision": { "code": "A2", "words": "certified in part", "verdict": "partially_approved", "known": true, "certification_number": "PWA033425780" }, "reason": { "code": "10", "words": "the delivery pattern asked (units, days, visits, weeks, hours or months)", "known": true }, "units": 2, "units_asked": 4, "units_cut": 2, "dates": { "begin": "2026-11-02", "end": "2026-11-02" }, "dates_asked": { "begin": "2026-11-02", "end": "2026-11-02" }, "dates_changed": false, "quantities": [], "administrative_reference_number": "", "messages": [] } ], "quantities": [], "messages": [], "claim": { "prior_authorization_number": "PWA033425780", "field": "claimInformation.claimSupplementalInformation.priorAuthorizationNumber", "segment": "2300 REF*G1", "route": "POST /v1/claims/837p" }, "notes": [ "From 01/01/2026, CMS-0057-F (89 FR 8758, 02/08/2024) requires Medicare Advantage organizations, Medicaid and CHIP programs and plans, and QHP issuers on the federal exchanges to give a specific reason for a denied prior authorization (drugs…" ], "label_source": "paraphrase" } ], "payer_choice": "Payerworks sends nothing. Whether a payer accepts an X12 278 or a FHIR prior authorization request at all is the payer's choice: many take them only on a portal, by fax or by phone, and its companion guide says what it requires." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `usage` | the value "T" | | | `transmitted` | the value false | | | `control` | object | | | `files` | object | | | `outcomes` | list of object | | | `requests` | list of object | | | `explained` | list of AuthorizationReview | | | `payer_choice` | string | | ### POST /v1/authorizations/required **Does Medicare require it.** Up to one hundred procedure codes checked against CMS’s published Medicare fee-for-service lists on a date of service: the DMEPOS Required Prior Authorization List and the hospital outpatient department list, each named and dated, with the state or the setting asked for where the list depends on it. A code on neither list is said as not on the lists held, never as not required, and for any other payer the answer is that no published list is held. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/authorizations/required" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postAuthorizationsRequired(body); ``` With the Python client ```python answer = pw.post_authorizations_required(body) ``` request.json ```json { "procedure_codes": [ "L1833", "L3761", "E0601" ], "date_of_service": "2026-11-02", "payer_type": "medicare_fee_for_service" } ``` The answer (200) ```json { "date_of_service": "2026-11-02", "payer_type": "medicare_fee_for_service", "state": null, "setting": null, "payer_choice": "Payerworks sends nothing. Whether a payer accepts an X12 278 or a FHIR prior authorization request at all is the payer's choice: many take them only on a portal, by fax or by phone, and its companion guide says what it requires.", "codes": [ { "code": "L1833", "kind": "hcpcs", "required": true, "lists": [ { "list": "dmepos", "name": "CMS DMEPOS Required Prior Authorization List", "url": "https://www.cms.gov/research-statistics-data-and-systems/monitoring-programs/medicare-ffs-compliance-programs/dmepos/downloads/dmepos_pa_required-prior-authorization-list.pdf", "updated": "07/29/2026", "read": "10/19/2026", "group": "Orthoses", "words": "knee orthosis, adjustable joints, positional, off the shelf", "required": true, "from": "2026-10-28", "why": "nationwide" } ], "words": "L1833 needs prior authorization for Medicare fee-for-service on 11/02/2026: it is on the DMEPOS Required Prior Authorization List, required from 10/28/2026." }, { "code": "L3761", "kind": "hcpcs", "required": "depends_on_state", "lists": [ { "list": "dmepos", "name": "CMS DMEPOS Required Prior Authorization List", "url": "https://www.cms.gov/research-statistics-data-and-systems/monitoring-programs/medicare-ffs-compliance-programs/dmepos/downloads/dmepos_pa_required-prior-authorization-list.pdf", "updated": "07/29/2026", "read": "10/19/2026", "group": "Orthoses", "words": "elbow orthosis, adjustable locking joint, off the shelf", "required": "depends_on_state", "from": "2026-10-28", "states": [ "NY", "MI" ], "nationwide_from": "2027-04-26", "why": "phased" } ], "words": "L3761 is on the DMEPOS Required Prior Authorization List, phased in by state: on 11/02/2026 it is required where the beneficiary lives in NY, MI, FL, CA, and nationwide from 04/26/2027. Send the state the beneficiary lives in to settle it." } ], "sources": [ { "name": "CMS DMEPOS Required Prior Authorization List", "url": "https://www.cms.gov/research-statistics-data-and-systems/monitoring-programs/medicare-ffs-compliance-programs/dmepos/downloads/dmepos_pa_required-prior-authorization-list.pdf", "updated": "07/29/2026", "read": "10/19/2026" }, { "name": "CMS Hospital Outpatient Department Services That Require Prior Authorization", "url": "https://www.cms.gov/files/document/opd-services-require-prior-authorization.pdf", "updated": "09/25/2026", "read": "10/19/2026" } ], "note": "Medicare fee-for-service claims for these items, by the date of delivery (for a rental, of the initial rental claim). It does not apply to Medicare Advantage plans. Medicare fee-for-service services furnished in a hospital outpatient depart…" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `date_of_service` | string | | | `payer_type` | string | | | `state` | string or null | | | `setting` | string or null | | | `payer_choice` | string | | | `codes` | list of object | | | `sources` | list of object | | | `note` | string | | ## Reading the answer - Each review in an explain answer has a `verdict` and its `words`, the `decision` and `reason` codes in words, the `certification_number` when the payer gave one, and the `next_step`. - A request to the sandbox with a member ID the eligibility sandbox cannot find is not found here either, and a claim the claims sandbox denies is not certified here. ## Troubleshooting - `invalid_authorization_requests` (422): One request would be refused, so nothing was written; each request’s errors are listed. - `not_a_278_response` (422): This 278 holds requests only: send the payer’s response. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_asked_requests` | 400 | asked is the review request, or the list of them, that the response answers. | | `invalid_date_of_service` | 400 | date_of_service is a day, YYYY-MM-DD. | | `invalid_explain_include` | 400 | include names what to add to the answer: answer (the read of the 278). | | `invalid_output` | 400 | output is "x12" (the default), "fhir" (the Da Vinci PAS bundle shape) or "both". | | `invalid_payer_type` | 400 | payer_type is medicare_fee_for_service, medicare_advantage, medicaid, commercial or other. | | `invalid_procedure_codes` | 400 | Each procedure code is a HCPCS Level II code (a letter and four digits) or a five-character CPT code. | | `invalid_sender_id` | 400 | sender_id is your submitter id at the receiver: 2 to 15 letters, digits or spaces. | | `invalid_setting` | 400 | setting is hospital_outpatient or other (where the service is furnished), or left out. | | `invalid_state` | 400 | state is the two-letter state the patient lives in, such as CA. | | `no_authorization_answer` | 400 | Send the 278 response as `x12` (the file's text), as `json` (Payerworks' own read of one) or as `fhir` (a Da Vinci PAS response Bundle or ClaimResponse). | | `no_authorization_request` | 400 | The body has no review request: send `requests` (a list), `request`, or the request itself in the JSON shape the docs describe. | | `no_procedure_codes` | 400 | Send `procedure_codes`: a list of HCPCS or CPT codes, such as L1833 or 64490. | | `too_many_authorization_requests` | 400 | At most 100 review requests a call. | | `too_many_procedure_codes` | 400 | At most 100 procedure codes a call. | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `answer_too_large` | 413 | This answer is larger than one response can carry: send less in each call (split the file, or send fewer claims or requests). | | `file_too_large` | 413 | This file holds more than one call can read: the error names what was counted (`unit`) and the most one call reads (`limit`). Split it and send each part in its own call. | | `duplicate_authorization_trace` | 422 | Two review requests in one file carry the same trace (controlNumber); each must be unique so its response matches back. | | `invalid_authorization_requests` | 422 | One or more review requests would be refused; nothing was written. Each request's errors are listed. | | `not_a_278` | 422 | This is not an X12 278 request for review and response (005010X217): send the 278 with its ISA envelope. | | `not_a_278_json` | 422 | This JSON is not a read 278: it holds no `reviews` with a decision or a rejection. | | `not_a_278_response` | 422 | This 278 holds requests only (BHT02 13): send the payer's response (BHT02 11). | | `not_a_pas_response` | 422 | This FHIR resource is not a PAS response: send a Bundle holding a ClaimResponse, or the ClaimResponse, with a reviewAction or an error. | ## Code lists ### The prior authorization sandbox’s member ID endings It answers from the same payer as the eligibility sandbox, and the endings the two share answer alike: a member the eligibility sandbox cannot find is not found here either, and a claim the claims sandbox denies is not certified here. These are its endings: | Ending | Outcome | What it answers | | --- | --- | --- | | `NOTFOUND` | `subscriber_not_found` | A member ID ending in NOTFOUND is not found: the response refuses in the subscriber’s loop with AAA 75 and reviews nothing. | | `BADDOB` | `birth_date_mismatch` | A member ID ending in BADDOB is found, but the birth date does not match: AAA 71 in the patient’s loop, and nothing reviewed. | | `DOWN` | `payer_unavailable` | A member ID ending in DOWN reaches a payer that cannot answer: AAA 42 in the payer’s loop, follow-up R. Send it again later, unchanged. | | `REJ` | `member_id_invalid` | A member ID ending in REJ is refused as a member ID the payer does not accept: AAA 72 in the subscriber’s loop, the same refusal the claims sandbox gives. | | `INACTIVE` | `not_eligible` | A member ID ending in INACTIVE is not certified because the patient was not covered on the date of service: HCR A3 with reason 53, as the eligibility sandbox ends that coverage last year. | | `NOTREQ` | `not_required` | A member ID ending in NOTREQ needs no authorization: HCR NA with reason 0H, and no certification number. | | `PARTIAL` | `partially_approved` | A member ID ending in PARTIAL is certified in part: each service asked for two or more units is approved for half of them, rounded down (HCR A2, reason 10); a service of one unit is approved in full. | | `MODDATE` | `modified_dates` | A member ID ending in MODDATE is certified with changes (HCR A6): the dates approved start two weeks after the dates asked and run for thirty days. | | `DENY` | `denied` | A member ID ending in DENY is not certified as not medically necessary (HCR A3, reason 0F), as the claims sandbox denies the claim. | | `INFO` | `more_information_needed` | A member ID ending in INFO is pended until the payer has more patient information (HCR A4, reason 0U), and a message names the visit notes it needs. | | `PEND` | `pended` | A member ID ending in PEND is pended while the first utilization review runs (HCR A4, reason 30), with an administrative reference number to follow it by. | | `` | `approved` | Any other member ID is certified in total (HCR A1) with a certification number, every service approved for the units asked, effective from the first date asked for at least ninety days. | ## Example [prior-authorization.mjs](https://payerworks.com/docs/examples/prior-authorization.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. prior-authorization.mjs ```js // Payerworks example: prior authorization, a 278 written and its response read. Docs: https://payerworks.com/docs/prior-authorization // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node prior-authorization.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { AUTHORIZATION_REQUEST, SENDER_ID, PAYER_ID, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The request written as a 278, to send the way the payer takes it. const written = await pw.postAuthorizations278({ sender_id: SENDER_ID, receiver_id: PAYER_ID, control_number: 31, request: copy(AUTHORIZATION_REQUEST) }); // 2. The authorization sandbox answers it with a 278 response, already explained. const request = { ...copy(AUTHORIZATION_REQUEST), tradingPartnerServiceId: 'PWSANDBOX' }; const sandbox = await pw.postSandboxAuthorizations({ request }); // 3. Explain a response you hold: the same reading, from the file alone. const explained = await pw.postAuthorizationsExplain({ x12: sandbox.files.authorization_response_278 }); // 4. The failure path: the request you sent is not the payer's response, and the route says so. const refused = await refusalOf(() => pw.postAuthorizationsExplain({ x12: written.x12 }), { PayerworksError }); return { written: { transmitted: written.transmitted }, verdict: explained.reviews[0].verdict, next_step: Boolean(explained.reviews[0].next_step), refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "written": { "transmitted": false }, "verdict": "approved", "next_step": true, "refused": { "status": 422, "code": "not_a_278_response", "request_id": "req_4" } } ``` # Patient estimates and money > What a patient may owe, figured from what you send, and a hospital’s standard charges file checked against the published template. Page: https://payerworks.com/docs/patient-money · Markdown: https://payerworks.com/docs/patient-money.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. What a patient may owe, figured from what you send: an estimate from a 271 and the allowed amounts you expect, a Good Faith Estimate laid out with the elements the rule requires, the replacement window for equipment, and income against the HHS poverty guideline. POST /v1/price-transparency/validate checks a hospital’s standard charges file against the published template. ## Routes - [POST /v1/estimates](#post-v1-estimates): Estimate a patient’s share - [POST /v1/estimates/good-faith](#post-v1-estimates-good-faith): Lay out a Good Faith Estimate - [POST /v1/dme/replacement-window](#post-v1-dme-replacement-window): When equipment may be replaced - [POST /v1/financial-assistance/fpl](#post-v1-financial-assistance-fpl): Income against the poverty guideline - [POST /v1/price-transparency/validate](#post-v1-price-transparency-validate): Check a hospital standard charges file Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/estimates **Estimate a patient’s share.** A patient’s share and the plan’s, line by line, from a 271 (X12 or JSON) and the allowed amounts you send, at most 50 lines. You say the network level and, where a copay is stated, how it applies (copay_rule): a 271 says neither. The lines draw on the deductible and the out-of-pocket amount left in the order sent; for one member, a family amount left the 271 states bounds the member’s, and since a 271 does not say whether a family deductible is embedded or aggregate, the answer says which it read and what the other would mean. Where the out-of-pocket limit caps a line, its deductible and coinsurance are cut to add up to the share. Every input names where it came from: the 271, you, or not stated. A benefit the 271 did not state stops that line rather than reading as zero, unless you supply it, and no allowed amount is ever supplied for you: Payerworks holds no prices. The payer’s own processing decides what the patient owes. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/estimates" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEstimates(body); ``` With the Python client ```python answer = pw.post_estimates(body) ``` request.json ```json { "x12": "ISA*00* *00* *ZZ*SAMPLEPAYER *ZZ*SAMPLEBILLER *261018*0900*^*00501*000000091*0*T*:~\nGS*HB*SAMPLEPAYER*SAMPLEBILLER*20261018*0900*91*X*005010X279A1~\nST*271*0001*005010X279A1~\nBHT*0022*11*ELIG-0001*20261018*0900~\nHL*1**20*1~\nNM1*PR*2*SAMPLE HEALTH PLAN*****PI*99999~\nHL*2*1*21*1~\nNM1*1P*2*SAMPLE FAMILY MEDICINE*****XX*1234567893~\nHL*3*2*22*0~\nTRN*2*ELIG-0001*9SAMPLEBIL~\nNM1*IL*1*PATIENT*SAMPLE****MI*W123456789~\nREF*6P*G7700*SAMPLE EMPLOYER GROUP~\nDMG*D8*19800214*F~\nDTP*346*D8*20260101~\nEB*1*IND*30*PR*SAMPLE PPO~\nEB*C*IND*30***23*1500*****Y~\nEB*C*IND*30***29*600*****Y~\nEB*G*IND*30***23*6000*****Y~\nEB*G*IND*30***29*4100*****Y~\nEB*C*FAM*30***23*3000*****Y~\nEB*C*FAM*30***29*1800*****Y~\nEB*A*IND*30*****.2****Y~\nEB*C*IND*30***23*3000*****N~\nEB*C*IND*30***29*3000*****N~\nEB*A*IND*30*****.4****N~\nEB*B*IND*98****25*****Y~\nEB*A*IND*98*****.4****N~\nEB*1*IND*62********Y~\nSE*27*0001~\nGE*1*91~\nIEA*1*000000091~", "network_level": "in_network", "date_of_service": "2026-10-20", "lines": [ { "reference": "L1", "allowed_amount": 180 }, { "reference": "L2", "allowed_amount": 900 } ] } ``` The answer (200) ```json { "kind": "estimate", "source": "x12", "member_index": 0, "payer": { "name": "SAMPLE HEALTH PLAN", "id": "99999" }, "plan": { "name": "SAMPLE PPO", "begins": "2026-01-01", "ends": "" }, "date_of_service": "2026-10-20", "network_level": "in_network", "coverage_level": "individual", "insurance_type": null, "coverage": { "state": "active", "active_on_date": "yes" }, "lines": [ { "index": 0, "reference": "L1", "service_type": "30", "procedure_code": null, "allowed_amount": 180, "inputs": { "allowed_amount": { "value": 180, "source": "caller", "words": "You sent it (lines[0].allowed_amount)." }, "copay": { "value": null, "source": "not_stated", "words": "The 271 did not state a copay for this line, and you sent none." }, "deductible_remaining_before": { "value": 600, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "individual", "insurance_type": null, "words": "The 271, plan level (30), in network, individual, remaining amount stated." }, "deductible_family_remaining_before": { "value": 1800, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "family", "insurance_type": null, "words": "The 271, plan level (30), in network, family, remaining amount stated." }, "deductible_family_design": { "value": "not_stated", "source": "not_stated", "words": "The 271 has no field saying whether the family deductible is embedded or aggregate; it is read as embedded, so the member owes toward it no more than the lesser of the individual and family amounts left." }, "out_of_pocket_remaining_before": { "value": 4100, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "individual", "insurance_type": null, "words": "The 271, plan level (30), in network, individual, remaining amount stated." } }, "deductible_applied": 180, "copay_applied": 0, "coinsurance_amount": 0, "patient_share": 180, "plan_share": 0, "out_of_pocket_reached": false, "not_stated": [], "why": [], "prior_auth_required": null }, { "index": 1, "reference": "L2", "service_type": "30", "procedure_code": null, "allowed_amount": 900, "inputs": { "allowed_amount": { "value": 900, "source": "caller", "words": "You sent it (lines[1].allowed_amount)." }, "copay": { "value": null, "source": "not_stated", "words": "The 271 did not state a copay for this line, and you sent none." }, "deductible_remaining_before": { "value": 420, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "individual", "insurance_type": null, "words": "The 271, plan level (30), in network, individual, remaining amount stated." }, "deductible_family_remaining_before": { "value": 1620, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "family", "insurance_type": null, "words": "The 271, plan level (30), in network, family, remaining amount stated." }, "deductible_family_design": { "value": "not_stated", "source": "not_stated", "words": "The 271 has no field saying whether the family deductible is embedded or aggregate; it is read as embedded, so the member owes toward it no more than the lesser of the individual and family amounts left." }, "coinsurance_percent": { "value": 20, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": null, "insurance_type": null, "words": "The 271, plan level (30), in network." }, "out_of_pocket_remaining_before": { "value": 3920, "source": "271", "scope": "plan", "service_type": "30", "network_tier": "in_network", "coverage_level": "individual", "insurance_type": null, "words": "The 271, plan level (30), in network, individual, remaining amount stated." } }, "deductible_applied": 420, "copay_applied": 0, "coinsurance_amount": 96, "patient_share": 516, "plan_share": 384, "out_of_pocket_reached": false, "not_stated": [], "why": [], "prior_auth_required": null } ], "totals": { "allowed_amount": 1080, "patient_share": 696, "plan_share": 384, "deductible_applied": 600, "copay_applied": 0, "coinsurance_amount": 96 }, "figured": true, "not_stated": [], "assumptions": [ "The 271 states both an individual ($600.00) and a family ($1800.00) deductible left and does not say whether the family deductible is embedded or aggregate. This estimate reads it as embedded (the member’s own amount caps what the member ow…" ], "notes": [ "An estimate from the benefits the payer stated and the allowed amounts you sent, before the payer adjudicates the claim. The payer’s own processing decides what the patient owes. Payerworks holds no prices and keeps nothing it was sent.", "The network level is yours: a 271 says what the plan pays at each benefit tier, never whether this provider participates with the plan." ], "eligibility_notes": [ "A remaining amount is what was left on the day the payer answered.", "In network and out of network are the plan’s benefit tiers as the payer stated them (EB12): what the plan pays at each tier. A 271 does not say whether this provider participates with the plan." ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "estimate" | | | `source` | string | | | `member_index` | integer | | | `payer` | object | | | `plan` | object | | | `date_of_service` | string or null | | | `network_level` | string | | | `coverage_level` | string | | | `insurance_type` | string or null | | | `coverage` | object | | | `lines` | list of object | | | `totals` | object or null | Null when any line could not be figured. | | `figured` | boolean | | | `not_stated` | list of object | | | `assumptions` | list of string | | | `notes` | list of string | | | `eligibility_notes` | list of string | | | `refusal` | object | When the 271 itself is a refusal: the explainer’s reasons. | | `transmitted` | the value false | | ### POST /v1/estimates/good-faith **Lay out a Good Faith Estimate.** A Good Faith Estimate for an uninsured or self-pay patient, with every element 45 CFR 149.610 lists, in order and each named by its paragraph: the patient, the primary service, the items grouped by provider with their codes and expected charges, the amount at which the patient may dispute the bill, the disclaimers, and when it is due by the business days you state. It is your document to issue, give and keep; Payerworks issues, delivers and keeps nothing. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/estimates/good-faith" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEstimatesGoodFaith(body); ``` With the Python client ```python answer = pw.post_estimates_good_faith(body) ``` request.json ```json { "issued_on": "2026-10-19", "uninsured_reason": "not_filing_claim", "patient": { "first_name": "SAMPLE", "last_name": "PATIENT", "date_of_birth": "1980-02-14" }, "primary_service": { "description": "A new patient visit and a sleep study", "scheduled_date": "2026-11-09" }, "providers": [ { "id": "p1", "role": "convening", "name": "Sample Family Medicine", "npi": "1234567893", "tin": "12-3456789", "locations": [ { "state": "IL" } ] }, { "id": "p2", "role": "co_provider", "name": "Sample Sleep Lab", "npi": "1999999992", "tin": "987654321", "locations": [ { "state": "IL" } ] } ], "items": [ { "provider_id": "p1", "description": "New patient visit", "service_code": "99203", "service_code_type": "CPT", "diagnosis_codes": [ "G47.33" ], "expected_charge": 180 }, { "provider_id": "p2", "description": "Home sleep apnea test", "service_code": "G0399", "service_code_type": "HCPCS", "diagnosis_codes": [ "G47.33" ], "expected_charge": 350 } ], "separately_scheduled": [ { "description": "A follow-up visit to read the results" } ], "timing": { "business_days_until_service": 15 } } ``` The answer (200) ```json { "document": "good_faith_estimate", "provider_document": true, "statement": "This is the provider’s Good Faith Estimate: the convening provider or facility issues it in writing, gives it to the patient and keeps it with the medical record (45 CFR 149.610(e)(1), (f)(1)). Payerworks laid it out from what you sent; it …", "rule": { "cite": "45 CFR 149.610", "url": "https://www.law.cornell.edu/cfr/text/45/149.610", "read_on": "10/19/2026" }, "issued_on": "2026-10-19", "issued_by": { "provider_id": "p1", "name": "Sample Family Medicine", "role": "convening" }, "uninsured_reason": { "code": "not_filing_claim", "words": "The patient has coverage but does not want a claim sent to it (45 CFR 149.610(a)(2)(xiii)(B))." }, "elements": [ { "cfr": "(c)(1)(i)", "element": "patient", "value": { "first_name": "SAMPLE", "last_name": "PATIENT", "date_of_birth": "1980-02-14" } }, { "cfr": "(c)(1)(ii)", "element": "primary_service", "value": { "description": "A new patient visit and a sleep study", "scheduled_date": "2026-11-09" } } ], "expected_total": 530, "expected_total_words": "Expected charges in all: $530.00.", "timing": [ { "rule": "45 CFR 149.610(b)(1)(vi)(B)", "words": "Scheduled at least 10 business days ahead: issue it within 3 business days of scheduling." } ], "delivery": "In writing, on paper or electronically as the patient asks; an electronic copy must be one the patient can save and print, in plain language (45 CFR 149.610(e)(1)). Information given by phone does not replace the written estimate ((e)(2)).", "retention": "Part of the patient’s medical record; copies of any estimate issued in the last 6 years are given on request (45 CFR 149.610(f)(1)).", "not_laid_out": "A recurring course of care on one estimate (at most 12 months, 45 CFR 149.610(b)(1)(x)) and a co-provider’s own submission to the convening provider ((d)) are not laid out by this route.", "notes": [ "Codes are checked for their shape only (an ICD-10-CM diagnosis, a CPT or HCPCS code, a DRG, an NDC); Payerworks does not look them up and prints no code descriptor." ], "sources": [ { "cite": "45 CFR 149.610", "url": "https://www.law.cornell.edu/cfr/text/45/149.610", "read_on": "10/19/2026" }, { "cite": "45 CFR 149.620", "url": "https://www.law.cornell.edu/cfr/text/45/149.620", "read_on": "10/19/2026" } ], "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `document` | the value "good_faith_estimate" | | | `provider_document` | the value true | | | `statement` | string | | | `rule` | object | | | `issued_on` | string | | | `issued_by` | object | | | `uninsured_reason` | object | | | `elements` | list of object | | | `expected_total` | number | | | `expected_total_words` | string | | | `timing` | list of object | | | `delivery` | string | | | `retention` | string | | | `not_laid_out` | string | | | `notes` | list of string | | | `sources` | list of object | | | `transmitted` | the value false | | ### POST /v1/dme/replacement-window **When equipment may be replaced.** For each item you send (a HCPCS code and the day it was delivered, at most 100): the earliest replacement for wear under Medicare’s reasonable useful lifetime, five years from delivery unless you name a longer lifetime and its source, whether a planned day falls inside it (judged from the latest delivery you sent of the same code), and what to consider by reason (an Advance Beneficiary Notice with GA or GZ, or the RA modifier for a loss, a theft or irreparable damage), each cited. It does not check Medicare’s records: only the DME MAC shows whether a same or similar item was supplied. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/dme/replacement-window" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postDmeReplacementWindow(body); ``` With the Python client ```python answer = pw.post_dme_replacement_window(body) ``` request.json ```json { "items": [ { "reference": "A", "hcpcs": "E0601", "delivered_on": "2021-03-15", "planned_on": "2026-11-02" }, { "reference": "B", "hcpcs": "K0001", "delivered_on": "2023-05-02", "planned_on": "2026-11-02", "reason": "theft" } ] } ``` The answer (200) ```json { "kind": "dme_replacement_window", "checks_medicare_records": false, "statement": "This does not check Medicare’s records. It works from the codes and delivery dates you sent; the patient may have had a same or similar item from another supplier, which only the DME MAC’s same or similar inquiry shows. Payerworks keeps not…", "items": [ { "reference": "A", "hcpcs": "E0601", "delivered_on": "2021-03-15", "useful_lifetime_years": 5, "useful_lifetime": { "source": "cfr_floor", "words": "5 years: the floor in 42 CFR 414.210(f)(1). A program instruction or the DME MAC may set a longer lifetime for this item, which this answer does not know." }, "earliest_replacement_for_wear": "2026-03-15", "earliest_words": "A replacement for wear is outside the lifetime on or after 03/15/2026 (5 years from delivery on 03/15/2021).", "latest_delivery_for_code": true, "planned_on": "2026-11-02", "reason": null, "inside_lifetime": false, "consider": [ { "step": "no_abn_for_lifetime", "words": "The planned day is on or after the end of the lifetime, so the lifetime gives no reason for an ABN (Noridian JD DME, Same or Similar Denials (one DME MAC’s guidance)). A replacement for wear needs a new order, and a new CMN where one is req…", "source": { "cite": "Noridian JD DME, Same or Similar Denials (one DME MAC’s guidance)", "url": "https://med.noridianmedicare.com/web/jddme/topics/same-or-similar/ss-denials", "read_on": "10/19/2026" } } ], "sources": [ { "cite": "42 CFR 414.210(f)(1)", "url": "https://www.law.cornell.edu/cfr/text/42/414.210", "read_on": "10/19/2026" }, { "cite": "Medicare Benefit Policy Manual, Pub. 100-02, ch. 15, §110.2.C", "url": "https://www.cms.gov/Regulations-and-Guidance/Guidance/Manuals/downloads/bp102c15.pdf", "read_on": "10/19/2026" } ] }, { "reference": "B", "hcpcs": "K0001", "delivered_on": "2023-05-02", "useful_lifetime_years": 5, "useful_lifetime": { "source": "cfr_floor", "words": "5 years: the floor in 42 CFR 414.210(f)(1). A program instruction or the DME MAC may set a longer lifetime for this item, which this answer does not know." }, "earliest_replacement_for_wear": "2028-05-02", "earliest_words": "A replacement for wear is outside the lifetime on or after 05/02/2028 (5 years from delivery on 05/02/2023).", "latest_delivery_for_code": true, "planned_on": "2026-11-02", "reason": "theft", "inside_lifetime": true, "consider": [ { "step": "ra_modifier", "modifier": "RA", "words": "A stolen item may be replaced inside the lifetime (42 CFR 414.210(f)(1)). Bill the replacement with RA, replacement of a DME item, and a narrative of why it was replaced (Medicare Claims Processing Manual, Pub. 100-04, ch. 20, §50.4); keep …", "source": { "cite": "Medicare Claims Processing Manual, Pub. 100-04, ch. 20, §50.4", "url": "https://www.cms.gov/Regulations-and-Guidance/Guidance/Manuals/downloads/clm104c20.pdf", "read_on": "10/19/2026" } } ], "sources": [ { "cite": "42 CFR 414.210(f)(1)", "url": "https://www.law.cornell.edu/cfr/text/42/414.210", "read_on": "10/19/2026" }, { "cite": "Medicare Benefit Policy Manual, Pub. 100-02, ch. 15, §110.2.C", "url": "https://www.cms.gov/Regulations-and-Guidance/Guidance/Manuals/downloads/bp102c15.pdf", "read_on": "10/19/2026" } ] } ], "notes": [ "Replacing rented oxygen equipment, items needing frequent and substantial servicing, and rented inexpensive or routinely purchased items is not covered at all (Medicare Benefit Policy Manual, Pub. 100-02, ch. 15, §110.2.C); a HCPCS code alo…", "Same or similar is judged across codes as well as within one; which codes Medicare treats as similar is the DME MAC’s, and this answer compares a code only with itself." ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "dme_replacement_window" | | | `checks_medicare_records` | the value false | | | `statement` | string | | | `items` | list of object | | | `notes` | list of string | | ### POST /v1/financial-assistance/fpl **Income against the poverty guideline.** Income as a percent of the 2026 HHS poverty guideline for the household’s size (Alaska and Hawaii on their own tables; a territory only when you say your program uses the contiguous table), and which of your policy’s tiers it falls in, compared in whole cents. Arithmetic only: no screening and no decision. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/financial-assistance/fpl" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postFinancialAssistanceFpl(body); ``` With the Python client ```python answer = pw.post_financial_assistance_fpl(body) ``` request.json ```json { "household_size": 3, "annual_income": 54640, "state": "IL", "tiers": [ { "name": "Full discount", "max_percent": 200 }, { "name": "Half discount", "max_percent": 300 } ] } ``` The answer (200) ```json { "kind": "fpl", "household_size": 3, "annual_income": 54640, "income_input": { "annual_income": 54640 }, "state": "IL", "guideline": { "year": 2026, "table": "48 contiguous states and the District of Columbia", "amount": 27320, "source": { "name": "2026 HHS Poverty Guidelines", "publisher": "U.S. Department of Health and Human Services, Office of the Assistant Secretary for Planning and Evaluation (ASPE)", "citation": "91 Fed. Reg. 1797 (01/15/2026), document 2026-00755", "url": "https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines", "federal_register_url": "https://www.govinfo.gov/content/pkg/FR-2026-01-15/html/2026-00755.htm", "effective": "01/13/2026, unless a program sets another date", "read_on": "10/19/2026" } }, "percent_of_guideline": 200, "tier": { "name": "Full discount", "max_percent": 200 }, "tier_words": "At 200% of the guideline, inside your tier Full discount (up to 200%).", "statement": "Arithmetic on the figures you sent against the published HHS poverty guideline. It is not a screening and not a decision: what counts as the household and its income, and what each tier grants, are the provider’s financial assistance policy…", "screening": false, "decision": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `kind` | the value "fpl" | | | `household_size` | integer | | | `annual_income` | number | | | `income_input` | object | | | `state` | string | | | `guideline` | object | | | `percent_of_guideline` | number | | | `tier` | object or null | | | `tier_words` | string | | | `statement` | string | | | `screening` | the value false | | | `decision` | the value false | | ### POST /v1/price-transparency/validate **Check a hospital standard charges file.** A hospital’s standard charges file, as the tall or wide CSV or as JSON, checked against CMS’s version 3.0.0 template and schema: every required header and value, the valid values, and each conditional requirement, each problem with its row or path, the column and the rule it breaks. A file over 2 MB is checked in pieces, the first three rows sent again with each and row_offset set, so every row number is the file’s own. Nothing is hosted, published or kept. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/price-transparency/validate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postPriceTransparencyValidate(body); ``` With the Python client ```python answer = pw.post_price_transparency_validate(body) ``` request.json ```json { "csv": "hospital_name,last_updated_on,version,location_name,hospital_address,license_number|IL,type_2_npi,\"To the best of its knowledge and belief, this hospital has included all applicable standard charge information in accordance with the requirements of 45 CFR 180.50, and the information encoded is true, accurate, and complete as of the date in the file. This hospital has included all payer-specific negotiated charges in dollars that can be expressed as a dollar amount. For payer-specific negotiated charges that cannot be expressed as a dollar amount in the machine-readable file or not knowable in advance, the hospital attests that the payer-specific negotiated charge is based on a contractual algorithm, percentage or formula that precludes the provision of a dollar amount and has provided all necessary information available to the hospital for the public to be able to derive the dollar amount, including, but not limited to, the specific fee schedule or components referenced in such percentage, algorithm or formula.\",attester_name\nSample Community Hospital,2026-10-01,3.0.0,Sample Community Hospital,\"100 Main Street, Springfield, IL 62701\",SAMPLE-1,1234567893,true,Sample Attester\ndescription,code|1,code|1|type,modifiers,setting,drug_unit_of_measurement,drug_type_of_measurement,standard_charge|gross,standard_charge|discounted_cash,payer_name,plan_name,standard_charge|negotiated_dollar,standard_charge|negotiated_percentage,standard_charge|negotiated_algorithm,median_amount,10th_percentile,90th_percentile,count,standard_charge|methodology,standard_charge|min,standard_charge|max,additional_generic_notes\nOffice visit,510,RC,,outpatient,,,200,160,Sample Health Plan,PPO,120,,,,,,,fee schedule,110,120,\nHip replacement,470,MS-DRG,,inpatient,,,,,Sample Health Plan,PPO,,45,,,,,,percent of total billed charges,,,\n" } ``` The answer (200) ```json { "valid": false, "format": "csv_tall", "file_version": "3.0.0", "errors": [ { "row": 5, "column": "count", "code": "count_missing", "rule": "csv_conditional_7", "message": "Row 5 has a negotiated percentage or algorithm but no count of allowed amounts." }, { "row": 5, "column": "median_amount", "code": "allowed_amount_missing", "rule": "csv_conditional_8", "message": "Row 5 has a negotiated percentage or algorithm but no median allowed amount." } ], "warnings": [], "error_count": 4, "warning_count": 0, "truncated": false, "rows_checked": 2, "template": { "version": "3.0.0", "repository": "https://github.com/CMSgov/hospital-price-transparency", "commit": "5333564a710f80d7740180b9ffab8dbdcba9b502", "committed_on": "06/09/2026", "read_on": "10/19/2026", "files": [ "documentation/CSV/README.md", "documentation/CSV/templates/V3.0.0_Tall_CSV_Format_Template.csv" ], "regulation": "45 CFR 180.50", "terms": "A work of the United States federal government (CMS); the repository carries no license file." }, "rules": { "csv_conditional_1": "A negotiated charge (dollar, percentage or algorithm) needs its payer name, plan name and methodology; a payer or plan named in the tall layout needs a negotiated charge.", "csv_conditional_2": "A row with a standard charge needs at least one code with its code type.", "csv_conditional_3": "A code needs its code type in the matching column, and a code type its code.", "csv_conditional_4": "A methodology of \"other\" needs an explanation in the notes for that charge.", "csv_conditional_5": "An item or service needs at least one of: gross charge, discounted cash price, or a negotiated dollar, percentage or algorithm.", "csv_conditional_6": "A negotiated dollar amount needs the de-identified minimum and maximum negotiated charges.", "csv_conditional_7": "A negotiated percentage or algorithm needs the count of allowed amounts.", "csv_conditional_8": "A negotiated percentage or algorithm needs the median, 10th and 90th percentile allowed amounts, unless the count is 0.", "csv_conditional_9": "A count of 0 needs an explanation in the notes for that charge.", "csv_conditional_10": "A code of type NDC needs the drug unit and drug type of measurement.", "csv_conditional_11": "A modifier row with no code needs a description and one of: a negotiated dollar, percentage or algorithm, or notes.", "csv_conditional_12": "A drug unit of measurement needs its drug type, and a drug type its unit.", "json_conditional_1": "A methodology of \"other\" needs additional_payer_notes.", "json_conditional_2": "A standard charge needs at least one of: gross_charge, discounted_cash, or a payer's negotiated dollar, percentage or algorithm.", "json_conditional_3": "Each payers_information object needs a standard_charge_dollar, standard_charge_percentage or standard_charge_algorithm.", "json_conditional_4": "A negotiated dollar amount needs minimum and maximum on its standard charge.", "json_conditional_5": "A negotiated percentage or algorithm needs count.", "json_conditional_6": "A negotiated percentage or algorithm needs median_amount, 10th_percentile and 90th_percentile, unless count is \"0\".", "json_conditional_7": "A count of \"0\" needs additional_payer_notes.", "json_conditional_8": "A code of type NDC needs drug_information.", "schema": "The v3.0.0 JSON schema's own requirement.", "template": "The v3.0.0 CSV template's own requirement." }, "hosted": false, "published": false, "stored": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `valid` | boolean | | | `format` | one of "csv_tall", "csv_wide", "json" | | | `file_version` | string or null | | | `errors` | list of object | The first 1000. | | `warnings` | list of object | The first 200: our advice, never a fault. | | `error_count` | integer | | | `warning_count` | integer | | | `truncated` | boolean | | | `rows_checked` | integer | | | `items_checked` | integer | | | `payer_plans` | integer | | | `template` | object | CMS’s template, by its repository, commit and the day read. | | `rules` | object | Each rule named by an error, in Payerworks’ words. | | `hosted` | the value false | | | `published` | the value false | | | `stored` | the value false | | ## Reading the answer - An estimate shows each line’s `patient_share` and `plan_share` with the deductible, copay and coinsurance applied, and the `totals`, from the 271 you sent and the allowed amount you gave, with its `assumptions` said. It is an estimate, not a promise of what the payer will pay. ## Troubleshooting - `member_required` (422): The 271 answers more than one patient: send member_index to say which one. - `invalid_good_faith_estimate` (422): An element the rule requires is missing; each is listed. - `guideline_year_not_held` (422): Only the guideline year Payerworks holds is answered. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `no_answer` | 400 | Send the 271 as `x12` (the file's text) or as `json` (the JSON answer). | | `no_dme_items` | 400 | Send items: each { hcpcs, delivered_on }, at most 100 a call. | | `no_estimate_lines` | 400 | Send lines: the planned services, each with the allowed amount you expect (allowed_amount), at most 50 a call. | | `no_mrf` | 400 | Send the file's text as `csv` (the tall or wide CSV template) or the file as `json` (an object, or its text). | | `too_many_dme_items` | 400 | At most 100 items a call. | | `too_many_estimate_lines` | 400 | At most 50 estimate lines a call. | | `answer_too_large` | 413 | This answer is larger than one response can carry: send less in each call (split the file, or send fewer claims or requests). | | `file_too_large` | 413 | This file holds more than one call can read: the error names what was counted (`unit`) and the most one call reads (`limit`). Split it and send each part in its own call. | | `mrf_too_large` | 413 | The file is longer than 1990000 characters, or holds more than 50000 rows or 50000 items: check it in pieces, the first three rows sent again with each piece and row_offset set. | | `guideline_not_defined` | 422 | The HHS poverty guidelines are not defined for Puerto Rico or the other territories; send use_contiguous_table: true if your program uses the contiguous table there. | | `guideline_year_not_held` | 422 | Payerworks holds the 2026 HHS poverty guidelines only; send guideline_year 2026 or leave it out. | | `invalid_dme_items` | 422 | One or more items cannot be read; nothing was answered. Each problem is listed. | | `invalid_estimate` | 422 | The estimate request has fields that cannot be read; nothing was figured. Each problem is listed. | | `invalid_fpl_request` | 422 | The request has fields that cannot be read; nothing was figured. Each problem is listed. | | `invalid_good_faith_estimate` | 422 | The Good Faith Estimate is missing elements 45 CFR 149.610 requires, or has fields that cannot be read; nothing was laid out. Each problem is listed. | | `member_required` | 422 | This 271 answers more than one patient: send member_index to say which one to estimate. | | `mrf_not_json` | 422 | `json` is not a JSON document that can be read. | | `no_member` | 422 | This 271 answers no patient to estimate. | | `not_a_271` | 422 | This is not an X12 271 eligibility answer: send the 271 with its ISA envelope. | | `not_a_271_json` | 422 | This JSON is not an eligibility answer: it holds no plans, benefitsInformation, errors or subscriber. | ## Example [patient-money.mjs](https://payerworks.com/docs/examples/patient-money.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. patient-money.mjs ```js // Payerworks example: a patient's share estimated from a 271. Docs: https://payerworks.com/docs/patient-money // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node patient-money.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { ELIGIBILITY_QUESTION, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. A 271 to estimate from: the eligibility sandbox's answer. const sandbox = await pw.postSandboxEligibility({ request: copy(ELIGIBILITY_QUESTION) }); // 2. The patient's share of two planned services, from that 271 and the allowed amounts you expect. const estimate = await pw.postEstimates({ x12: sandbox.files.eligibility_271, network_level: 'in_network', date_of_service: '2026-10-20', lines: [{ reference: 'visit', allowed_amount: 180 }, { reference: 'test', allowed_amount: 40 }], }); // 3. The failure path: a line with no allowed amount is refused. Payerworks never supplies one. const refused = await refusalOf(() => pw.postEstimates({ x12: sandbox.files.eligibility_271, network_level: 'in_network', date_of_service: '2026-10-20', lines: [{ reference: 'visit' }] }), { PayerworksError }); return { figured: estimate.figured, totals: estimate.totals, transmitted: estimate.transmitted, refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "figured": true, "totals": { "allowed_amount": 220, "patient_share": 220, "plan_share": 0, "deductible_applied": 220, "copay_applied": 0, "coinsurance_amount": 0 }, "transmitted": false, "refused": { "status": 422, "code": "invalid_estimate", "request_id": "req_3" } } ``` # The payer directory > Find the payer and its published payer ID before anything else, from public federal files, each ID with its source. Page: https://payerworks.com/docs/payers · Markdown: https://payerworks.com/docs/payers.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. A search of payers and plan programs from public federal files, with a payer ID only where the payer publishes it, each with the page it came from and the day it was read. It is a directory, not a list of payers Payerworks can send to: Payerworks is connected to no payer yet. ## Routes - [GET /v1/payers](#get-v1-payers): Search the payer directory - [GET /v1/payers/{id}](#get-v1-payers-id): One directory row Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### GET /v1/payers **Search the payer directory.** Payers and plan programs from public federal files (Medicare Advantage and Part D contracts, Medicaid managed care plans, insurers’ loss ratio filings, employer plans’ Form 5500 carriers) and the Medicare contractors; not a list Payerworks can send to; a payer ID only where the payer publishes it, with the page and the day it was read. Search with q by name, a misspelling, a payer ID, a NAIC code or a CMS contract ID (a state named in q is read as state=); narrow with state and type; page with limit (up to 100) and starting_after. Every row says connection none, and an all-digit payer ID under five digits is warned about: leading zeros are part of it. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/payers?q=18003" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getPayers({ query: {"q":"18003"} }); ``` With the Python client ```python answer = pw.get_payers(query={"q": "18003"}) ``` The answer (200) ```json { "object": "list", "scope": "Payers and plan programs from public federal files; not a list of payers Payerworks can send to; payer IDs only where the payer publishes one.", "payer_ids_are": "A payer ID is shown only where the payer or its Medicare contractor publishes it, with the page and the day it was read. Clearinghouses can route the same payer under other IDs, and Payerworks is connected to no payer yet.", "built_on": "2026-10-19", "query": { "q": "18003", "state": null, "type": null }, "total": 1, "data": [ { "id": "pwd_fe5efb2967c8dc5d", "object": "payer_directory_row", "kind": "plan_program", "name": "Medicare DME MAC Jurisdiction C (CGS Administrators)", "aliases": [ "DME MAC C", "DME MAC Jurisdiction C" ], "program": "medicare_dme", "organization_id": "pwd_deeb1d92abd324d6", "organization_name": "CGS Administrators", "parent": "Medicare fee-for-service (Original Medicare)", "states": [ "AL", "AR" ], "nationwide": false, "area_stated": true, "payer_id": "18003", "payer_id_words": null, "published_payer_ids": [ { "payer_id": "18003", "for": "Medicare DME claims, Jurisdiction C", "transactions": [ "claims" ], "claim_type": "professional", "source": { "url": "https://www.cgsmedicare.com/jc/claims/edi.html", "label": "CGS JC DME: electronic claims", "read_on": "2026-10-19" } } ], "naic_codes": [], "cms_contract_ids": [], "hios_issuer_ids": [], "connection": "none", "transmits": false, "match": { "by": "payer_id", "exact": true, "payer_id": "18003" } } ], "has_more": false, "next_cursor": null, "warnings": [], "connection": "none", "transmits": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "list" | | | `scope` | string | What the directory is, and is not. | | `payer_ids_are` | string | | | `built_on` | string | YYYY-MM-DD. | | `query` | object | | | `total` | integer | | | `data` | list of object | | | `has_more` | boolean | | | `next_cursor` | string or null | | | `warnings` | list of FieldProblem | | | `bluecard` | object | Asked about BlueCard: how it routes, in words. | | `medicare_words` | string | | | `connection` | the value "none" | | | `transmits` | the value false | | ### GET /v1/payers/{id} **One directory row.** One row in full by its directory id (pwd_ and 16 hex): its names, where its sources say it serves (or Not stated), programs, the payer IDs it publishes with their sources, and the files it came from. A payer ID sent here is refused 400 not_a_directory_row_id and pointed to the search, never answered as not found. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/payers/pwd_fe5efb2967c8dc5d" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getPayersById("pwd_fe5efb2967c8dc5d"); ``` With the Python client ```python answer = pw.get_payers_by_id("pwd_fe5efb2967c8dc5d") ``` The answer (200) ```json { "id": "pwd_fe5efb2967c8dc5d", "object": "payer_directory_row", "kind": "plan_program", "name": "Medicare DME MAC Jurisdiction C (CGS Administrators)", "aliases": [ "DME MAC C", "DME MAC Jurisdiction C" ], "program": "medicare_dme", "organization_id": "pwd_deeb1d92abd324d6", "organization_name": "CGS Administrators", "parent": "Medicare fee-for-service (Original Medicare)", "states": [ "AL", "AR" ], "nationwide": false, "area_stated": true, "payer_id": "18003", "payer_id_words": null, "published_payer_ids": [ { "payer_id": "18003", "for": "Medicare DME claims, Jurisdiction C", "transactions": [ "claims" ], "claim_type": "professional", "source": { "url": "https://www.cgsmedicare.com/jc/claims/edi.html", "label": "CGS JC DME: electronic claims", "read_on": "2026-10-19" } } ], "naic_codes": [], "cms_contract_ids": [], "hios_issuer_ids": [], "connection": "none", "transmits": false, "naic_group_codes": [], "dme_jurisdiction": "C", "claims_mailing_address": null, "medicare": "fee_for_service", "medicaid": null, "possibly_same_as": [], "possibly_same_as_words": null, "sources": [ "payer_pages" ], "children": [], "scope": "Payers and plan programs from public federal files; not a list of payers Payerworks can send to; payer IDs only where the payer publishes one.", "payer_ids_are": "A payer ID is shown only where the payer or its Medicare contractor publishes it, with the page and the day it was read. Clearinghouses can route the same payer under other IDs, and Payerworks is connected to no payer yet.", "built_on": "2026-10-19", "source_files": [ { "key": "payer_pages" } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `id` | string | The directory row id: pwd_ and 16 hex, Payerworks’ own, never a payer ID. | | `object` | the value "payer_directory_row" | | | `kind` | one of "organization", "plan_program" | | | `name` | string | | | `aliases` | list of string | | | `types` | list of one of "commercial_insurer", "group_health_carrier", "group_dental_carrier", "medicare_advantage", "medicare_cost_plan", "medicare_part_d", "pace", "medicaid_managed_care", "medicaid_pccm", "medicaid_dental", "medicaid_behavioral_health", "medicaid_long_term_care", "medicaid_transportation", "medicaid_limited_benefit", "medicare_fee_for_service", "medicare_contractor", "medicare_dme", "tricare", "payer" | An organization’s kinds. | | `program` | one of "commercial_insurer", "group_health_carrier", "group_dental_carrier", "medicare_advantage", "medicare_cost_plan", "medicare_part_d", "pace", "medicaid_managed_care", "medicaid_pccm", "medicaid_dental", "medicaid_behavioral_health", "medicaid_long_term_care", "medicaid_transportation", "medicaid_limited_benefit", "medicare_fee_for_service", "medicare_contractor", "medicare_dme", "tricare", "payer" | A plan program’s kind. | | `organization_id` | string or null | | | `organization_name` | string or null | | | `parent` | string or null | | | `states` | list of string | | | `nationwide` | boolean | True only where the source says the whole country. | | `area_stated` | boolean | False when no file or page the row was read from says where it serves: its states are then empty, and Not stated, never guessed. | | `area_words` | string | With area_stated false: Not stated, in words. | | `area_note` | object | What the row’s own page says about where it serves, with the page. | | `employer_group_states` | list of string | States where CMS lists the contract for employer group plans only; never in states. | | `see_payer_ids` | object | A Medicare Advantage contract with no published ID of its own whose parent organization publishes payer IDs for its plans: the row that holds them. The IDs are never copied onto this row. | | `payer_id` | string or null | The one claims payer ID the payer publishes; null when it publishes none Payerworks has read, or several. | | `payer_id_words` | string or null | Why payer_id is null, in words. | | `published_payer_ids` | list of object | Every ID the payer or its Medicare contractor publishes, each with its page and the day it was read. | | `naic_codes` | list of string | | | `cms_contract_ids` | list of string | | | `hios_issuer_ids` | list of string | | | `connection` | the value "none" | Payerworks is connected to no payer yet. | | `transmits` | the value false | | | `naic_group_codes` | list of string | | | `programs` | list of string | | | `plan_type` | string | | | `organization_type` | string | | | `effective_date` | string | | | `medicaid_programs` | list of string | | | `regions` | list of string | | | `dme_jurisdiction` | string | | | `legal_state` | string | | | `domiciliary_states` | list of string | | | `claims_mailing_address` | object or null | The claims mailing address the payer gave CMS, an organization fact. | | `medicare` | the value "fee_for_service" or list of string or null | fee_for_service, or the Medicare programs the row offers, or null. | | `medicaid` | list of string | | | `possibly_same_as` | list of string | | | `possibly_same_as_words` | string or null | | | `sources` | list of string | | | `children` | list of object | | | `scope` | string | | | `payer_ids_are` | string | | | `built_on` | string | | | `source_files` | list of object | | ## Reading the answer - Each row says how it matched (`match.by`: name, alias, legal form or spelling) and whether exactly, and carries `connection: "none"` and `transmits: false`. A row with no published payer ID says so, never a guessed one. ## Troubleshooting - `not_a_directory_row_id` (400): GET /v1/payers/{id} takes a directory row id, which begins pwd_, never a payer ID. Search with ?q= first. - `payer_not_in_directory` (404): The directory holds no published facts about that payer. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_payer_query` | 400 | A search parameter is out of shape; the error names it in field (q, state, type, limit or starting_after). | | `not_a_directory_row_id` | 400 | GET /v1/payers/{id} takes a directory row id (pwd_ and 16 hex), never a payer ID: search GET /v1/payers?q= to find a payer by its payer ID or name. | | `payer_not_in_directory` | 404 | The Payerworks directory holds no published facts about this payer. It lists payers and plan programs from public federal files, not every payer, and not the payers Payerworks can send to. | | `payer_directory_unavailable` | 503 | The payer directory is not loaded where this was asked; the Payerworks API answers it. | ## Example [payers.mjs](https://payerworks.com/docs/examples/payers.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. payers.mjs ```js // Payerworks example: the payer directory. Docs: https://payerworks.com/docs/payers // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node payers.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Search by name or payer ID. A payer ID is shown only where the payer publishes it, with its source. const found = await pw.getPayers({ query: { q: '18003' } }); const row = found.data[0]; // 2. One row by its directory id (pwd_...), never by a payer ID. const one = await pw.getPayersById(row.id); // 3. The failure path: a payer ID in place of a row id is refused, and the error says to search first. const refused = await refusalOf(() => pw.getPayersById('99999'), { PayerworksError }); return { total: found.total, name: one.name, payer_id: one.payer_id, connection: one.connection, transmits: one.transmits, refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "total": 1, "name": "Medicare DME MAC Jurisdiction C (CGS Administrators)", "payer_id": "18003", "connection": "none", "transmits": false, "refused": { "status": 400, "code": "not_a_directory_row_id", "request_id": "req_3" } } ``` # Transaction enrollment > Ask a payer to send a provider’s files through Payerworks, and follow the request to the end. The one part of the API that keeps what you send. Page: https://payerworks.com/docs/enrollment · Markdown: https://payerworks.com/docs/enrollment.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. A payer sends a provider’s eligibility answers, claim status answers, claims or remittances through a clearinghouse only after it is asked to: that is transaction enrollment. Create the provider once (its name, NPI and tax id), then request enrollment per payer and transaction, answer the tasks it raises, and follow it to the end. Payerworks is registered with no payer yet, so a live request waits with Payerworks. Only the sandbox payer `PWSANDBOX` runs the whole path, and every sandbox status word starts with “Sandbox:”. New here? Start with [The enrollment sandbox](https://payerworks.com/docs/quickstart-enrollment). ## Routes - [POST /v1/providers](#post-v1-providers): Create a provider - [GET /v1/providers](#get-v1-providers): List your providers - [GET /v1/providers/{id}](#get-v1-providers-id): One provider - [POST /v1/providers/{id}](#post-v1-providers-id): Change a provider - [DELETE /v1/providers/{id}](#delete-v1-providers-id): Delete a provider - [POST /v1/enrollments](#post-v1-enrollments): Request an enrollment - [GET /v1/enrollments](#get-v1-enrollments): List your enrollments - [GET /v1/enrollments/{id}](#get-v1-enrollments-id): One enrollment - [POST /v1/enrollments/{id}](#post-v1-enrollments-id): Change an enrollment - [DELETE /v1/enrollments/{id}](#delete-v1-enrollments-id): Delete an enrollment - [POST /v1/enrollments/{id}/submit](#post-v1-enrollments-id-submit): Submit a draft - [POST /v1/enrollments/{id}/cancel](#post-v1-enrollments-id-cancel): Cancel an enrollment - [POST /v1/tasks/{id}](#post-v1-tasks-id): Answer a task - [POST /v1/enrollments/{id}/documents](#post-v1-enrollments-id-documents): Upload a PDF for a task - [POST /v1/documents/{id}/confirm](#post-v1-documents-id-confirm): Confirm an upload - [GET /v1/documents/{id}](#get-v1-documents-id): Download a document - [DELETE /v1/documents/{id}](#delete-v1-documents-id): Delete a document - [GET /v1/enrollment-requirements](#get-v1-enrollment-requirements): Payers with published steps - [GET /v1/enrollment-requirements/{payer_id}](#get-v1-enrollment-requirements-payer-id): What a payer asks - [POST /v1/sandbox/enrollments/{id}/advance](#post-v1-sandbox-enrollments-id-advance): Advance a sandbox enrollment - [POST /v1/enrollments/import/check](#post-v1-enrollments-import-check): Check an enrollment import - [POST /v1/enrollments/import](#post-v1-enrollments-import): Import enrollment requests - [GET /v1/enrollments/export](#get-v1-enrollments-export): Export your enrollments - [GET /v1/enrollments/durations](#get-v1-enrollments-durations): How long your requests took Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/providers **Create a provider.** A provider record: its name, billing NPI, tax id (an EIN, never a Social Security number) and contacts. One per NPI and tax id pair: a second is refused, naming the first. With a live key the answer also says what the NPI registry holds for the NPI: whether it is there, Type 1 or 2, and whether the provider’s name matches (its own name only, never a contact’s; a warning, never a refusal; a lookup that fails says so, and is never read as not found). Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/providers" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postProviders(body); ``` With the Python client ```python answer = pw.post_providers(body) ``` request.json ```json { "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id": "12-3456789", "tax_id_type": "ein", "contacts": [ { "first_name": "Jane", "last_name": "Doe", "email": "enrollment@sampleclinic.example", "phone": "8015550100" } ] } ``` The answer (201) ```json { "object": "provider", "id": "prv_01a154ad358043763d71b95c", "mode": "test", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id": "123456789", "tax_id_type": "ein", "contacts": [ { "organization_name": null, "first_name": "Jane", "last_name": "Doe", "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": null } ], "identifiers_locked": false, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "provider" | | | `id` | string | prv_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `name` | string | | | `npi` | string | | | `tax_id` | string | | | `tax_id_type` | one of "ein" | | | `contacts` | list of Contact | | | `identifiers_locked` | boolean | True once a request for this provider has gone to a payer: the NPI and tax id no longer change. | | `created_at` | string | | | `updated_at` | string | | | `npi_registry` | object | A live key’s new provider (wave 4): what the NPI registry holds for the NPI, found, holding no active record, or not asked (with why). Never stored. | | `warnings` | list of FieldProblem | A live key’s new provider: what the NPI registry answer warns of. Never a refusal. | ### GET /v1/providers **List your providers.** Newest first. Page with limit (up to 100) and starting_after, and filter with npi, tax_id or q (part of the name). Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/providers" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getProviders(); ``` With the Python client ```python answer = pw.get_providers() ``` The answer (200) ```json { "data": [ { "object": "provider", "id": "prv_01a154ad358043763d71b95c", "mode": "test", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id": "123456789", "tax_id_type": "ein", "contacts": [ { "organization_name": null, "first_name": "Jane", "last_name": "Doe", "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": null } ], "identifiers_locked": false, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z" } ], "has_more": false, "next_cursor": null } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "list" | | | `data` | list of Provider | Newest first. | | `has_more` | boolean | | | `next_cursor` | string or null | Pass as starting_after. | ### GET /v1/providers/{id} **One provider.** The provider, and whether its NPI and tax id are locked because a request for it has gone to a payer. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/providers/prv_01a154ad358043763d71b95c" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getProvidersById("prv_01a154ad358043763d71b95c"); ``` With the Python client ```python answer = pw.get_providers_by_id("prv_01a154ad358043763d71b95c") ``` The answer (200) ```json { "object": "provider", "id": "prv_01a154ad358043763d71b95c", "mode": "test", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id": "123456789", "tax_id_type": "ein", "contacts": [ { "organization_name": null, "first_name": "Jane", "last_name": "Doe", "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": null } ], "identifiers_locked": false, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "provider" | | | `id` | string | prv_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `name` | string | | | `npi` | string | | | `tax_id` | string | | | `tax_id_type` | one of "ein" | | | `contacts` | list of Contact | | | `identifiers_locked` | boolean | True once a request for this provider has gone to a payer: the NPI and tax id no longer change. | | `created_at` | string | | | `updated_at` | string | | | `npi_registry` | object | A live key’s new provider (wave 4): what the NPI registry holds for the NPI, found, holding no active record, or not asked (with why). Never stored. | | `warnings` | list of FieldProblem | A live key’s new provider: what the NPI registry answer warns of. Never a refusal. | ### POST /v1/providers/{id} **Change a provider.** Only the fields you send change. The NPI and tax id can be corrected until a request for the provider has gone to a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/providers/prv_01a154ad358043763d71b95c" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postProvidersById("prv_01a154ad358043763d71b95c", body); ``` With the Python client ```python answer = pw.post_providers_by_id("prv_01a154ad358043763d71b95c", body) ``` request.json ```json { "name": "Sample Family Clinic TASKS" } ``` The answer (200) ```json { "object": "provider", "id": "prv_01a154ad358043763d71b95c", "mode": "test", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id": "123456789", "tax_id_type": "ein", "contacts": [ { "organization_name": null, "first_name": "Jane", "last_name": "Doe", "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": null } ], "identifiers_locked": false, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "provider" | | | `id` | string | prv_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `name` | string | | | `npi` | string | | | `tax_id` | string | | | `tax_id_type` | one of "ein" | | | `contacts` | list of Contact | | | `identifiers_locked` | boolean | True once a request for this provider has gone to a payer: the NPI and tax id no longer change. | | `created_at` | string | | | `updated_at` | string | | | `npi_registry` | object | A live key’s new provider (wave 4): what the NPI registry holds for the NPI, found, holding no active record, or not asked (with why). Never stored. | | `warnings` | list of FieldProblem | A live key’s new provider: what the NPI registry answer warns of. Never a refusal. | ### DELETE /v1/providers/{id} **Delete a provider.** Its drafts go with it. A provider with a request that is open or live is kept, and the refusal names those requests. Deleting again answers the same. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/providers/prv_01a154ad358043763d71b95c" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteProvidersById("prv_01a154ad358043763d71b95c"); ``` With the Python client ```python answer = pw.delete_providers_by_id("prv_01a154ad358043763d71b95c") ``` The answer (200) ```json { "deleted": true, "id": "prv_01a154ad358043763d71b95c" } ``` | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | | `id` | string | | ### POST /v1/enrollments **Request an enrollment.** One request per provider, payer and transaction: eligibility, claim_status, professional_claims or remittances. Kept as a draft, or handed to Payerworks with submit: true. Payerworks is not yet registered with any payer, so a live request waits with Payerworks, the answer carries a route_not_open warning, and nothing is sent. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollments(body); ``` With the Python client ```python answer = pw.post_enrollments(body) ``` request.json ```json { "provider_id": "prv_01a154ad358043763d71b95c", "payer": { "id": "PWSANDBOX" }, "transaction": "remittances", "primary_contact": { "organization_name": "Sample Family Clinic LLC", "email": "enrollment@sampleclinic.example", "phone": "8015550100", "address": { "line1": "100 Main St", "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "submit": true } ``` The answer (201) ```json { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "provider_action_required", "status_words": "Waiting on the provider: 2 steps below need the provider. Finish them in order.", "waiting_on": "provider", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 3, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "provider_action_required", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances", "warnings": [] } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment" | | | `id` | string | enr_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `provider` | object | | | `payer` | object | | | `transaction` | one of "eligibility", "claim_status", "professional_claims", "remittances" | | | `x12` | string | | | `status` | one of "draft", "payerworks_action_required", "provider_action_required", "with_payer", "live", "rejected", "canceled", "ended" | | | `status_words` | string | | | `waiting_on` | string or null | Whose turn it is; null when nobody’s. | | `reason_code` | string or null | Why it is Payerworks’ turn, at payerworks_action_required. | | `route` | object | | | `notify_email` | string or null | | | `requested_effective_date` | string or null | | | `era_aggregation` | string or null | | | `ptan` | string or null | | | `note` | string | | | `tasks` | list of Task | In rank order. | | `documents` | list of Document | | | `rejection` | Rejection or null | | | `payer_reference` | string or null | What the payer’s confirmation said. | | `created_at` | string | | | `updated_at` | string | | | `submitted_at` | string or null | | | `status_changed_at` | string or null | | | `sent_to_payer_at` | string or null | | | `live_at` | string or null | | | `ended_at` | string or null | | | `cancel_requested_at` | string or null | | | `requirements_url` | string | The payer’s published steps for this transaction. | | `open_tasks` | integer | List items only: tasks not yet finished. | | `primary_contact` | Contact or null | | | `notes` | list of object | From Payerworks. | | `history` | list of object | Every status change, oldest first. | | `history_truncated` | boolean | | | `warnings` | list of object | On a create or submit: route_not_open while Payerworks’ route is closed. | ### GET /v1/enrollments **List your enrollments.** Newest first, each with its status, whose turn it is and its open tasks. Filter with status, transaction, payer_id, provider_id, npi, waiting_on and the created and updated dates. A page may hold fewer than limit, so keep paging while has_more is true. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollments" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollments(); ``` With the Python client ```python answer = pw.get_enrollments() ``` The answer (200) ```json { "data": [ { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "provider_action_required", "status_words": "Waiting on the provider: 2 steps below need the provider. Finish them in order.", "waiting_on": "provider", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "rejection": null, "payer_reference": null, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances", "open_tasks": 2 }, { "object": "enrollment", "id": "enr_01a154ad35802c85a436872c", "mode": "test", "provider": { "id": "prv_01a154ad35807d5d51823b6e", "name": "Sample Family Clinic SLOW", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "with_payer", "status_words": "Sandbox: waiting on the sandbox payer. Nothing was sent anywhere; advance moves it on.", "waiting_on": "payer", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [], "documents": [], "rejection": null, "payer_reference": null, "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": "2026-10-19T15:00:00.000Z", "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances", "open_tasks": 0 } ], "has_more": false, "next_cursor": null } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "list" | | | `data` | list of EnrollmentSummary | Newest first. | | `has_more` | boolean | A page may hold fewer than limit: keep paging while has_more is true. | | `next_cursor` | string or null | | ### GET /v1/enrollments/{id} **One enrollment.** Its status and whose turn it is, in words; its tasks in rank order, its documents, notes from Payerworks, a rejection with its remedy, and the history of every status change. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad35808fafb0295307" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollmentsById("enr_01a154ad35808fafb0295307"); ``` With the Python client ```python answer = pw.get_enrollments_by_id("enr_01a154ad35808fafb0295307") ``` The answer (200) ```json { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "provider_action_required", "status_words": "Waiting on the provider: 1 step below needs the provider. Finish it, in rank order after any step before it.", "waiting_on": "provider", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [ { "key": "packet_id", "text": "PKT-0001" } ], "complete": true, "completed_at": "2026-10-19T15:00:00.000Z", "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 3, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "provider_action_required", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances" } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment" | | | `id` | string | enr_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `provider` | object | | | `payer` | object | | | `transaction` | one of "eligibility", "claim_status", "professional_claims", "remittances" | | | `x12` | string | | | `status` | one of "draft", "payerworks_action_required", "provider_action_required", "with_payer", "live", "rejected", "canceled", "ended" | | | `status_words` | string | | | `waiting_on` | string or null | Whose turn it is; null when nobody’s. | | `reason_code` | string or null | Why it is Payerworks’ turn, at payerworks_action_required. | | `route` | object | | | `notify_email` | string or null | | | `requested_effective_date` | string or null | | | `era_aggregation` | string or null | | | `ptan` | string or null | | | `note` | string | | | `tasks` | list of Task | In rank order. | | `documents` | list of Document | | | `rejection` | Rejection or null | | | `payer_reference` | string or null | What the payer’s confirmation said. | | `created_at` | string | | | `updated_at` | string | | | `submitted_at` | string or null | | | `status_changed_at` | string or null | | | `sent_to_payer_at` | string or null | | | `live_at` | string or null | | | `ended_at` | string or null | | | `cancel_requested_at` | string or null | | | `requirements_url` | string | The payer’s published steps for this transaction. | | `open_tasks` | integer | List items only: tasks not yet finished. | | `primary_contact` | Contact or null | | | `notes` | list of object | From Payerworks. | | `history` | list of object | Every status change, oldest first. | | `history_truncated` | boolean | | | `warnings` | list of object | On a create or submit: route_not_open while Payerworks’ route is closed. | ### POST /v1/enrollments/{id} **Change an enrollment.** The contact, the notice email, the effective date, the remittance grouping, the PTAN or your note, until the request is sent to a payer; while it is a draft, the payer and the transaction too. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad35808fafb0295307" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsById("enr_01a154ad35808fafb0295307", body); ``` With the Python client ```python answer = pw.post_enrollments_by_id("enr_01a154ad35808fafb0295307", body) ``` request.json ```json { "note": "Our remittances go to the billing office." } ``` The answer (200) ```json { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "provider_action_required", "status_words": "Waiting on the provider: 1 step below needs the provider. Finish it, in rank order after any step before it.", "waiting_on": "provider", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [ { "key": "packet_id", "text": "PKT-0001" } ], "complete": true, "completed_at": "2026-10-19T15:00:00.000Z", "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 3, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "provider_action_required", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances" } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment" | | | `id` | string | enr_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `provider` | object | | | `payer` | object | | | `transaction` | one of "eligibility", "claim_status", "professional_claims", "remittances" | | | `x12` | string | | | `status` | one of "draft", "payerworks_action_required", "provider_action_required", "with_payer", "live", "rejected", "canceled", "ended" | | | `status_words` | string | | | `waiting_on` | string or null | Whose turn it is; null when nobody’s. | | `reason_code` | string or null | Why it is Payerworks’ turn, at payerworks_action_required. | | `route` | object | | | `notify_email` | string or null | | | `requested_effective_date` | string or null | | | `era_aggregation` | string or null | | | `ptan` | string or null | | | `note` | string | | | `tasks` | list of Task | In rank order. | | `documents` | list of Document | | | `rejection` | Rejection or null | | | `payer_reference` | string or null | What the payer’s confirmation said. | | `created_at` | string | | | `updated_at` | string | | | `submitted_at` | string or null | | | `status_changed_at` | string or null | | | `sent_to_payer_at` | string or null | | | `live_at` | string or null | | | `ended_at` | string or null | | | `cancel_requested_at` | string or null | | | `requirements_url` | string | The payer’s published steps for this transaction. | | `open_tasks` | integer | List items only: tasks not yet finished. | | `primary_contact` | Contact or null | | | `notes` | list of object | From Payerworks. | | `history` | list of object | Every status change, oldest first. | | `history_truncated` | boolean | | | `warnings` | list of object | On a create or submit: route_not_open while Payerworks’ route is closed. | ### DELETE /v1/enrollments/{id} **Delete an enrollment.** A draft, a canceled or a rejected request. Deleting again answers the same. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad35808fafb0295307" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteEnrollmentsById("enr_01a154ad35808fafb0295307"); ``` With the Python client ```python answer = pw.delete_enrollments_by_id("enr_01a154ad35808fafb0295307") ``` The answer (200) ```json { "deleted": true, "id": "enr_01a154ad35808fafb0295307" } ``` | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | | `id` | string | | ### POST /v1/enrollments/{id}/submit **Submit a draft.** The draft is checked again and handed to Payerworks. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad3580afd6446277eb/submit" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsByIdSubmit("enr_01a154ad3580afd6446277eb"); ``` With the Python client ```python answer = pw.post_enrollments_by_id_submit("enr_01a154ad3580afd6446277eb") ``` The answer (200) ```json { "object": "enrollment", "id": "enr_01a154ad3580afd6446277eb", "mode": "test", "provider": { "id": "prv_01a154ad35802482aa01ccf0", "name": "Sample Family Clinic WAITS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "payerworks_action_required", "status_words": "Waiting on Payerworks. Payerworks is not yet registered to exchange 835 files with PAYERWORKS SANDBOX PAYER, so nothing has been sent to the payer. Your request waits here, and you can change or cancel it until it is sent.", "waiting_on": "payerworks", "reason_code": "route_not_open", "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "draft", "by": "you", "reason_code": null }, { "sequence": 2, "at": "2026-10-19T15:00:00.000Z", "from": "draft", "to": "payerworks_action_required", "by": "you", "reason_code": "route_not_open" } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances", "warnings": [] } ``` | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment" | | | `id` | string | enr_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `provider` | object | | | `payer` | object | | | `transaction` | one of "eligibility", "claim_status", "professional_claims", "remittances" | | | `x12` | string | | | `status` | one of "draft", "payerworks_action_required", "provider_action_required", "with_payer", "live", "rejected", "canceled", "ended" | | | `status_words` | string | | | `waiting_on` | string or null | Whose turn it is; null when nobody’s. | | `reason_code` | string or null | Why it is Payerworks’ turn, at payerworks_action_required. | | `route` | object | | | `notify_email` | string or null | | | `requested_effective_date` | string or null | | | `era_aggregation` | string or null | | | `ptan` | string or null | | | `note` | string | | | `tasks` | list of Task | In rank order. | | `documents` | list of Document | | | `rejection` | Rejection or null | | | `payer_reference` | string or null | What the payer’s confirmation said. | | `created_at` | string | | | `updated_at` | string | | | `submitted_at` | string or null | | | `status_changed_at` | string or null | | | `sent_to_payer_at` | string or null | | | `live_at` | string or null | | | `ended_at` | string or null | | | `cancel_requested_at` | string or null | | | `requirements_url` | string | The payer’s published steps for this transaction. | | `open_tasks` | integer | List items only: tasks not yet finished. | | `primary_contact` | Contact or null | | | `notes` | list of object | From Payerworks. | | `history` | list of object | Every status change, oldest first. | | `history_truncated` | boolean | | | `warnings` | list of object | On a create or submit: route_not_open while Payerworks’ route is closed. | ### POST /v1/enrollments/{id}/cancel **Cancel an enrollment.** In any open status. Before it is sent to a payer it is canceled at once; once it is with a payer, or live, the answer is 202 and Payerworks withdraws it. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad35808fafb0295307/cancel" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsByIdCancel("enr_01a154ad35808fafb0295307", body); ``` With the Python client ```python answer = pw.post_enrollments_by_id_cancel("enr_01a154ad35808fafb0295307", body) ``` request.json ```json { "reason": "Filed by mistake." } ``` The answer (200) ```json { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "canceled", "status_words": "Canceled. Nothing more happens to this request.", "waiting_on": null, "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 3, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "provider_action_required", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": "2026-10-19T15:00:00.000Z", "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment" | | | `id` | string | enr_ and 24 hexadecimal characters. | | `mode` | one of "test", "live" | | | `provider` | object | | | `payer` | object | | | `transaction` | one of "eligibility", "claim_status", "professional_claims", "remittances" | | | `x12` | string | | | `status` | one of "draft", "payerworks_action_required", "provider_action_required", "with_payer", "live", "rejected", "canceled", "ended" | | | `status_words` | string | | | `waiting_on` | string or null | Whose turn it is; null when nobody’s. | | `reason_code` | string or null | Why it is Payerworks’ turn, at payerworks_action_required. | | `route` | object | | | `notify_email` | string or null | | | `requested_effective_date` | string or null | | | `era_aggregation` | string or null | | | `ptan` | string or null | | | `note` | string | | | `tasks` | list of Task | In rank order. | | `documents` | list of Document | | | `rejection` | Rejection or null | | | `payer_reference` | string or null | What the payer’s confirmation said. | | `created_at` | string | | | `updated_at` | string | | | `submitted_at` | string or null | | | `status_changed_at` | string or null | | | `sent_to_payer_at` | string or null | | | `live_at` | string or null | | | `ended_at` | string or null | | | `cancel_requested_at` | string or null | | | `requirements_url` | string | The payer’s published steps for this transaction. | | `open_tasks` | integer | List items only: tasks not yet finished. | | `primary_contact` | Contact or null | | | `notes` | list of object | From Payerworks. | | `history` | list of object | Every status change, oldest first. | | `history_truncated` | boolean | | | `warnings` | list of object | On a create or submit: route_not_open while Payerworks’ route is closed. | ### POST /v1/tasks/{id} **Answer a task.** Save a task’s values, text or an uploaded document, and finish it with complete: true, in rank order. A task held up by an earlier one is refused naming that task, its rank and who holds it, Payerworks included. complete: false reopens a task finished by mistake while nothing has gone to a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/tasks/tsk_01a154ad35808fafb0295307_01" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postTasksById("tsk_01a154ad35808fafb0295307_01", body); ``` With the Python client ```python answer = pw.post_tasks_by_id("tsk_01a154ad35808fafb0295307_01", body) ``` request.json ```json { "values": [ { "key": "packet_id", "text": "PKT-0001" } ], "complete": true } ``` The answer (200) ```json { "task": { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [ { "key": "packet_id", "text": "PKT-0001" } ], "complete": true, "completed_at": "2026-10-19T15:00:00.000Z", "created_at": "2026-10-19T15:00:00.000Z" }, "enrollment": { "object": "enrollment", "id": "enr_01a154ad35808fafb0295307", "mode": "test", "provider": { "id": "prv_01a154ad358043763d71b95c", "name": "Sample Family Clinic TASKS", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "provider_action_required", "status_words": "Waiting on the provider: 1 step below needs the provider. Finish it, in rank order after any step before it.", "waiting_on": "provider", "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [ { "id": "tsk_01a154ad35808fafb0295307_01", "rank": 0, "responsible": "provider", "title": "Give the sandbox payer’s packet number", "instructions": "Sandbox: the payer emailed a packet number when the provider finished its form. Enter it here.", "fields": [ { "key": "packet_id", "label": "Packet number", "type": "text", "format": "identifier", "required": true, "description": "" } ], "links": [], "template_document_id": null, "values": [ { "key": "packet_id", "text": "PKT-0001" } ], "complete": true, "completed_at": "2026-10-19T15:00:00.000Z", "created_at": "2026-10-19T15:00:00.000Z" }, { "id": "tsk_01a154ad35808fafb0295307_02", "rank": 1, "responsible": "provider", "title": "Finish the sandbox payer’s steps", "instructions": "Sandbox: these steps happen on the payer’s own site. Mark this task complete, with no values, once they are done.", "fields": [], "links": [], "template_document_id": null, "values": [], "complete": false, "completed_at": null, "created_at": "2026-10-19T15:00:00.000Z" } ], "documents": [], "notes": [], "rejection": null, "payer_reference": null, "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 3, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "provider_action_required", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": null, "live_at": null, "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances" } } ``` | Field | Type | What it is | | --- | --- | --- | | `task` | Task | | | `enrollment` | Enrollment | An enrollment request: never the tax id, only its last four. | ### POST /v1/enrollments/{id}/documents **Upload a PDF for a task.** A signed upload address for one PDF of up to 10 MB, good for 15 minutes. Post the file there, then confirm it, or finish the task with its document_id, which confirms it too. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/enr_01a154ad358059512fd26707/documents" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsByIdDocuments("enr_01a154ad358059512fd26707", body); ``` With the Python client ```python answer = pw.post_enrollments_by_id_documents("enr_01a154ad358059512fd26707", body) ``` request.json ```json { "task_id": "tsk_01a154ad358059512fd26707_01", "field_key": "signed_form", "filename": "signed-form.pdf" } ``` The answer (201) ```json { "document": { "id": "doc_01a154ad358059512fd26707_e8039f84", "task_id": "tsk_01a154ad358059512fd26707_01", "field_key": "signed_form", "filename": "signed-form.pdf", "status": "pending", "size": null, "content_type": "application/pdf", "source": "you", "created_at": "2026-10-19T15:00:00.000Z", "uploaded_at": null }, "upload": { "url": "https://uploads.example.com/", "method": "POST", "fields": { "key": "pending/pwk_0123456789abcdef/enr_01a154ad358059512fd26707/doc_01a154ad358059512fd26707_e8039f84.pdf", "Content-Type": "application/pdf" }, "expires_at": "2026-10-19T15:15:00.000Z" } } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | Document | | | `upload` | object | A signed form POST: one PDF, up to 10 MB, within 15 minutes. It takes no key. | ### POST /v1/documents/{id}/confirm **Confirm an upload.** Checks that the file arrived and is a PDF of up to 10 MB. One that is not is deleted, and the document is marked failed. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/documents/doc_01a154ad358059512fd26707_e8039f84/confirm" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postDocumentsByIdConfirm("doc_01a154ad358059512fd26707_e8039f84"); ``` With the Python client ```python answer = pw.post_documents_by_id_confirm("doc_01a154ad358059512fd26707_e8039f84") ``` The answer (200) ```json { "id": "doc_01a154ad358059512fd26707_e8039f84", "task_id": "tsk_01a154ad358059512fd26707_01", "field_key": "signed_form", "filename": "signed-form.pdf", "status": "uploaded", "size": 48213, "content_type": "application/pdf", "source": "you", "created_at": "2026-10-19T15:00:00.000Z", "uploaded_at": "2026-10-19T15:00:00.000Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | doc_, the enrollment’s 24 hexadecimal characters, an underscore and 8 more. | | `task_id` | string or null | | | `field_key` | string or null | | | `filename` | string | | | `status` | one of "pending", "uploaded", "failed" | | | `failure` | string | Why it failed: not_a_pdf, too_large or upload_expired. | | `size` | integer or null | Bytes. | | `content_type` | the value "application/pdf" | | | `source` | one of "you", "payerworks" | Who added it. | | `created_at` | string | | | `uploaded_at` | string or null | | ### GET /v1/documents/{id} **Download a document.** A signed download address, good for 15 minutes, for a PDF you uploaded or a form Payerworks attached to a task. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/documents/doc_01a154ad358059512fd26707_e8039f84" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getDocumentsById("doc_01a154ad358059512fd26707_e8039f84"); ``` With the Python client ```python answer = pw.get_documents_by_id("doc_01a154ad358059512fd26707_e8039f84") ``` The answer (200) ```json { "document": { "id": "doc_01a154ad358059512fd26707_e8039f84", "task_id": "tsk_01a154ad358059512fd26707_01", "field_key": "signed_form", "filename": "signed-form.pdf", "status": "uploaded", "size": 48213, "content_type": "application/pdf", "source": "you", "created_at": "2026-10-19T15:00:00.000Z", "uploaded_at": "2026-10-19T15:00:00.000Z" }, "download": { "url": "https://downloads.example.com/signed-form.pdf", "expires_at": "2026-10-19T15:15:00.000Z" } } ``` | Field | Type | What it is | | --- | --- | --- | | `document` | Document | | | `download` | object | A signed GET, good for 15 minutes. | ### DELETE /v1/documents/{id} **Delete a document.** Refused while a finished task names it: reopen the task first. Deleting again answers the same. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/documents/doc_01a154ad358059512fd26707_e8039f84" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteDocumentsById("doc_01a154ad358059512fd26707_e8039f84"); ``` With the Python client ```python answer = pw.delete_documents_by_id("doc_01a154ad358059512fd26707_e8039f84") ``` The answer (200) ```json { "deleted": true, "id": "doc_01a154ad358059512fd26707_e8039f84" } ``` | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | | `id` | string | | ### GET /v1/enrollment-requirements **Payers with published steps.** Every payer Payerworks holds published enrollment steps for, the four Medicare DME MACs and CMS’s HETS, and the sandbox payer: per transaction, who files, the fields needed and each step, with its source and the day it was read. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollment-requirements" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollmentRequirements(); ``` With the Python client ```python answer = pw.get_enrollment_requirements() ``` The answer (200) ```json { "object": "list", "data": [ { "payer": { "id": "16013", "name": "Medicare DME MAC Jurisdiction A", "sandbox": false }, "transactions": [ { "transaction": "eligibility", "x12": "270/271", "applies": false }, { "transaction": "claim_status", "x12": "276/277", "applies": true } ] }, { "payer": { "id": "17013", "name": "Medicare DME MAC Jurisdiction B", "sandbox": false }, "transactions": [ { "transaction": "eligibility", "x12": "270/271", "applies": false }, { "transaction": "claim_status", "x12": "276/277", "applies": true } ] } ], "checked_on": "2026-10-19", "scope": "The payers Payerworks holds published enrollment steps for, and the sandbox payer. Every other payer id answers published: false with no steps; that never means nothing is required." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "list" | | | `checked_on` | string | | | `scope` | string | | | `data` | list of object | | ### GET /v1/enrollment-requirements/{payer_id} **What a payer asks.** For any payer id, per transaction: whether Payerworks’ route is open, who files, the fields needed and each published step with its source. A payer Payerworks holds nothing for answers published: false, never not required. Add ?transaction= for one. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollment-requirements/19003" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollmentRequirementsByPayerId("19003"); ``` With the Python client ```python answer = pw.get_enrollment_requirements_by_payer_id("19003") ``` The answer (200) ```json { "object": "enrollment_requirements", "payer": { "id": "19003", "name": "Medicare DME MAC Jurisdiction D", "sandbox": false }, "published": true, "checked_on": "2026-10-19", "transactions": [ { "transaction": "eligibility", "x12": "270/271", "applies": false, "ask": { "payer_id": "CMS", "words": "Traditional Medicare eligibility is asked of CMS’s HETS, not a DME MAC. File eligibility with payer CMS.", "source": { "url": "https://www.cms.gov/data-research/cms-information-technology/hipaa-eligibility-transaction-system-hets/hets-electronic-data-interchange-edi-enrollment", "label": "CMS: HETS EDI Enrollment (page last modified 10/13/2026)", "checked_on": "2026-10-19" } } }, { "transaction": "claim_status", "x12": "276/277", "applies": true, "route": { "open": false, "waiting_on": "payerworks", "words": "Payerworks is not yet a CEDI trading partner, so it cannot exchange 276/277 files with any DME MAC. A request for this waits at payerworks_action_required and nothing is sent." }, "where": { "name": "CEDI, the front end for all four DME MACs", "url": "https://cedi.wellpointfederal.com/enrollment" }, "who_files": "provider", "fields_required": [ "ptan" ], "steps": [ { "key": "npi_active_in_pecos", "who": "provider", "words": "The provider’s NPI is active in PECOS, Medicare’s enrollment system. CEDI checks it before it signs the provider up for EDI, and denies the EDI agreement when it cannot find it.", "source": { "url": "https://www.cms.gov/regulations-and-guidance/guidance/manuals/downloads/clm104c24.pdf", "label": "CMS Medicare Claims Processing Manual (Pub. 100-04), chapter 24, section 30.1", "checked_on": "2026-10-19" } }, { "key": "edi_agreement", "who": "provider", "words": "Every provider that exchanges files with Medicare, directly or through a clearinghouse, signs the EDI enrollment agreement before CEDI accepts its claims or sends it any file. For the DME MACs that agreement is CEDI’s online packet.", "source": { "url": "https://www.cms.gov/regulations-and-guidance/guidance/manuals/downloads/clm104c24.pdf", "label": "CMS Medicare Claims Processing Manual (Pub. 100-04), chapter 24, section 30.2", "checked_on": "2026-10-19" } } ], "timing": { "words": "CEDI processes a new enrollment request within 5 business days of receipt, and a large request may take longer. A rejected packet is emailed with its reasons, and a new packet is needed.", "source": { "url": "https://cedi.wellpointfederal.com/documents/20124/298616/DME_CEDI_FAQ.pdf/5b891dee-4847-d113-e04d-ba5130fb36ae", "label": "CEDI Frequently Asked Questions (last revision 03/19/2026)", "checked_on": "2026-10-19" } } } ], "words": "These are the payer’s own published steps, read on 10/19/2026. Payerworks is not registered with this payer." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment_requirements" | | | `payer` | object | | | `published` | boolean | false: Payerworks holds no published steps for this payer, which never means nothing is required. | | `checked_on` | string | | | `transactions` | list of RequirementTransaction | | | `words` | string | | ### POST /v1/sandbox/enrollments/{id}/advance **Advance a sandbox enrollment.** The sandbox payer’s next move on a test request, with the events it raised. A test key only; the last word of the provider’s name picks the scenario. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/enrollments/enr_01a154ad35802c85a436872c/advance" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postSandboxEnrollmentsByIdAdvance("enr_01a154ad35802c85a436872c"); ``` With the Python client ```python answer = pw.post_sandbox_enrollments_by_id_advance("enr_01a154ad35802c85a436872c") ``` The answer (200) ```json { "enrollment": { "object": "enrollment", "id": "enr_01a154ad35802c85a436872c", "mode": "test", "provider": { "id": "prv_01a154ad35807d5d51823b6e", "name": "Sample Family Clinic SLOW", "npi": "1234567893", "tax_id_last_four": "6789" }, "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER", "sandbox": true }, "transaction": "remittances", "x12": "835", "status": "live", "status_words": "Sandbox: live with the sandbox payer. It is invented, so no files flow.", "waiting_on": null, "reason_code": null, "route": { "open": true, "sandbox": true, "words": "The sandbox payer answers every enrollment in the sandbox. Nothing is sent anywhere." }, "primary_contact": { "organization_name": "Sample Family Clinic LLC", "first_name": null, "last_name": null, "email": "enrollment@sampleclinic.example", "phone": "8015550100", "phone_extension": null, "address": { "line1": "100 Main St", "line2": null, "city": "Salt Lake City", "state": "UT", "zip": "84101" } }, "notify_email": "dev@sampleclinic.example", "requested_effective_date": null, "era_aggregation": null, "ptan": null, "note": "", "tasks": [], "documents": [], "notes": [], "rejection": null, "payer_reference": "PWSENR3814935860", "history": [ { "sequence": 1, "at": "2026-10-19T15:00:00.000Z", "from": null, "to": "payerworks_action_required", "by": "you", "reason_code": "preparing" }, { "sequence": 2, "at": "2026-10-19T15:00:00.000Z", "from": "payerworks_action_required", "to": "with_payer", "by": "sandbox", "reason_code": null } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "submitted_at": "2026-10-19T15:00:00.000Z", "status_changed_at": "2026-10-19T15:00:00.000Z", "sent_to_payer_at": "2026-10-19T15:00:00.000Z", "live_at": "2026-10-19T15:00:00.000Z", "ended_at": null, "cancel_requested_at": null, "requirements_url": "/v1/enrollment-requirements/PWSANDBOX?transaction=remittances" }, "moved": { "from": "with_payer", "to": "live" }, "events": [ { "id": "evt_bd653cd8c0654191bf653ffe", "type": "enrollment.live" } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `enrollment` | Enrollment | An enrollment request: never the tax id, only its last four. | | `moved` | object or null | | | `events` | list of object | | ### POST /v1/enrollments/import/check **Check an enrollment import.** Send { csv } with the template’s header and up to 100 rows, one enrollment request a row. Each row is checked exactly as POST /v1/providers and POST /v1/enrollments check their bodies, each problem named by its column, and said as ready, invalid or duplicate (a request that already stands, or an earlier row asking the same). With a live key, each NPI is looked up in the NPI registry. Nothing is saved. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/import/check" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsImportCheck(body); ``` With the Python client ```python answer = pw.post_enrollments_import_check(body) ``` request.json ```json { "csv": "provider_name,npi,tax_id,tax_id_type,payer_id,payer_name,transaction,contact_organization_name,contact_first_name,contact_last_name,contact_email,contact_phone,contact_phone_extension,contact_address_line1,contact_address_line2,contact_city,contact_state,contact_zip,notify_email,requested_effective_date,era_aggregation,ptan,note,submit\r\nSample Family Clinic,1234567893,12-3456789,,PWSANDBOX,,remittances,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,62701,dev@example.com,,,,,\r\nSample Family Clinic,1234567893,12-3456789,,PWSANDBOX,,eligibility,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,62701,dev@example.com,,,,,true\r\nSample Family Clinic,1234567890,12-3456789,,PWSANDBOX,,remittances,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,6270,dev@example.com,,,,," } ``` The answer (200) ```json { "object": "enrollment_import_check", "saved": false, "rows": 3, "results": { "ready": 2, "invalid": 1, "duplicate": 0 }, "data": [ { "row": 1, "line": 2, "provider": { "id": null, "exists": false }, "enrollment": null, "problems": [], "warnings": [], "npi_registry": { "checked": false, "npi": "1234567893", "reason": "test_key", "words": "A test key's NPIs are invented, so they are not looked up in the NPI registry. A live key's are." }, "result": "ready" }, { "row": 2, "line": 3, "provider": { "id": null, "exists": false }, "enrollment": null, "problems": [], "warnings": [], "npi_registry": { "checked": false, "npi": "1234567893", "reason": "test_key", "words": "A test key's NPIs are invented, so they are not looked up in the NPI registry. A live key's are." }, "result": "ready" } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment_import_check" | | | `saved` | the value false | Nothing was saved. | | `rows` | integer | | | `results` | object | How many rows ended each way. | | `data` | list of ImportRow | | ### POST /v1/enrollments/import **Import enrollment requests.** The same file, each valid row made into one request, and its provider when your key has none with that NPI and tax id. A provider you already have is used as it is, never changed. Each row answers created, invalid, duplicate, refused or not_run; rows not started in time are not_run from not_run_from_row, so send those again. A row is submitted only when its submit cell says true. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/enrollments/import" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEnrollmentsImport(body); ``` With the Python client ```python answer = pw.post_enrollments_import(body) ``` request.json ```json { "csv": "provider_name,npi,tax_id,tax_id_type,payer_id,payer_name,transaction,contact_organization_name,contact_first_name,contact_last_name,contact_email,contact_phone,contact_phone_extension,contact_address_line1,contact_address_line2,contact_city,contact_state,contact_zip,notify_email,requested_effective_date,era_aggregation,ptan,note,submit\r\nSample Family Clinic,1234567893,12-3456789,,PWSANDBOX,,remittances,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,62701,dev@example.com,,,,,\r\nSample Family Clinic,1234567893,12-3456789,,PWSANDBOX,,eligibility,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,62701,dev@example.com,,,,,true\r\nSample Family Clinic,1234567890,12-3456789,,PWSANDBOX,,remittances,SAMPLE FAMILY CLINIC LLC,,,enrollment@example.com,5555550100,,100 MAIN STREET,,SPRINGFIELD,IL,6270,dev@example.com,,,,," } ``` The answer (200) ```json { "object": "enrollment_import", "saved": true, "rows": 3, "results": { "created": 2, "invalid": 1, "duplicate": 0, "refused": 0, "not_run": 0 }, "data": [ { "row": 1, "line": 2, "provider": { "id": "prv_01a15aba4882eabbf1056c4e", "created": true }, "enrollment": { "id": "enr_01a15aba4882c925e2480746", "status": "draft", "waiting_on": "you" }, "problems": [], "warnings": [], "npi_registry": { "checked": false, "npi": "1234567893", "reason": "test_key", "words": "A test key's NPIs are invented, so they are not looked up in the NPI registry. A live key's are." }, "result": "created" }, { "row": 2, "line": 3, "provider": { "id": "prv_01a15aba4882eabbf1056c4e", "created": false }, "enrollment": { "id": "enr_01a15aba488250af02bb4634", "status": "live", "waiting_on": null }, "problems": [], "warnings": [], "npi_registry": { "checked": false, "npi": "1234567893", "reason": "test_key", "words": "A test key's NPIs are invented, so they are not looked up in the NPI registry. A live key's are." }, "result": "created" } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment_import" | | | `saved` | boolean | | | `rows` | integer | | | `results` | object | How many rows ended each way. | | `data` | list of ImportRow | | | `not_run_from_row` | integer | The first row not started in time: send it and the rows after it again. | ### GET /v1/enrollments/export **Export your enrollments.** Every request matching the list’s filters, as CSV text in the answer, up to 5,000 rows: status, whose turn, provider, payer, transaction and the dates. No contact, note or full tax id. A cell a spreadsheet would run as a formula starts with an apostrophe. It takes no limit or starting_after. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollments/export" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollmentsExport(); ``` With the Python client ```python answer = pw.get_enrollments_export() ``` The answer (200) ```json { "object": "enrollment_export", "content_type": "text/csv", "filename": "payerworks-enrollments-2026-10-20.csv", "row_count": 1, "columns": [ "id", "status" ], "csv": "id,status,waiting_on,reason_code,provider_id,provider_name,npi,tax_id_last_four,payer_id,payer_name,transaction,x12,notify_email,requested_effective_date,era_aggregation,ptan,open_tasks,payer_reference,created_at,updated_at,submitted_at,sta…" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment_export" | | | `content_type` | the value "text/csv" | | | `filename` | string | | | `row_count` | integer | | | `columns` | list of one of "id", "status", "waiting_on", "reason_code", "provider_id", "provider_name", "npi", "tax_id_last_four", "payer_id", "payer_name", "transaction", "x12", "notify_email", "requested_effective_date", "era_aggregation", "ptan", "open_tasks", "payer_reference", "created_at", "updated_at", "submitted_at", "status_changed_at", "sent_to_payer_at", "live_at", "ended_at" | | | `csv` | string | The CSV’s text: a cell a spreadsheet would run as a formula starts with an apostrophe. | ### GET /v1/enrollments/durations **How long your requests took.** By payer and transaction, how long your own requests spent waiting on Payerworks, on the provider and with the payer, and from submitted to live: how many finished, and once at least five did, the median and the middle half in hours. Fewer than five says not enough history. From your key’s own history only: Payerworks has no other figures. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/enrollments/durations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEnrollmentsDurations(); ``` With the Python client ```python answer = pw.get_enrollments_durations() ``` The answer (200) ```json { "object": "enrollment_durations", "basis": "This key's own requests, from their own history: no other key's requests, no figure from anywhere else and no model. A payer's time is shown only once this key has enough finished stays with it.", "floor": 5, "as_of": "2026-10-20T19:12:00.138Z", "statuses": [ "payerworks_action_required", "provider_action_required" ], "data": [ { "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER" }, "transaction": "eligibility", "requests": 1, "submitted_to_live": { "n": 1, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 1 finished stay from submitted to live on this key, and 5 are needed before a figure is given." }, "in_status": { "payerworks_action_required": { "n": 1, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 1 finished stay in payerworks_action_required on this key, and 5 are needed before a figure is given." }, "provider_action_required": { "n": 0, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 0 finished stays in provider_action_required on this key, and 5 are needed before a figure is given." }, "with_payer": { "n": 1, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 1 finished stay in with_payer on this key, and 5 are needed before a figure is given." } } }, { "payer": { "id": "PWSANDBOX", "name": "PAYERWORKS SANDBOX PAYER" }, "transaction": "remittances", "requests": 1, "submitted_to_live": { "n": 0, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 0 finished stays from submitted to live on this key, and 5 are needed before a figure is given." }, "in_status": { "payerworks_action_required": { "n": 0, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 0 finished stays in payerworks_action_required on this key, and 5 are needed before a figure is given." }, "provider_action_required": { "n": 0, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 0 finished stays in provider_action_required on this key, and 5 are needed before a figure is given." }, "with_payer": { "n": 0, "open": 0, "enough": false, "median_hours": null, "p25_hours": null, "p75_hours": null, "words": "Not enough history: 0 finished stays in with_payer on this key, and 5 are needed before a figure is given." } } } ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "enrollment_durations" | | | `basis` | string | | | `floor` | the value 5 | | | `as_of` | string | | | `statuses` | list of string | | | `data` | list of object | | ## Reading the answer - A request’s `status` is one of eight, with `waiting_on` saying whose turn it is and `reason_code` why. The statuses and what moves each are listed below. - Every change is an event (`enrollment.updated` and the rest) with a `sequence` that only increases, so a receiver can tell an old event from a new one. ## Troubleshooting - `provider_exists` (409): This key already has a provider with that NPI and tax id; the error names it, so use that one. - `enrollment_exists` (409): The provider already has an open request for that payer and transaction; the error names it and its status. - `task_blocked` (409): Tasks are finished in rank order; the error names the open task before this one and who holds it. - `upload_missing` (409): Confirm only after the PDF reached the upload address the API answered. - `sandbox_nothing_to_advance` (409): The sandbox payer has no move to make now: the next move is yours, or the request has ended. - `changed_meanwhile` (409): The request changed while yours ran, so nothing was saved: read it again and send again. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `export_takes_no_paging` | 400 | An export holds every request that matches its filters, so it takes no limit or starting_after. | | `invalid_date_filter` | 400 | A date filter is a day, YYYY-MM-DD, or an instant, YYYY-MM-DDTHH:MM:SSZ. | | `invalid_document_id` | 400 | A document id is doc_, the enrollment’s 24 hexadecimal characters, an underscore and 8 more. | | `invalid_enrollment_id` | 400 | An enrollment id is enr_ followed by 24 lowercase hexadecimal characters. | | `invalid_npi_filter` | 400 | npi is the provider’s 10-digit NPI, with a valid check digit. | | `invalid_payer_filter` | 400 | payer_id is 1 to 80 letters and digits. | | `invalid_payer_id` | 400 | A payer id is 1 to 80 letters and digits. | | `invalid_provider_filter` | 400 | provider_id is prv_ followed by 24 lowercase hexadecimal characters. | | `invalid_provider_id` | 400 | A provider id is prv_ followed by 24 lowercase hexadecimal characters. | | `invalid_starting_after` | 400 | starting_after is the next_cursor of an earlier page, or the id of an item on an earlier page. | | `invalid_status_filter` | 400 | status is one or more of the eight enrollment statuses, separated by commas. | | `invalid_task_id` | 400 | A task id is tsk_, the enrollment’s 24 hexadecimal characters, an underscore and two digits. | | `invalid_tax_id_filter` | 400 | tax_id is the provider’s 9-digit EIN, with or without the hyphen after the second digit. | | `invalid_transaction_filter` | 400 | transaction is eligibility, claim_status, professional_claims or remittances. | | `invalid_waiting_on_filter` | 400 | waiting_on is you, provider, payerworks or payer. | | `q_too_long` | 400 | q is at most 100 characters. | | `document_not_found` | 404 | This key has no document with that id. | | `enrollment_not_found` | 404 | This key has no enrollment request with that id. | | `provider_not_found` | 404 | This key has no provider with that id. | | `task_not_found` | 404 | This key has no task with that id. | | `changed_meanwhile` | 409 | This request changed while yours ran, so nothing was saved. Send it again. | | `document_in_use` | 409 | A completed task names this document. Reopen the task first (complete: false), then delete it. | | `document_limit_reached` | 409 | An enrollment request holds at most 20 documents. Delete one you no longer need. | | `document_not_uploaded` | 409 | This document has not been uploaded and confirmed, so there is nothing to download. | | `enrollment_ended` | 409 | This request is canceled, rejected or ended; nothing more can change. File a new request. | | `enrollment_exists` | 409 | This provider already has an open request for this payer and transaction; the error names it and its status. | | `enrollment_limit_reached` | 409 | A key holds at most 5,000 enrollment requests (a test key 500). Delete drafts and ended requests you no longer need. | | `enrollment_not_deletable` | 409 | Only a draft, canceled or rejected request can be deleted. Cancel it first. | | `enrollment_not_draft` | 409 | Only a draft is submitted; this request already was. | | `enrollment_sent` | 409 | This request has gone to the payer, so its fields can no longer change. Cancel it and file a new one. | | `provider_exists` | 409 | This key already has a provider with this NPI and tax id; the error names it. | | `provider_has_enrollments` | 409 | This provider has requests that are open or live; cancel them first. The error names them. | | `provider_in_use` | 409 | The NPI and tax id can no longer change: a request for this provider has gone to a payer. The error names those requests. | | `provider_limit_reached` | 409 | A key holds at most 500 providers (a test key 50). Delete one you no longer use. | | `sandbox_nothing_to_advance` | 409 | The sandbox payer has no move to make now: the next move is yours, the request has ended, or this scenario never moves. The error says whose turn it is. | | `task_blocked` | 409 | A task ranked before this one is still open; the error names it, its rank and who holds it. Tasks are finished in rank order. | | `task_is_payerworks` | 409 | This task is Payerworks’ to do, not the provider’s. It is shown so you can see what Payerworks is waiting on. | | `task_not_open` | 409 | This request is not waiting on the provider, so its tasks cannot change now. | | `upload_missing` | 409 | Nothing has arrived at the upload address yet. Upload the PDF, then confirm again. | | `export_too_large` | 413 | Every request matching these filters does not fit in one export. Narrow the filters (a payer, a transaction, a created range) and export each part. | | `document_too_large` | 422 | A document is at most 10 MB. It was deleted; upload a smaller PDF. | | `field_not_a_document` | 422 | That field of the task does not take a document. | | `invalid_cancel` | 422 | reason is text of at most 200 characters, with no run of eight or more digits. | | `invalid_document_request` | 422 | Send task_id, field_key and filename (ending .pdf, at most 120 characters); each problem is listed with its field. | | `invalid_enrollment` | 422 | One or more enrollment fields would be refused; nothing was saved. Each problem is listed with its field. | | `invalid_import` | 422 | The CSV could not be read as an enrollment import, so no row was checked or saved. Each problem is listed; the columns are in the import template. | | `invalid_provider` | 422 | One or more provider fields would be refused; nothing was saved. Each problem is listed with its field. | | `invalid_task_values` | 422 | One or more task values would be refused; nothing was saved. Each problem is listed with its field. | | `not_a_pdf` | 422 | The uploaded file is not a PDF (it does not start %PDF-). It was deleted; upload the PDF again. | | `upload_unavailable` | 503 | No upload address could be made just now; nothing was saved. Retry in a moment. | ## Code lists ### The eight statuses | Status | Waiting on | What it means | Who moves it | | --- | --- | --- | --- | | `draft` | `you` | Being written. Only this key sees it. | You: submit, change or delete it. | | `payerworks_action_required` | `payerworks` | Submitted; Payerworks’ turn. reason_code says why. | Payerworks staff on the desk, or the sandbox. | | `provider_action_required` | `provider` | One or more provider tasks are open. | You, completing the tasks in rank order. | | `with_payer` | `payer` | Payerworks sent the request to the payer and follows up. Never set while the route is not open. | Payerworks staff, recording the payer’s answer. | | `live` | nobody | The payer confirmed. Files of this kind may flow. | Payerworks staff, if the payer later ends it. | | `rejected` | `you` | The payer, or Payerworks before sending, refused it. rejection holds the words and the remedy. | None: fix it and file a new request. | | `canceled` | nobody | Ended at your request or by Payerworks before it went live. | None. | | `ended` | nobody | Was live; the payer ended the route. | None: a new request starts again. | ### The transactions a request names | Transaction | X12 | In words | | --- | --- | --- | | `eligibility` | 270/271 | Eligibility and benefits: Payerworks writes the 270 and reads the 271. | | `claim_status` | 276/277 | Claim status: Payerworks writes the 276 and reads the 277. | | `professional_claims` | 837P | Professional claims: Payerworks writes and checks the 837P. | | `remittances` | 835 | Remittances: Payerworks reads and explains the 835. | ### The enrollment sandbox’s provider name endings | Name ends in | What it does | | --- | --- | | any other word | Sandbox: a provider whose name ends in any other word goes live in one call: submitting moves it to payerworks_action_required (preparing), then with_payer, then live, with an event for each move. Advance has nothing to do. | | `WAITS` | Sandbox: a provider whose name ends in WAITS waits at payerworks_action_required (route_not_open) and stays there. That is the state every live request is in today, so build that screen first. | | `TASKS` | Sandbox: a provider whose name ends in TASKS gets two provider tasks on submit: rank 0 asks for a packet number (an identifier), rank 1 is steps done elsewhere, closed with no values. Once both are complete it waits for review; advance moves it to with_payer, then live. | | `SIGN` | Sandbox: a provider whose name ends in SIGN gets one task with a document field, signed_form, and an invented one-page form as its template. Download the template, upload a signed PDF, complete the task; advance moves it to with_payer, then live. | | `BLOCKED` | Sandbox: a provider whose name ends in BLOCKED gets a Payerworks task at rank 0 and a provider task at rank 1. Completing rank 1 first answers 409 task_blocked, naming the Payerworks task. Advance completes the Payerworks task; complete yours; advance moves it to with_payer and live. | | `REPLACE` | Sandbox: a provider whose name ends in REPLACE starts as TASKS. The first advance deletes the open rank 0 task and adds a new one in its place (enrollment.task.deleted with superseded_by, then enrollment.task.assigned). | | `REJECT` | Sandbox: a provider whose name ends in REJECT goes with_payer on submit; advance rejects it with the payer’s words and the remedy, carried whole. | | `ENDS` | Sandbox: a provider whose name ends in ENDS goes live on submit; advance ends the live route. | | `SLOW` | Sandbox: a provider whose name ends in SLOW goes with_payer on submit and stays there until advance moves it to live. | ### What enrollment keeps, and for how long Enrollment is the one part of the API that keeps what you send: the providers and enrollment requests you create (names, NPIs, tax ids, contacts and notes) and the PDFs you upload for a task, because a request lives for weeks and Payerworks people work it. They are for a practice’s business facts; never put a patient’s information in them. A test key’s providers and requests expire 30 days after their last change, and its PDFs 30 days after each was uploaded, or about a day after you delete one; a PDF uploaded but never confirmed goes after a day, whatever the key. A live key’s are kept until you delete them, and the copy of a deleted live PDF for 30 days more. Apart from that, our backups hold every PDF that was there each night, a test key’s included, for 35 days, and each Sunday’s for a year. ## Example [enrollment.mjs](https://payerworks.com/docs/examples/enrollment.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. enrollment.mjs ```js // Payerworks example: transaction enrollment with the sandbox payer. Docs: https://payerworks.com/docs/enrollment // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node enrollment.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { PROVIDER, ENROLLMENT_CONTACT, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The provider, once. Enrollment keeps what you send (a practice's business facts, never a patient's). // The sandbox payer reads the last word of the provider's name: SLOW waits with the payer until you advance it. const provider = await pw.postProviders({ ...copy(PROVIDER), name: `${PROVIDER.name} SLOW` }); // 2. Ask the sandbox payer PWSANDBOX for its remittances, and hand the request to Payerworks at once. const requested = await pw.postEnrollments({ provider_id: provider.id, payer: { id: 'PWSANDBOX' }, transaction: 'remittances', primary_contact: copy(ENROLLMENT_CONTACT), notify_email: 'dev@sampleclinic.example', submit: true, }); // 3. The sandbox payer's next move: with the payer, then live. Each move is an event too. const advanced = await pw.postSandboxEnrollmentsByIdAdvance(requested.id); // 4. The failure path: a live request has nothing left for the sandbox payer to do. const refused = await refusalOf(() => pw.postSandboxEnrollmentsByIdAdvance(requested.id), { PayerworksError }); return { submitted: requested.status, moved: advanced.moved, now: advanced.enrollment.status, events: advanced.events.map((e) => e.type), refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "submitted": "with_payer", "moved": { "from": "with_payer", "to": "live" }, "now": "live", "events": [ "enrollment.live" ], "refused": { "status": 409, "code": "sandbox_nothing_to_advance", "request_id": "req_4" } } ``` # Events and webhooks > Every answer the claims sandbox makes, and every change to an enrollment request, posted to you as a signed event and kept for 30 days. Page: https://payerworks.com/docs/events · Markdown: https://payerworks.com/docs/events.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. An event is a row before it is sent: every answer the claims sandbox makes, and every change to an enrollment request, is recorded on your account and posted to your webhook, and to each webhook destination you add, signed with a secret only you and Payerworks hold. Every event is kept for 30 days, to list and to send again. Your server may receive an event more than once. Each event carries one id in the webhook-id header: keep the ids you have handled and skip one you have seen. New here? Start with [Following an event](https://payerworks.com/docs/quickstart-events). ## Routes - [PUT /v1/account/webhook](#put-v1-account-webhook): Set your webhook - [DELETE /v1/account/webhook](#delete-v1-account-webhook): Remove your webhook - [POST /v1/account/webhook/test](#post-v1-account-webhook-test): Send a test event - [GET /v1/events](#get-v1-events): List your events - [GET /v1/events/{id}](#get-v1-events-id): One event - [POST /v1/events/{id}/redeliver](#post-v1-events-id-redeliver): Send an event again - [GET /v1/events/{id}/attempts](#get-v1-events-id-attempts): Every attempt for an event - [GET /v1/webhook-destinations](#get-v1-webhook-destinations): List your webhook destinations - [POST /v1/webhook-destinations](#post-v1-webhook-destinations): Add a webhook destination - [GET /v1/webhook-destinations/{id}](#get-v1-webhook-destinations-id): One webhook destination - [PUT /v1/webhook-destinations/{id}](#put-v1-webhook-destinations-id): Change a webhook destination - [DELETE /v1/webhook-destinations/{id}](#delete-v1-webhook-destinations-id): Remove a webhook destination - [POST /v1/webhook-destinations/{id}/ping](#post-v1-webhook-destinations-id-ping): Ping a webhook destination - [GET /v1/webhook-destinations/{id}/deliveries](#get-v1-webhook-destinations-id-deliveries): What a destination received Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### PUT /v1/account/webhook **Set your webhook.** The https address Payerworks posts your events to. The first answer carries the signing secret, once. Setting the address again keeps the secret; send rotate: true for a new one, and the previous secret keeps signing beside it for overlap_hours (0 to 720, 24 by default). Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/account/webhook" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putAccountWebhook(body); ``` With the Python client ```python answer = pw.put_account_webhook(body) ``` request.json ```json { "url": "https://hooks.example.com/payerworks/events" } ``` The answer (200) ```json { "url": "https://hooks.example.com/payerworks/events", "secret": "whsec_bTqF5EiMSt1kZ4Sh577Ya/UQMCWse+eDI5CyF9JdOsk=", "secret_last_four": "Osk=", "created_at": "2026-10-19T15:00:00.000Z", "rotated": false, "previous_secret_expires_at": null } ``` | Field | Type | What it is | | --- | --- | --- | | `url` | string | | | `secret` | string or null | whsec_ and the secret. Set only when this request made one (the first set, or rotate: true); null otherwise. | | `secret_last_four` | string | | | `created_at` | string | | | `rotated` | boolean | | | `previous_secret_expires_at` | string or null | | ### DELETE /v1/account/webhook **Remove your webhook.** No more events are sent. Removing a webhook that is not there is not an error. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/account/webhook" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteAccountWebhook(); ``` With the Python client ```python answer = pw.delete_account_webhook() ``` The answer (200) ```json { "deleted": true } ``` | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | ### POST /v1/account/webhook/test **Send a test event.** One ping event queued for your webhook, signed like every other. The answer is 202 with the event’s id; GET /v1/events/{id} says a moment later what your server did with it. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/account/webhook/test" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postAccountWebhookTest(); ``` With the Python client ```python answer = pw.post_account_webhook_test() ``` The answer (202) ```json { "queued": true, "event_id": "evt_7cb356efb22e856293c87091" } ``` | Field | Type | What it is | | --- | --- | --- | | `queued` | the value true | | | `event_id` | string | The event’s id: GET /v1/events/{id} says what your server did with it. | | `destination_id` | string | A redelivery to a webhook destination: which one. | ### GET /v1/events **List your events.** Every webhook event queued for your key that is still kept (each is kept 30 days), newest first, each as it was sent with what delivering it did. Page with limit (up to 100) and starting_after (the next_cursor of the page before, or an event id), and filter with type, delivery_state (none: recorded for your webhook destinations only, with no account webhook set) and created_gte and created_lt. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEvents(); ``` With the Python client ```python answer = pw.get_events() ``` The answer (200) ```json { "data": [ { "id": "evt_4fbc127e84a0331dfb743e10", "type": "sandbox.remittance.received", "created": 1792422000, "mode": "test", "data": { "check_number": "PWSANDBOX-000000003-1", "payment_date": "20261019", "total": 1392, "claims": [ { "reference": "PW-SBX-PAID", "payer_claim_number": "PWS2292711347", "status_code": "1", "status": "processed as primary", "paid": 1392 } ] }, "delivery": { "state": "delivered", "attempts": 1, "last_status": 200, "last_attempt_at": "2026-10-19T15:00:00.000Z", "last_error": null, "delivered_at": "2026-10-19T15:00:00.000Z" } }, { "id": "evt_e38518c4523f2dcd24e34de2", "type": "sandbox.claim.acknowledged", "created": 1792422000, "mode": "test", "data": { "reference": "PW-SBX-PAID", "payer_claim_number": "PWS2292711347", "verdict": "A", "fixes": [ "The claim was accepted into the payer's adjudication system: accepted for processing (STC A2:20). Nothing to fix." ] }, "delivery": { "state": "delivered", "attempts": 1, "last_status": 200, "last_attempt_at": "2026-10-19T15:00:00.000Z", "last_error": null, "delivered_at": "2026-10-19T15:00:00.000Z" } } ], "has_more": true, "next_cursor": "cur_45233137393234313634363138393523303031236576745f376362333536656662323265383536323933633837303931" } ``` | Field | Type | What it is | | --- | --- | --- | | `data` | list of Event | Newest first. | | `has_more` | boolean | | | `next_cursor` | string or null | Pass as starting_after for the next page. | ### GET /v1/events/{id} **One event.** One event by its id, as it was sent, with its delivery: pending, delivered or failed, how many times it was posted, the last status and the last error. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events/evt_4ba3d86a9cb07e1b79f6e144" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEventsById("evt_4ba3d86a9cb07e1b79f6e144"); ``` With the Python client ```python answer = pw.get_events_by_id("evt_4ba3d86a9cb07e1b79f6e144") ``` The answer (200) ```json { "id": "evt_4fbc127e84a0331dfb743e10", "type": "sandbox.remittance.received", "created": 1792422000, "mode": "test", "data": { "check_number": "PWSANDBOX-000000003-1", "payment_date": "20261019", "total": 1392, "claims": [ { "reference": "PW-SBX-PAID", "payer_claim_number": "PWS2292711347", "status_code": "1", "status": "processed as primary", "paid": 1392 } ] }, "delivery": { "state": "delivered", "attempts": 1, "last_status": 200, "last_attempt_at": "2026-10-19T15:00:00.000Z", "last_error": null, "delivered_at": "2026-10-19T15:00:00.000Z" } } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | evt_ and hex. The same each time the same event is sent. | | `type` | one of "ping", "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received", "enrollment.updated", "enrollment.live", "enrollment.rejected", "enrollment.canceled", "enrollment.ended", "enrollment.task.assigned", "enrollment.task.completed", "enrollment.task.deleted", "job.completed", "job.failed", "submission.file.acknowledged", "submission.claim.acknowledged", "submission.remittance.received" | | | `created` | integer | Unix seconds. | | `mode` | one of "test", "live" | The key’s: an enrollment event of a live key is live, and a submission event always is; a ping and the sandbox payer’s events are always test. | | `data` | object | The event’s own facts: for a sandbox event, invented answers, your claim references and your file’s control numbers; for an enrollment event, ids, statuses and words only; for a submission event, your references, the control numbers and the partner’s verdicts and amounts, never a name or a member ID. | | `delivery` | EventDelivery | | ### POST /v1/events/{id}/redeliver **Send an event again.** The event queued once more for the address your webhook has now, signed the same way and with the same id. The answer is 202, and the event’s delivery then says what your server did. It counts against your key’s six deliveries a minute. Send destination_id to send it to that webhook destination instead, as a manual attempt that starts its retries again from the first. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/events/evt_4ba3d86a9cb07e1b79f6e144/redeliver" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postEventsByIdRedeliver("evt_4ba3d86a9cb07e1b79f6e144", body); ``` With the Python client ```python answer = pw.post_events_by_id_redeliver("evt_4ba3d86a9cb07e1b79f6e144", body) ``` request.json ```json { "destination_id": "wd_4e1687f5d8638cbd" } ``` The answer (202) ```json { "queued": true, "event_id": "evt_4fbc127e84a0331dfb743e10", "destination_id": "wd_4e1687f5d8638cbd" } ``` | Field | Type | What it is | | --- | --- | --- | | `queued` | the value true | | | `event_id` | string | The event’s id: GET /v1/events/{id} says what your server did with it. | | `destination_id` | string | A redelivery to a webhook destination: which one. | ### GET /v1/events/{id}/attempts **Every attempt for an event.** Each post made for one event at your webhook destinations: its number, automatic or manual, the status your server answered and when. destination_id narrows it to one. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events/evt_298da382d80136d9249cf920/attempts" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getEventsByIdAttempts("evt_298da382d80136d9249cf920"); ``` With the Python client ```python answer = pw.get_events_by_id_attempts("evt_298da382d80136d9249cf920") ``` The answer (200) ```json { "event_id": "evt_298da382d80136d9249cf920", "data": [ { "event_id": "evt_298da382d80136d9249cf920", "destination_id": "wd_4e1687f5d8638cbd", "number": 1, "type": "automatic", "delivered": false, "status": 503, "error": "The endpoint answered 503; only a 2xx counts as delivered.", "at": "2026-10-20T19:12:00.093Z" } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `event_id` | string | | | `data` | list of object | Each post: its headers carry Payerworks-Attempt-Number, Payerworks-Attempt-Type, Payerworks-Destination-Id. | ### GET /v1/webhook-destinations **List your webhook destinations.** Every destination your key posts events to, besides your account webhook: up to ten, each with its name, address, the event types it takes, whether it is enabled, how many deliveries it takes at once, and the last four characters of its secret. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getWebhookDestinations(); ``` With the Python client ```python answer = pw.get_webhook_destinations() ``` The answer (200) ```json { "data": [ { "id": "wd_4e1687f5d8638cbd", "name": "Primary", "url": "https://hooks.example.com/payerworks/primary", "event_types": [ "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "enabled": true, "concurrency": 2, "secret_last_four": "J+c=", "previous_secret_expires_at": null, "disabled_at": null, "disabled_reason": null, "failing_since": null, "notices": [], "created_at": "2026-10-20T19:12:00.036Z", "updated_at": "2026-10-20T19:12:00.036Z" } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `data` | list of WebhookDestination | At most 10. | ### POST /v1/webhook-destinations **Add a webhook destination.** A named https address with its own signing secret, shown once in this answer. event_types narrows what it receives (leave it out for every type), enabled turns it off and on, and concurrency (1 to 10, 2 by default) is how many deliveries it takes at once. Only events Payerworks makes are sent. A delivery that fails is tried again 5 minutes, 30 minutes, 2 hours, 8 hours, 24 hours and 48 hours later; after the seventh attempt fails, the destination is disabled. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/webhook-destinations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postWebhookDestinations(body); ``` With the Python client ```python answer = pw.post_webhook_destinations(body) ``` request.json ```json { "name": "Primary", "url": "https://hooks.example.com/payerworks/primary", "event_types": [ "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "concurrency": 2 } ``` The answer (201) ```json { "id": "wd_4e1687f5d8638cbd", "name": "Primary", "url": "https://hooks.example.com/payerworks/primary", "event_types": [ "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "enabled": true, "concurrency": 2, "secret_last_four": "J+c=", "previous_secret_expires_at": null, "disabled_at": null, "disabled_reason": null, "failing_since": null, "notices": [], "created_at": "2026-10-20T19:12:00.036Z", "updated_at": "2026-10-20T19:12:00.036Z", "secret": "whsec_Y9WgA88flTmkMq89BiZzDzjILhNojHvXhMpB0Td1J+c=" } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | wd_ and 16 hexadecimal characters. | | `name` | string | | | `url` | string | | | `event_types` | list of one of "ping", "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received", "enrollment.updated", "enrollment.live", "enrollment.rejected", "enrollment.canceled", "enrollment.ended", "enrollment.task.assigned", "enrollment.task.completed", "enrollment.task.deleted", "job.completed", "job.failed", "submission.file.acknowledged", "submission.claim.acknowledged", "submission.remittance.received" | Empty: every type. | | `enabled` | boolean | | | `concurrency` | integer | | | `secret_last_four` | string | | | `previous_secret_expires_at` | string or null | | | `disabled_at` | string or null | | | `disabled_reason` | string or null | retries_exhausted, when Payerworks disabled it. | | `failing_since` | string or null | | | `notices` | list of object | | | `created_at` | string | | | `updated_at` | string | | | `secret` | string | whsec_ and the secret: only in the answer that made it (the create, or rotate: true). | ### GET /v1/webhook-destinations/{id} **One webhook destination.** One destination, with when it started failing and the notices sent about it. Never its secret. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getWebhookDestinationsById("wd_4e1687f5d8638cbd"); ``` With the Python client ```python answer = pw.get_webhook_destinations_by_id("wd_4e1687f5d8638cbd") ``` The answer (200) ```json { "id": "wd_4e1687f5d8638cbd", "name": "Primary", "url": "https://hooks.example.com/payerworks/primary", "event_types": [ "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "enabled": true, "concurrency": 2, "secret_last_four": "J+c=", "previous_secret_expires_at": null, "disabled_at": null, "disabled_reason": null, "failing_since": null, "notices": [], "created_at": "2026-10-20T19:12:00.036Z", "updated_at": "2026-10-20T19:12:00.036Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | wd_ and 16 hexadecimal characters. | | `name` | string | | | `url` | string | | | `event_types` | list of one of "ping", "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received", "enrollment.updated", "enrollment.live", "enrollment.rejected", "enrollment.canceled", "enrollment.ended", "enrollment.task.assigned", "enrollment.task.completed", "enrollment.task.deleted", "job.completed", "job.failed", "submission.file.acknowledged", "submission.claim.acknowledged", "submission.remittance.received" | Empty: every type. | | `enabled` | boolean | | | `concurrency` | integer | | | `secret_last_four` | string | | | `previous_secret_expires_at` | string or null | | | `disabled_at` | string or null | | | `disabled_reason` | string or null | retries_exhausted, when Payerworks disabled it. | | `failing_since` | string or null | | | `notices` | list of object | | | `created_at` | string | | | `updated_at` | string | | | `secret` | string | whsec_ and the secret: only in the answer that made it (the create, or rotate: true). | ### PUT /v1/webhook-destinations/{id} **Change a webhook destination.** Change any of its fields, turn it on again after it was disabled, or send rotate: true for a new secret (shown once); the old one keeps signing beside it for overlap_hours, 0 to 720, 24 by default. Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putWebhookDestinationsById("wd_4e1687f5d8638cbd", body); ``` With the Python client ```python answer = pw.put_webhook_destinations_by_id("wd_4e1687f5d8638cbd", body) ``` request.json ```json { "rotate": true, "overlap_hours": 48 } ``` The answer (200) ```json { "id": "wd_4e1687f5d8638cbd", "name": "Primary", "url": "https://hooks.example.com/payerworks/primary", "event_types": [ "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "enabled": true, "concurrency": 2, "secret_last_four": "nHY=", "previous_secret_expires_at": "2026-10-22T19:12:00.046Z", "disabled_at": null, "disabled_reason": null, "failing_since": null, "notices": [], "created_at": "2026-10-20T19:12:00.036Z", "updated_at": "2026-10-20T19:12:00.046Z", "secret": "whsec_+3n+g2Y/+EkwVwm/9GiroC62uAVWtcbDgxSrcoK2nHY=" } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | wd_ and 16 hexadecimal characters. | | `name` | string | | | `url` | string | | | `event_types` | list of one of "ping", "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received", "enrollment.updated", "enrollment.live", "enrollment.rejected", "enrollment.canceled", "enrollment.ended", "enrollment.task.assigned", "enrollment.task.completed", "enrollment.task.deleted", "job.completed", "job.failed", "submission.file.acknowledged", "submission.claim.acknowledged", "submission.remittance.received" | Empty: every type. | | `enabled` | boolean | | | `concurrency` | integer | | | `secret_last_four` | string | | | `previous_secret_expires_at` | string or null | | | `disabled_at` | string or null | | | `disabled_reason` | string or null | retries_exhausted, when Payerworks disabled it. | | `failing_since` | string or null | | | `notices` | list of object | | | `created_at` | string | | | `updated_at` | string | | | `secret` | string | whsec_ and the secret: only in the answer that made it (the create, or rotate: true). | ### DELETE /v1/webhook-destinations/{id} **Remove a webhook destination.** No more events are sent to it, and its retries already scheduled are skipped. Removing one that is not there is not an error. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteWebhookDestinationsById("wd_4e1687f5d8638cbd"); ``` With the Python client ```python answer = pw.delete_webhook_destinations_by_id("wd_4e1687f5d8638cbd") ``` The answer (200) ```json { "id": "wd_4e1687f5d8638cbd", "deleted": true } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | | | `deleted` | the value true | | ### POST /v1/webhook-destinations/{id}/ping **Ping a webhook destination.** One signed ping event to this destination alone, whatever event types it takes. The answer is 202 with the event’s id. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd/ping" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postWebhookDestinationsByIdPing("wd_4e1687f5d8638cbd"); ``` With the Python client ```python answer = pw.post_webhook_destinations_by_id_ping("wd_4e1687f5d8638cbd") ``` The answer (202) ```json { "queued": true, "event_id": "evt_9b13ad1f89c0ff82ddbb2bf1", "destination_id": "wd_4e1687f5d8638cbd" } ``` | Field | Type | What it is | | --- | --- | --- | | `queued` | the value true | | | `event_id` | string | | | `destination_id` | string | | ### GET /v1/webhook-destinations/{id}/deliveries **What a destination received.** Each event’s delivery at this destination, newest first: pending, scheduled (failed, with the next automatic attempt’s time), delivered or failed, how many attempts, the last status and error. Filter with state, created_gte and created_lt; page with limit and starting_after. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd/deliveries" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getWebhookDestinationsByIdDeliveries("wd_4e1687f5d8638cbd"); ``` With the Python client ```python answer = pw.get_webhook_destinations_by_id_deliveries("wd_4e1687f5d8638cbd") ``` The answer (200) ```json { "data": [ { "event_id": "evt_298da382d80136d9249cf920", "event_type": "sandbox.remittance.received", "destination_id": "wd_4e1687f5d8638cbd", "created_at": "2026-10-20T19:12:00.057Z", "state": "scheduled", "attempts": 1, "next_attempt_at": "2026-10-20T19:17:00.093Z", "last_attempt_at": "2026-10-20T19:12:00.093Z", "last_status": 503, "last_error": "The endpoint answered 503; only a 2xx counts as delivered.", "delivered_at": null }, { "event_id": "evt_de844b60d20dade1dfdfdf52", "event_type": "sandbox.claim.acknowledged", "destination_id": "wd_4e1687f5d8638cbd", "created_at": "2026-10-20T19:12:00.057Z", "state": "scheduled", "attempts": 1, "next_attempt_at": "2026-10-20T19:17:00.093Z", "last_attempt_at": "2026-10-20T19:12:00.093Z", "last_status": 503, "last_error": "The endpoint answered 503; only a 2xx counts as delivered.", "delivered_at": null } ], "has_more": false, "next_cursor": null } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `data` | list of DestinationDelivery | Newest first. | | `has_more` | boolean | | | `next_cursor` | string or null | Pass as starting_after. | ## Reading the answer - Each delivery carries the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature) and Payerworks-Signature. Check either with the clients’ verifiers, over the raw body, before you parse it. - GET /v1/events lists newest first, filtered by type, by delivery state and by when it was made; GET /v1/events/{id}/attempts lists every attempt, automatic or by hand. ## Troubleshooting - `invalid_webhook_url` (400): A webhook address is https, at a public hostname, on port 443, with no login. - `event_limit_reached` (429): An account keeps at most its daily number of events; past it, nothing more is recorded until 00:00 UTC. - `not_at_destination` (404): That event was never sent to this destination: it was made before the destination was added, or its type is filtered out. - A signature that does not verify is almost always a body that was parsed and written again before the check. Check the raw bytes as they arrived. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_concurrency` | 400 | concurrency is a whole number from 1 to 10: how many deliveries this destination takes at once. | | `invalid_contact_email` | 400 | email is an address of at most 254 characters, or null to remove it. | | `invalid_created` | 400 | created_gte and created_lt are instants written like 2026-10-19T14:00:00Z, and created_gte comes before created_lt. | | `invalid_cursor` | 400 | starting_after is the next_cursor of an earlier page, or the id of one of this key's events. | | `invalid_delivery_state` | 400 | The delivery state filter names one of the states the developers page lists. | | `invalid_destination_id` | 400 | A destination id is wd_ followed by 16 lowercase hexadecimal characters. | | `invalid_destination_name` | 400 | A destination's name is 1 to 80 characters. | | `invalid_enabled` | 400 | enabled is true or false, or left out. | | `invalid_event_id` | 400 | An event id is evt_ followed by 16 to 64 lowercase hexadecimal characters. | | `invalid_event_type` | 400 | type is one of the event types the developers page lists. | | `invalid_event_types` | 400 | event_types is a list of the event types the developers page lists; an empty list, or leaving it out, receives every type. | | `invalid_expires_in_days` | 400 | expires_in_days is a whole number from 1 to 365, or left out for a key that does not expire. | | `invalid_key_id` | 400 | A key id is pwk_ followed by 16 to 40 letters and digits. | | `invalid_key_name` | 400 | A key's name is 1 to 80 characters. | | `invalid_limit` | 400 | limit is a whole number from 1 to 100. | | `invalid_overlap_hours` | 400 | overlap_hours is a whole number from 0 to 720: how long the old secret is still accepted after a rotation. | | `invalid_rotate` | 400 | rotate is true or false, or left out. | | `invalid_webhook_url` | 400 | A webhook address is https, at a public hostname, on port 443, with no login, at most 500 characters. | | `contact_not_proved` | 403 | Notices go only to an address Payerworks has proved: the one that asked for this account's test key by email and opened the link we sent. Send that address, or null to stop the notices. To use another, write to us from it. | | `destination_not_found` | 404 | This key has no webhook destination with that id. | | `event_not_found` | 404 | No event of this key has that id. Events are kept for 30 days. | | `key_not_found` | 404 | This account has no key with that id. | | `not_at_destination` | 404 | That event was never sent to this destination (it was made before the destination was added, or its type is filtered out). A ping or a new event will be. | | `destination_conflict` | 409 | The destination changed while this request ran, so nothing was saved. Send the request again. | | `destination_disabled` | 409 | This destination is disabled. Enable it with PUT /v1/webhook-destinations/{id} and `enabled: true` first. | | `destination_limit_reached` | 409 | A key holds at most 10 webhook destinations. Remove one to add another. | | `key_limit_reached` | 409 | An account holds at most 3 test keys or 25 live keys that are not revoked. Revoke one to make another. | | `key_revoked` | 409 | That key is revoked, so it cannot be rotated. Make a new key with POST /v1/keys. | | `key_rotation_conflict` | 409 | Another rotation of this key finished while this one ran, so this one made nothing: the secret that other request showed is the key's current one. List the keys with GET /v1/keys, and rotate again if that secret was lost. | | `key_settings_out_of_range` | 409 | The key's stored rate or account name is outside what a key may hold, so nothing was made or rotated; reason says which. Ask Payerworks to correct the key: sending the request again answers the same. | | `no_webhook` | 409 | This key has no webhook yet: set one with PUT /v1/account/webhook first. | | `webhook_conflict` | 409 | The webhook changed while this request ran, so nothing was saved. Send the request again. | | `too_many_events` | 413 | This run made more events than one delivery carries, so none were queued. Send fewer claims a call. | | `event_limit_reached` | 429 | This account's keys have recorded the 2,000 events an account keeps in a UTC day, so no more are recorded or sent this UTC day. Events are recorded again from 00:00 UTC; an event refused before then is never sent by itself. | | `webhook_rate_limited` | 429 | Over this key's webhook deliveries a minute, so nothing was queued. Wait for the next minute. | | `queue_unavailable` | 503 | We could not queue the events for delivery just now. Retry in a moment. | ## Code lists ### The event types | Type | When it is sent | | --- | --- | | `ping` | Sent when you ask for a test event, from the console or POST /v1/account/webhook/test. It carries no claim. | | `sandbox.file.acknowledged` | The 999 for your file: its verdict, how many transaction sets were received and accepted, and the control numbers of the interchange, the group and each set. | | `sandbox.claim.acknowledged` | One for each claim, from the 277CA: your reference, the payer claim number, accepted or rejected, and every fix as a sentence. | | `sandbox.remittance.received` | One for each check in the 835, once the payer adjudicated anything (an 835 pays each billing provider by its own check): the check number, its date and total, and each claim that check paid, with its reference, status and amount paid. | | `enrollment.updated` | An enrollment request changed: a status change no type below names, or its fields, tasks, documents or notes. It carries the status, the previous status, whose turn it is, the reason, the fields that changed and the sequence. | | `enrollment.live` | The payer confirmed the request: files of this kind may flow. It adds when, and the payer’s reference. | | `enrollment.rejected` | The request was refused. It adds the rejection: the payer’s words whole, and the remedy. | | `enrollment.canceled` | The request was canceled, or you asked to cancel one that is with the payer or live (cancel_requested says which). | | `enrollment.ended` | A live route ended: the payer ended it, or a cancel you asked for while it was live went through (in the sandbox, at the next advance). | | `enrollment.task.assigned` | A task was added: its id, rank, who holds it, its title and its fields. | | `enrollment.task.completed` | A task was finished, by the provider, or by Payerworks for its own. | | `enrollment.task.deleted` | A task was replaced by another (superseded_by names it). | | `job.completed` | A batch job finished, every call answered or, when it ran out of time, partly: its id, route, state and counts, and until when its result is kept. Never the result or a link to it: ask GET /v1/jobs/{id} for the link. | | `job.failed` | A batch job failed, or was stopped: its id, route, state, counts and the failure’s code. Nothing of the file you uploaded. | | `submission.file.acknowledged` | The partner’s 999 for a file you sent with POST /v1/claims/submit: accepted, rejected or partial, with each refused claim’s fix in words. | | `submission.claim.acknowledged` | The partner’s 277CA verdict on one claim you sent, with the payer’s claim number when it gives one, and each fix in words. | | `submission.remittance.received` | An 835 whose every claim you sent: the check, and each claim’s billed, paid and patient share. An 835 that also pays claims you did not send is held, and reaches no one. | ### A delivery’s state `pending`, `scheduled`, `delivered`, `failed` ### When a destination’s delivery is tried again After a failed attempt, a destination is tried again after 5 minutes, then 30 minutes, then 2 hours, then 8 hours, then 1 day, then 2 days: 7 attempts in all, then it is said failed, and you can send it again by hand. ## Example [events.mjs](https://payerworks.com/docs/examples/events.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. events.mjs ```js // Payerworks example: events, signed, listed and sent again. Docs: https://payerworks.com/docs/events // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node events.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError, verifyStandardWebhook } from './payerworks.mjs'; import { SENDER_ID, claimEndingIn, refusalOf } from './sample-data.mjs'; /** * On YOUR server, for each delivery: check the signature over the raw body, before you parse it. * `secret` is the whsec_ secret PUT /v1/account/webhook answered once; `headers` the request's headers. * A delivery may arrive more than once: skip a webhook-id you have handled. * @param {string} secret @param {Record} headers @param {string} rawBody */ export function readDelivery(secret, headers, rawBody, seen = new Set()) { if (!verifyStandardWebhook(secret, headers, rawBody)) return { ok: false, reason: 'signature' }; const id = headers['webhook-id']; if (seen.has(id)) return { ok: true, duplicate: true }; seen.add(id); return { ok: true, event: JSON.parse(rawBody) }; } /** @param {Payerworks} pw */ export async function run(pw) { // 1. Your webhook: an https address of yours. The signing secret is shown once: store it. const hook = await pw.putAccountWebhook({ url: 'https://hooks.example.com/payerworks' }); // 2. A sandbox run makes events: the 999, each claim's 277CA, and each check's 835. const sandbox = await pw.postSandboxClaims({ sender_id: SENDER_ID, claims: [claimEndingIn('')] }); // 3. Every event is kept for 30 days: list them, read one, and send one again. const listed = await pw.getEvents({ query: { limit: 10 } }); const one = await pw.getEventsById(listed.data[0].id); const again = await pw.postEventsByIdRedeliver(one.id); // 4. The failure path: an id this key never had. const refused = await refusalOf(() => pw.getEventsById(`evt_${'0'.repeat(24)}`), { PayerworksError }); return { secret_starts: hook.secret.slice(0, 6), made: sandbox.events.map((e) => e.type), listed: listed.data.length, redelivered: Boolean(again), refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "secret_starts": "whsec_", "made": [ "sandbox.file.acknowledged", "sandbox.claim.acknowledged", "sandbox.remittance.received" ], "listed": 3, "redelivered": true, "refused": { "status": 404, "code": "event_not_found", "request_id": "req_6" } } ``` # Keys and your account > How a call is authorized, what each answer carries, and the routes that manage your keys and your account. Page: https://payerworks.com/docs/keys · Markdown: https://payerworks.com/docs/keys.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Every call but GET /v1/health sends a key as `Authorization: Bearer pw_test_…` or `pw_live_…`. A test key is free from the developers page, for invented data only, and writes test files only; a live key needs a signed business associate agreement first. Your key is a bearer key: keep it on your server, never in browser code. Every answer carries `x-request-id`, and every refusal carries the same id as `request_id` inside the error. Quote it when you write to us. A POST, and PUT /v1/account/webhook, may carry an Idempotency-Key, unless its route says it takes none: a retry with the same key and request within 24 hours is answered as the first call was, by running it again, because Payerworks never keeps an answer. ## Routes - [GET /v1/health](#get-v1-health): Health - [GET /v1/account](#get-v1-account): Your account - [GET /v1/account/contact](#get-v1-account-contact): Your notice address - [PUT /v1/account/contact](#put-v1-account-contact): Set your notice address - [GET /v1/keys](#get-v1-keys): Your account’s keys - [POST /v1/keys](#post-v1-keys): Make a key - [DELETE /v1/keys/{id}](#delete-v1-keys-id): Revoke a key - [POST /v1/keys/{id}/rotate](#post-v1-keys-id-rotate): Rotate a key - [POST /v1/mcp](#post-v1-mcp): The MCP server, hosted - [GET /v1/audit-log](#get-v1-audit-log): Your account’s audit log - [GET /v1/audit-log/export](#get-v1-audit-log-export): Download the audit log Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### GET /v1/health **Health.** Whether the API is answering. No key needed. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/health" ``` With the JavaScript client ```js const answer = await pw.getHealth(); ``` With the Python client ```python answer = pw.get_health() ``` The answer (200) ```json { "ok": true, "service": "Payerworks", "version": "v1", "transmits": false } ``` | Field | Type | What it is | | --- | --- | --- | | `ok` | boolean | | | `service` | string | | | `version` | string | | | `transmits` | the value false | No route transmits to a payer yet. | ### GET /v1/account **Your account.** The key’s account, label, mode and rate, the calls it made on each day of this month (counted by UTC day) and the calls refused before they ran, and its webhook if one is set, with what its last delivery did. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/account" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getAccount(); ``` With the Python client ```python answer = pw.get_account() ``` The answer (200) ```json { "key_id": "pwk_0123456789abcdef", "account": "Sample Billing Co", "label": "Self-serve", "mode": "test", "rpm": 60, "usage": { "month": "2026-10", "days": [ { "date": "2026-10-18", "calls": 12, "refused": 0 }, { "date": "2026-10-19", "calls": 4, "refused": 1 } ], "total": 16, "refused": 1 }, "webhook": { "url": "https://hooks.example.com/payerworks/events", "secret_last_four": "Osk=", "created_at": "2026-10-19T15:00:00.000Z", "previous_secret_expires_at": null, "last_delivery": { "at": "2026-10-19T15:00:00.000Z", "trigger": "sandbox", "first_event_id": "evt_cc264f7536a9315a0c6fe09f", "events": 3, "delivered": 3, "failed": 0, "status": 200, "error": null } } } ``` | Field | Type | What it is | | --- | --- | --- | | `key_id` | string | | | `account` | string | | | `label` | string | | | `mode` | one of "test", "live" | | | `rpm` | integer | Calls a minute. | | `usage` | object | This month to date, by UTC day. | | `webhook` | object or null | | ### GET /v1/account/contact **Your notice address.** Where Payerworks emails you about this key: when one of its webhook destinations keeps failing, and when one of its enrollment requests moves without your doing it. A test key from this page starts with the address its link was sent to, the only address it may set. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/account/contact" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getAccountContact(); ``` With the Python client ```python answer = pw.get_account_contact() ``` The answer (200) ```json { "email": "dev@example.com", "updated_at": "2026-10-20T19:12:00.052Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `email` | string or null | | | `updated_at` | string or null | | ### PUT /v1/account/contact **Set your notice address.** The address told when a destination has failed for 2 hours, for 24 hours, and when it was disabled after its last attempt, and sent one email for every enrollment request that moved without your doing it inside the same 15 minutes. Send email: null to remove it, and nothing is emailed. Only the address that asked for this account’s test key by email (any key of the account may set it), or null; any other address is refused 403 contact_not_proved. A key issued by Payerworks has its address set by us: write to us. Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/account/contact" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putAccountContact(body); ``` With the Python client ```python answer = pw.put_account_contact(body) ``` request.json ```json { "email": "dev@example.com" } ``` The answer (200) ```json { "email": "dev@example.com", "updated_at": "2026-10-20T19:12:00.052Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `email` | string or null | | | `updated_at` | string or null | | ### GET /v1/keys **Your account’s keys.** Every key of your account in your key’s own mode (a test key lists the test keys, a live key the live ones): its name, mode, last four characters, when it was made, when it expires, when it was last rotated, until when its previous secret is accepted, whether it has expired, when it was revoked, and which one is the key asking, beside the account’s id. A revoked key is listed too. Never a key itself. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/keys" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getKeys(); ``` With the Python client ```python answer = pw.get_keys() ``` The answer (200) ```json { "account_id": "pwk_0123456789abcdef01", "data": [ { "key_id": "pwk_0123456789abcdef01", "name": "Server", "mode": "test", "last_four": "aaaa", "created_at": "2026-10-19T12:00:00.000Z", "expires_at": null, "expired": false, "revoked_at": null, "rotated_at": null, "previous_secret_accepted_until": null, "current": true }, { "key_id": "pwk_153493376889aa3239d7aab0", "name": "CI runner", "mode": "test", "last_four": "khYc", "created_at": "2026-10-20T19:12:00.100Z", "expires_at": "2027-01-18T19:12:00.100Z", "expired": false, "revoked_at": null, "rotated_at": null, "previous_secret_accepted_until": null, "current": false } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `account_id` | string | The account: the keys one key started. | | `data` | list of AccountKey | | ### POST /v1/keys **Make a key.** A named key of your key’s own mode and rate, shown once. expires_in_days (1 to 365) makes it stop on its own. A key never outlives the key that made it: its expiry is the sooner of expires_in_days and the making key’s own, and the making key’s when none is asked. An account holds 3 test keys or 25 live ones that are not revoked. Every key of an account may manage its keys, unless the account’s organization chose members only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/keys" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postKeys(body); ``` With the Python client ```python answer = pw.post_keys(body) ``` request.json ```json { "name": "CI runner", "expires_in_days": 90 } ``` The answer (201) ```json { "key_id": "pwk_153493376889aa3239d7aab0", "name": "CI runner", "mode": "test", "last_four": "khYc", "created_at": "2026-10-20T19:12:00.100Z", "expires_at": "2027-01-18T19:12:00.100Z", "expired": false, "revoked_at": null, "rotated_at": null, "previous_secret_accepted_until": null, "current": false, "key": "pw_test_pBP8..." } ``` | Field | Type | What it is | | --- | --- | --- | | `key_id` | string | | | `name` | string | | | `mode` | one of "test", "live" | | | `last_four` | string | | | `created_at` | string | | | `expires_at` | string or null | | | `expired` | boolean | | | `revoked_at` | string or null | | | `rotated_at` | string or null | | | `previous_secret_accepted_until` | string or null | | | `current` | boolean | True for the key this call was made with. | | `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). | | `overlap_hours` | integer | | ### DELETE /v1/keys/{id} **Revoke a key.** The key stops working within a minute, and its SFTP user, if it has one, is removed at once. Revoking a key that is already revoked is not an error. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/keys/pwk_153493376889aa3239d7aab0" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteKeysById("pwk_153493376889aa3239d7aab0"); ``` With the Python client ```python answer = pw.delete_keys_by_id("pwk_153493376889aa3239d7aab0") ``` The answer (200) ```json { "key_id": "pwk_153493376889aa3239d7aab0", "name": "CI runner", "mode": "test", "last_four": "6NXM", "created_at": "2026-10-20T19:12:00.100Z", "expires_at": "2027-01-18T19:12:00.100Z", "expired": false, "revoked_at": "2026-10-20T19:12:00.111Z", "rotated_at": "2026-10-20T19:12:00.108Z", "previous_secret_accepted_until": "2026-10-21T19:12:00.108Z", "current": false } ``` | Field | Type | What it is | | --- | --- | --- | | `key_id` | string | | | `name` | string | | | `mode` | one of "test", "live" | | | `last_four` | string | | | `created_at` | string | | | `expires_at` | string or null | | | `expired` | boolean | | | `revoked_at` | string or null | | | `rotated_at` | string or null | | | `previous_secret_accepted_until` | string or null | | | `current` | boolean | True for the key this call was made with. | | `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). | | `overlap_hours` | integer | | ### POST /v1/keys/{id}/rotate **Rotate a key.** A new secret for the same key id, shown once, so its webhooks, events, usage and enrollments carry on. The old secret is still accepted for overlap_hours, 0 to 720, 24 by default, and stops at once if the key is revoked. Two rotations at once: the first finishes, and the other answers 409 key_rotation_conflict and makes nothing. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/keys/pwk_153493376889aa3239d7aab0/rotate" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postKeysByIdRotate("pwk_153493376889aa3239d7aab0", body); ``` With the Python client ```python answer = pw.post_keys_by_id_rotate("pwk_153493376889aa3239d7aab0", body) ``` request.json ```json { "overlap_hours": 24 } ``` The answer (200) ```json { "key_id": "pwk_153493376889aa3239d7aab0", "name": "CI runner", "mode": "test", "last_four": "6NXM", "created_at": "2026-10-20T19:12:00.100Z", "expires_at": "2027-01-18T19:12:00.100Z", "expired": false, "revoked_at": null, "rotated_at": "2026-10-20T19:12:00.108Z", "previous_secret_accepted_until": "2026-10-21T19:12:00.108Z", "current": false, "key": "pw_test_OOHH...", "overlap_hours": 24 } ``` | Field | Type | What it is | | --- | --- | --- | | `key_id` | string | | | `name` | string | | | `mode` | one of "test", "live" | | | `last_four` | string | | | `created_at` | string | | | `expires_at` | string or null | | | `expired` | boolean | | | `revoked_at` | string or null | | | `rotated_at` | string or null | | | `previous_secret_accepted_until` | string or null | | | `current` | boolean | True for the key this call was made with. | | `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). | | `overlap_hours` | integer | | ### POST /v1/mcp **The MCP server, hosted.** The same tools as the downloadable MCP server, over MCP’s Streamable HTTP transport (2026-07-28, and the 2025 handshakes), with your key as the bearer. Each tool call is a call to its route under your key. With a live key no tool is called unless the request carries Payerworks-MCP-Allow-Live-Key: 1. Payerworks transmits nothing. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/mcp" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postMcp(body); ``` With the Python client ```python answer = pw.post_mcp(body) ``` request.json ```json { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "example-client", "version": "1" } } } ``` The answer (200) ```json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": false }, "prompts": { "listChanged": false } }, "serverInfo": { "name": "payerworks", "title": "Payerworks", "version": "1.0.0" }, "instructions": "Each tool calls one route of the Payerworks API with the key this connection presents. Payerworks validates claims, writes and reads X12 files and explains each answer in plain words. It transmits nothing: no file reaches a payer. post_sand…" } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `jsonrpc` | the value "2.0" | | | `id` | object | | | `result` | object | | | `error` | object | A JSON-RPC error: never the API’s error shape. | ### GET /v1/audit-log **Your account’s audit log.** Every change to this account, newest first: keys made, rotated, revoked or expired; the webhook, destinations and the notice address; providers, enrollment requests, tasks and documents; the SFTP user and its SSH keys; batch jobs; the organization and its members. Each entry says what, when, who (a key, a member, Payerworks, or Payerworks automatically), through which door, the object’s id, and the fields that changed by name only, never a value; a web address by its host. limit 1 to 100 (25), type (an entry type, or its family before the dot), created_gte and created_lt, starting_after. Each entry is kept 365 days, then deleted, and never changed before then. Every key of the account reads the same log. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/audit-log" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getAuditLog(); ``` With the Python client ```python answer = pw.get_audit_log() ``` The answer (200) ```json { "object": "list", "data": [ { "id": "aud_aeb4acb2fdf610d4bc531306", "type": "webhook_destination.removed", "words": "Webhook destination removed", "at": "2026-10-20T19:12:00.114Z", "actor": { "type": "key", "id": "pwk_0123456789abcdef01" }, "door": "api", "object": { "kind": "webhook_destination", "id": "wd_4e1687f5d8638cbd" }, "changed": [], "url_host": null, "count": null, "request_id": "req_example_15" }, { "id": "aud_a08c0b0cd9c2ce25b9ef59ab", "type": "key.revoked", "words": "API key revoked", "at": "2026-10-20T19:12:00.111Z", "actor": { "type": "key", "id": "pwk_0123456789abcdef01" }, "door": "api", "object": { "kind": "key", "id": "pwk_153493376889aa3239d7aab0" }, "changed": [], "url_host": null, "count": null, "request_id": "req_example_14" } ], "has_more": true, "next_cursor": "acur_412331373932353233353230313038236175645f663663623031613433613161613366356130353463653832", "retention_days": 365 } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "list" | | | `data` | list of AuditEntry | Newest first. | | `has_more` | boolean | | | `next_cursor` | string or null | Pass as starting_after. | | `retention_days` | the value 365 | Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is. | ### GET /v1/audit-log/export **Download the audit log.** The same entries and filters as a CSV file in the answer (csv, filename, columns), at most 5,000 entries a call; next_cursor names where the rest start. No cell opens as a formula in a spreadsheet. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/audit-log/export" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getAuditLogExport(); ``` With the Python client ```python answer = pw.get_audit_log_export() ``` The answer (200) ```json { "object": "audit_log_export", "content_type": "text/csv", "filename": "payerworks-audit-log-2026-10-20.csv", "row_count": 7, "columns": [ "id", "at" ], "csv": "id,at,type,words,actor_type,actor_id,door,object_kind,object_id,changed,url_host,count,request_id\r\naud_aeb4acb2fdf610d4bc531306,2026-10-20T19:12:00.114Z,webhook_destination.removed,Webhook destination removed,key,pwk_0123456789abcdef01,api,…", "has_more": false, "next_cursor": null, "retention_days": 365 } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `object` | the value "audit_log_export" | | | `content_type` | the value "text/csv" | | | `filename` | string | | | `row_count` | integer | At most 5,000. | | `columns` | list of one of "id", "at", "type", "words", "actor_type", "actor_id", "door", "object_kind", "object_id", "changed", "url_host", "count", "request_id" | | | `csv` | string | The file, CRLF line ends; no cell opens as a formula in a spreadsheet. | | `has_more` | boolean | | | `next_cursor` | string or null | Pass as starting_after for the rest. | | `retention_days` | the value 365 | Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is. | ## Reading the answer - GET /v1/account answers the key’s rate a minute and its usage this month to date, by UTC day, with refused calls counted apart. - A rotation (POST /v1/keys/{id}/rotate) keeps the key id, so webhooks, events, usage and enrollments carry on; the old secret is accepted only inside the overlap you choose. Every key of an account may manage its keys, unless the account’s organization chose that only its signed-in members do (see [Organizations and members](https://payerworks.com/docs/organizations)). - GET /v1/audit-log answers who changed what on the account, when and through which door, by field name only, never a value; each entry is kept one year. GET /v1/audit-log/export gives the same entries as a CSV file. ## Troubleshooting - `invalid_key` (401): The key is not valid, or was revoked: a revoked key stops within a minute. - `rate_limited` (429): Over the key’s calls a minute. Wait the seconds retry-after names; the clients do this for you, at most twice. - `idempotency_key_reused` (422): That Idempotency-Key was used in the last 24 hours for a different request. Make a new key for each new request. - `too_large` (413): A request body is at most 2 MB. Larger work is split across calls. - `invalid_audit_type` (400): type names an entry type (key.created) or its family (key). - `keys_managed_by_members` (403): The account’s organization chose that only its signed-in members make, rotate and revoke keys: do it in the console, or ask an owner to switch it back. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_audit_cursor` | 400 | starting_after is the next_cursor of an earlier page of this audit log. | | `invalid_audit_type` | 400 | type is one of the audit log's entry types, or the part before its dot (key, webhook, enrollment...), as the developers page lists them. | | `invalid_concurrency` | 400 | concurrency is a whole number from 1 to 10: how many deliveries this destination takes at once. | | `invalid_contact_email` | 400 | email is an address of at most 254 characters, or null to remove it. | | `invalid_destination_id` | 400 | A destination id is wd_ followed by 16 lowercase hexadecimal characters. | | `invalid_destination_name` | 400 | A destination's name is 1 to 80 characters. | | `invalid_enabled` | 400 | enabled is true or false, or left out. | | `invalid_event_types` | 400 | event_types is a list of the event types the developers page lists; an empty list, or leaving it out, receives every type. | | `invalid_expires_in_days` | 400 | expires_in_days is a whole number from 1 to 365, or left out for a key that does not expire. | | `invalid_idempotency_key` | 400 | An Idempotency-Key is 1 to 255 visible characters, with no spaces, quotes or backslashes. A UUID is a good one. | | `invalid_json` | 400 | The body must be a JSON object. | | `invalid_key_id` | 400 | A key id is pwk_ followed by 16 to 40 letters and digits. | | `invalid_key_name` | 400 | A key's name is 1 to 80 characters. | | `invalid_overlap_hours` | 400 | overlap_hours is a whole number from 0 to 720: how long the old secret is still accepted after a rotation. | | `invalid_key` | 401 | That key is not valid. Keys are issued by Payerworks; a revoked key stops within a minute. | | `no_key` | 401 | Send your API key as `Authorization: Bearer `. | | `contact_not_proved` | 403 | Notices go only to an address Payerworks has proved: the one that asked for this account's test key by email and opened the link we sent. Send that address, or null to stop the notices. To use another, write to us from it. | | `keys_managed_by_members` | 403 | This account's organization chose that only its signed-in members manage keys. Make, rotate or revoke the key at payerworks.com/console. | | `destination_not_found` | 404 | This key has no webhook destination with that id. | | `key_not_found` | 404 | This account has no key with that id. | | `not_at_destination` | 404 | That event was never sent to this destination (it was made before the destination was added, or its type is filtered out). A ping or a new event will be. | | `destination_conflict` | 409 | The destination changed while this request ran, so nothing was saved. Send the request again. | | `destination_disabled` | 409 | This destination is disabled. Enable it with PUT /v1/webhook-destinations/{id} and `enabled: true` first. | | `destination_limit_reached` | 409 | A key holds at most 10 webhook destinations. Remove one to add another. | | `idempotency_in_flight` | 409 | The first request with this Idempotency-Key is still running. Retry after the seconds retry-after names. | | `key_limit_reached` | 409 | An account holds at most 3 test keys or 25 live keys that are not revoked. Revoke one to make another. | | `key_revoked` | 409 | That key is revoked, so it cannot be rotated. Make a new key with POST /v1/keys. | | `key_rotation_conflict` | 409 | Another rotation of this key finished while this one ran, so this one made nothing: the secret that other request showed is the key's current one. List the keys with GET /v1/keys, and rotate again if that secret was lost. | | `key_settings_out_of_range` | 409 | The key's stored rate or account name is outside what a key may hold, so nothing was made or rotated; reason says which. Ask Payerworks to correct the key: sending the request again answers the same. | | `too_large` | 413 | A request body may be at most 2 MB. | | `idempotency_key_reused` | 422 | This Idempotency-Key was used in the last 24 hours for a different request. Send a new key for a new request. | | `rate_limited` | 429 | Over this key's calls a minute. Wait the seconds retry-after names. | | `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. | ## Code lists ### The headers on every answer | Header | What it says | | --- | --- | | `x-request-id` | The request’s id. Quote it when you write to Payerworks. | | `x-ratelimit-limit` | The key’s calls a minute. | | `x-ratelimit-remaining` | Calls left in this UTC minute. | | `x-ratelimit-reset` | Seconds until the window resets. | | `RateLimit` | The same figures in the IETF draft’s form. | | `RateLimit-Policy` | The key’s policy in the IETF draft’s form. | | `idempotent-replayed` | true when this is the first answer to an Idempotency-Key request, given again. | | `retry-after` | Seconds to wait before a retry. | ### The audit log’s entry types An entry names what changed by field name only, never a value: no name, address, note or answer is kept. A web address is kept as its host alone. Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is. | Type | What changed | Fields it may name | | --- | --- | --- | | `key.created` | API key created | `name`, `mode`, `rpm`, `expires_at` | | `key.rotated` | API key secret rotated | `secret`, `overlap_hours` | | `key.revoked` | API key revoked | none | | `key.expired` | API key expired | none | | `webhook.set` | Account webhook set | `url`, `secret` | | `webhook.changed` | Account webhook changed | `url`, `secret`, `overlap_hours` | | `webhook.removed` | Account webhook removed | none | | `webhook_destination.created` | Webhook destination added | `name`, `url`, `event_types`, `enabled`, `concurrency`, `secret` | | `webhook_destination.changed` | Webhook destination changed | `name`, `url`, `event_types`, `enabled`, `concurrency`, `secret`, `overlap_hours` | | `webhook_destination.removed` | Webhook destination removed | none | | `contact.changed` | Notice contact set | `email` | | `contact.removed` | Notice contact removed | `email` | | `provider.created` | Provider added | `name`, `npi`, `tax_id`, `tax_id_type`, `contacts` | | `provider.changed` | Provider changed | `name`, `npi`, `tax_id`, `tax_id_type`, `contacts` | | `provider.removed` | Provider removed | none | | `enrollment.created` | Enrollment request created | `provider_id`, `payer`, `transaction`, `primary_contact`, `notify_email`, `requested_effective_date`, `era_aggregation`, `ptan`, `note` | | `enrollment.changed` | Enrollment request changed | `provider_id`, `payer`, `transaction`, `primary_contact`, `notify_email`, `requested_effective_date`, `era_aggregation`, `ptan`, `note` | | `enrollment.submitted` | Enrollment request submitted | none | | `enrollment.withdrawn` | Enrollment request withdrawn | `reason` | | `enrollment.withdrawal_requested` | Withdrawal of an enrollment request asked of Payerworks | `reason` | | `enrollment.removed` | Enrollment request deleted | none | | `enrollment.status_changed` | Enrollment request status changed | `status` | | `enrollment.imported` | Enrollment requests imported from a file | none | | `enrollment.note_added` | Note added to an enrollment request | none | | `enrollment_task.answered` | Enrollment task answered | `values` | | `enrollment_task.added` | Enrollment task added | none | | `enrollment_task.superseded` | Enrollment task replaced by another | none | | `enrollment_task.completed` | Enrollment task marked done | none | | `enrollment_document.requested` | Enrollment document upload started | none | | `enrollment_document.added` | Enrollment document added | none | | `enrollment_document.removed` | Enrollment document removed | none | | `sftp_user.added` | SFTP user added | `acknowledgment_999` | | `sftp_user.changed` | SFTP user changed | `acknowledgment_999` | | `sftp_user.removed` | SFTP user removed | none | | `sftp_ssh_key.added` | SFTP SSH key added | none | | `sftp_ssh_key.removed` | SFTP SSH key removed | none | | `job.created` | Batch job created | `route` | | `job.canceled` | Batch job canceled | none | | `job.removed` | Batch job and its files deleted | none | | `account_baa.recorded` | Business associate agreement recorded | `signer_name`, `signer_title`, `organization`, `signed_on`, `baa_version`, `reference_prefix` | | `account_baa.withdrawn` | Business associate agreement withdrawn | none | | `org.created` | Organization created | `name` | | `org.updated` | Organization changed | `name`, `key_management` | | `member.invited` | Member invited | `email`, `role` | | `member.invitation_canceled` | Member invitation canceled | none | | `member.joined` | Member joined | none | | `member.role_changed` | Member role changed | `role` | | `member.removed` | Member removed | `keys_revoked` | ## Example [keys.mjs](https://payerworks.com/docs/examples/keys.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. keys.mjs ```js // Payerworks example: keys and your account. Docs: https://payerworks.com/docs/keys // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node keys.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. Health needs no key, and says the API transmits nothing. const health = await pw.getHealth(); // 2. Your key's rate a minute, and its calls this month to date by UTC day. const account = await pw.getAccount(); // 3. A named key for one job, shown once, then revoked. Every key of an account may manage its keys, // unless the account's organization chose that only its signed-in members do. const made = await pw.postKeys({ name: 'Example job', expires_in_days: 1 }); const revoked = await pw.deleteKeysById(made.key_id); // 4. The account's audit log: who made and revoked that key, when, and through which door, newest first. const log = await pw.getAuditLog({ query: { limit: 2 } }); // 5. The failure path: a key id this account never had. const refused = await refusalOf(() => pw.deleteKeysById('pwk_0000000000000000'), { PayerworksError }); return { transmits: health.transmits, mode: account.mode, rpm: account.rpm, made: { key_id: made.key_id.startsWith('pwk_'), shown_once: typeof made.key === 'string' }, revoked: Boolean(revoked), audit: log.data.map((e) => ({ type: e.type, by: e.actor.type, door: e.door })), refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "transmits": false, "mode": "test", "rpm": 600, "made": { "key_id": true, "shown_once": true }, "revoked": true, "audit": [ { "type": "key.revoked", "by": "key", "door": "api" }, { "type": "key.created", "by": "key", "door": "api" } ], "refused": { "status": 404, "code": "key_not_found", "request_id": "req_6" } } ``` # The SFTP file drop > For a system that sends files rather than calls: an SFTP user under your own key, files dropped in one folder and answers read from another. Page: https://payerworks.com/docs/sftp · Markdown: https://payerworks.com/docs/sftp.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. For a system that drops files rather than calling an API. Make an SFTP user under your key, add your SSH public key (there are no passwords), and drop 837P files in `/inbound`: the answers are written to `/outbound`. The drop is open only where Payerworks has turned it on; elsewhere every route answers `sftp_not_open` and every other API route still answers. ## Routes - [GET /v1/sftp](#get-v1-sftp): Your SFTP user - [PUT /v1/sftp](#put-v1-sftp): Make your SFTP user - [DELETE /v1/sftp](#delete-v1-sftp): Remove your SFTP user - [POST /v1/sftp/ssh-keys](#post-v1-sftp-ssh-keys): Add an SSH key - [DELETE /v1/sftp/ssh-keys/{id}](#delete-v1-sftp-ssh-keys-id): Remove an SSH key Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### GET /v1/sftp **Your SFTP user.** The host, your user name, your two folders (/inbound and /outbound), your SSH keys by fingerprint, when a 999 is written, and what is kept and for how long. A key with no user answers 404 sftp_user_not_found. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/sftp" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getSftp(); ``` With the Python client ```python answer = pw.get_sftp() ``` The answer (200) ```json { "user_name": "pwc-pwk_0123456789abcdef01", "host": "s-0123456789abcdef0.server.transfer.us-east-1.amazonaws.com", "port": 22, "mode": "test", "folders": { "inbound": "/inbound", "outbound": "/outbound" }, "receiver": { "qualifier": "ZZ", "id": "PAYERWORKSTEST", "usage_indicator": "T" }, "acknowledgment_999": "always", "reads": [ "837P", "837I" ], "max_file_bytes": 2000000, "ssh_keys": [ { "id": "key-0a1b2c3d4e5f6a7b1", "algorithm": "ssh-ed25519", "fingerprint": "SHA256:EN9Jp9JJbIabR3Qcsj0tqYdUVi1NxOEfJBNgjLzYedc", "added_at": "2026-10-19T15:00:00.000Z" } ], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "retention": [ "A file you drop in inbound is read once and deleted as soon as its answers are written. A file the lane could not reach is deleted by the bucket 1 day after it arrived; S3 removes an expired file in the day or so after its expiry.", "Each answer (a TA1, a 999 and the report) waits in outbound for 7 days after it was written, then the bucket deletes it; you may delete it sooner. S3 removes an expired file in the day or so after its expiry." ], "transmits": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `user_name` | string | | | `host` | string | | | `port` | integer | | | `mode` | one of "test", "live" | | | `folders` | object | | | `receiver` | object | What a dropped file’s ISA07, ISA08 and ISA15 must say. | | `acknowledgment_999` | one of "always", "errors_only", "never" | | | `reads` | list of string | | | `max_file_bytes` | integer | | | `ssh_keys` | list of SshKey | At most 10. | | `created_at` | string | | | `updated_at` | string | | | `retention` | list of string | What is kept, and for how long. | | `transmits` | the value false | | ### PUT /v1/sftp **Make your SFTP user.** Makes this key’s SFTP user, or changes when a 999 is written for a file you drop: acknowledgment_999 is always, errors_only (the default: only when a group or a set is not plainly accepted) or never. A test key’s user sends usage indicator T to receiver PAYERWORKSTEST; a live key’s sends P to PAYERWORKS (qualifier ZZ). Payerworks validates and acknowledges what you drop; nothing goes on to a payer. Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/sftp" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putSftp(body); ``` With the Python client ```python answer = pw.put_sftp(body) ``` request.json ```json { "acknowledgment_999": "always" } ``` The answer (200) ```json { "user_name": "pwc-pwk_0123456789abcdef01", "host": "s-0123456789abcdef0.server.transfer.us-east-1.amazonaws.com", "port": 22, "mode": "test", "folders": { "inbound": "/inbound", "outbound": "/outbound" }, "receiver": { "qualifier": "ZZ", "id": "PAYERWORKSTEST", "usage_indicator": "T" }, "acknowledgment_999": "always", "reads": [ "837P", "837I" ], "max_file_bytes": 2000000, "ssh_keys": [], "created_at": "2026-10-19T15:00:00.000Z", "updated_at": "2026-10-19T15:00:00.000Z", "retention": [ "A file you drop in inbound is read once and deleted as soon as its answers are written. A file the lane could not reach is deleted by the bucket 1 day after it arrived; S3 removes an expired file in the day or so after its expiry.", "Each answer (a TA1, a 999 and the report) waits in outbound for 7 days after it was written, then the bucket deletes it; you may delete it sooner. S3 removes an expired file in the day or so after its expiry." ], "transmits": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `user_name` | string | | | `host` | string | | | `port` | integer | | | `mode` | one of "test", "live" | | | `folders` | object | | | `receiver` | object | What a dropped file’s ISA07, ISA08 and ISA15 must say. | | `acknowledgment_999` | one of "always", "errors_only", "never" | | | `reads` | list of string | | | `max_file_bytes` | integer | | | `ssh_keys` | list of SshKey | At most 10. | | `created_at` | string | | | `updated_at` | string | | | `retention` | list of string | What is kept, and for how long. | | `transmits` | the value false | | ### DELETE /v1/sftp **Remove your SFTP user.** The user can no longer sign in. Answers already in /outbound are deleted by the bucket 7 days after each was written. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/sftp" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteSftp(); ``` With the Python client ```python answer = pw.delete_sftp() ``` The answer (200) ```json { "deleted": true, "note": "The user can no longer sign in. Answers already in outbound are deleted by the bucket 7 days after each was written.", "retention": [ "A file you drop in inbound is read once and deleted as soon as its answers are written. A file the lane could not reach is deleted by the bucket 1 day after it arrived; S3 removes an expired file in the day or so after its expiry.", "Each answer (a TA1, a 999 and the report) waits in outbound for 7 days after it was written, then the bucket deletes it; you may delete it sooner. S3 removes an expired file in the day or so after its expiry." ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | | `note` | string | | | `retention` | list of string | | ### POST /v1/sftp/ssh-keys **Add an SSH key.** One OpenSSH public key line (ssh-ed25519, ssh-rsa or ecdsa), as your .pub file holds it; at most 10. There are no passwords. A private key is refused and never kept; a key’s comment is dropped. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sftp/ssh-keys" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postSftpSshKeys(body); ``` With the Python client ```python answer = pw.post_sftp_ssh_keys(body) ``` request.json ```json { "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINXvOd5VaJqJWXfe+7VV3zPAkJmYjT4WyPx9TO5a9G+H dev@example.com" } ``` The answer (200) ```json { "id": "key-0a1b2c3d4e5f6a7b1", "algorithm": "ssh-ed25519", "fingerprint": "SHA256:EN9Jp9JJbIabR3Qcsj0tqYdUVi1NxOEfJBNgjLzYedc", "added_at": "2026-10-19T15:00:00.000Z" } ``` | Field | Type | What it is | | --- | --- | --- | | `id` | string | key- and 17 lowercase hexadecimal characters. | | `algorithm` | string | | | `fingerprint` | string | SHA256:, as ssh-keygen -lf shows it. | | `added_at` | string | | ### DELETE /v1/sftp/ssh-keys/{id} **Remove an SSH key.** That key can no longer sign in. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/sftp/ssh-keys/key-0a1b2c3d4e5f6a7b1" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteSftpSshKeysById("key-0a1b2c3d4e5f6a7b1"); ``` With the Python client ```python answer = pw.delete_sftp_ssh_keys_by_id("key-0a1b2c3d4e5f6a7b1") ``` The answer (200) ```json { "deleted": true, "id": "key-0a1b2c3d4e5f6a7b1" } ``` | Field | Type | What it is | | --- | --- | --- | | `deleted` | the value true | | | `id` | string | | ## Reading the answer - GET /v1/sftp answers the host and port to connect to, your user name, the two folders, the kinds of file read, your SSH keys and how long files are kept (`retention`). ## Troubleshooting - `sftp_not_open` (403): The file drop is not open on this deployment. The API routes are the same work, call by call. - `private_key_sent` (400): That was a private key. Nothing was kept: send the .pub file, and keep the private key to yourself. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_acknowledgment_999` | 400 | acknowledgment_999 is "always", "errors_only", "never": when a 999 is written for a file you drop. | | `invalid_ssh_key_id` | 400 | An SSH key id is key- followed by 17 lowercase hexadecimal characters, as GET /v1/sftp lists it. | | `invalid_ssh_public_key` | 400 | public_key is one OpenSSH public key line (ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521), as the .pub file holds it. | | `private_key_sent` | 400 | That is a PRIVATE key. Nothing was kept: send the matching public key (the .pub file), and keep the private key on your own machine. | | `unknown_sftp_field` | 400 | PUT /v1/sftp takes acknowledgment_999 and nothing else. An SFTP user signs in with an SSH key only: add one with POST /v1/sftp/ssh-keys. | | `sftp_not_open` | 403 | The SFTP file drop is not open on this Payerworks deployment. Every API route still answers. | | `sftp_user_not_found` | 404 | This key has no SFTP user. Make one with PUT /v1/sftp. | | `ssh_key_not_found` | 404 | This SFTP user holds no SSH key with that id. | | `ssh_key_exists` | 409 | This SFTP user already holds that SSH key. | | `ssh_key_limit_reached` | 409 | An SFTP user holds at most 10 SSH keys. Remove one to add another. | | `ssh_key_refused` | 422 | The SFTP endpoint refused that SSH key, so nothing was added. Send another public key; an ssh-ed25519 key is the simplest. | | `sftp_unavailable` | 503 | The SFTP user could not be read or changed just now, so nothing changed. Send the request again. | ## Code lists ### How long files are kept - A file you drop in inbound is read once and deleted as soon as its answers are written. A file the lane could not reach is deleted by the bucket 1 day after it arrived; S3 removes an expired file in the day or so after its expiry. - Each answer (a TA1, a 999 and the report) waits in outbound for 7 days after it was written, then the bucket deletes it; you may delete it sooner. S3 removes an expired file in the day or so after its expiry. - The bucket keeps no earlier version of a file and is in no backup. Nothing you drop is stored anywhere else, logged or sent on: the lane's own log line names the key, the kind of file and the outcome, never its contents or its name. - The endpoint's transfer log in CloudWatch records the name of each file you move, so never put a patient's name or any identifier in a file name. - Nothing is sent to a payer: Payerworks validates and acknowledges the file, and you send the claims on yourself. - When your key is revoked, its SFTP user is removed at once; when it expires, or when the endpoint could not be reached at the revoke, by the next daily pass, within a day. A file dropped under a revoked or expired key before then is deleted unread, with no answer. ## Example [sftp.mjs](https://payerworks.com/docs/examples/sftp.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. sftp.mjs ```js // Payerworks example: the SFTP file drop. Docs: https://payerworks.com/docs/sftp // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node sftp.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // The drop is open only where Payerworks has turned it on. Where it is not, every SFTP route // answers 403 sftp_not_open, and every other route is the same work, call by call. const closed = await refusalOf(() => pw.getSftp(), { PayerworksError }); if (closed?.code === 'sftp_not_open') return { open: false, refused: closed }; if (closed?.code === 'sftp_user_not_found') { // Open, and no user yet: make one. Add your SSH public key with POST /v1/sftp/ssh-keys (there are no passwords). const user = await pw.putSftp({ acknowledgment_999: 'errors_only' }); return { open: true, host: user.host, inbound: user.folders.inbound, outbound: user.folders.outbound }; } if (closed) return { open: null, refused: closed }; const user = await pw.getSftp(); return { open: true, host: user.host, inbound: user.folders.inbound, outbound: user.folders.outbound }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "open": false, "refused": { "status": 403, "code": "sftp_not_open", "request_id": "req_1" } } ``` # Batch jobs > For more than one call can carry: many calls to one route from one file you upload, answered off the API’s clock, the result kept 24 hours. Page: https://payerworks.com/docs/jobs · Markdown: https://payerworks.com/docs/jobs.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. For more than one call can carry. Make a job naming the one route it runs, upload one file of calls to the signed form it answers, and read the job until it finishes: each call is answered as that route answers it, one after another, off the API’s ten-second clock, and the result is a file you fetch by a signed link. A job runs claim validation, 837P writing, the remittance, eligibility and claim status explanations, analytics, payment variance and claim tracking; POST /v1/eligibility/270 as a job writes one 270 of up to 7,500 questions. A job is the one part of the API that keeps what you send for a while: your file until the job finishes, then it is deleted, and the result for 24 hours after, in storage of its own that keeps no earlier copy and is in no backup. Batch jobs take test keys only, for invented data, and are off until Payerworks turns them on: until then every jobs route answers 501 jobs_not_offered. ## Routes - [POST /v1/jobs](#post-v1-jobs): Make a batch job - [GET /v1/jobs](#get-v1-jobs): Your batch jobs - [GET /v1/jobs/{id}](#get-v1-jobs-id): One batch job - [POST /v1/jobs/{id}/cancel](#post-v1-jobs-id-cancel): Cancel a batch job - [DELETE /v1/jobs/{id}](#delete-v1-jobs-id): Delete a batch job Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### POST /v1/jobs **Make a batch job.** For more than one call can carry: name the route (route, as POST /v1/claims/validate) and, if every call carries one, the query. You get the job and a signed upload form for its file: { calls: [ ... ] }, each entry exactly the body that route takes in one call, up to 1,000 calls and 50 MB, uploaded within an hour. Each call is answered as the route answers it and held to its limits (2 MB a call; a larger one is answered 413 too_large at its index). A job runs claim validation, 837P writing, remittance, eligibility and claim status explanations, analytics, payment variance and claim tracking. POST /v1/eligibility/270 as a job is different: its file is that route’s own body with up to 7,500 requests, written as one 270. A file whose requests would write more segments than one 270 is read in fails the job job_270_too_many_segments. A job keeps your file until it finishes and its result for 24 hours, which no other route does: use invented data with a test key. Off until Payerworks turns it on; test keys only for now; 50 jobs a UTC day for an account’s keys together. Nothing is sent to a payer. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/jobs" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postJobs(body); ``` With the Python client ```python answer = pw.post_jobs(body) ``` request.json ```json { "route": "POST /v1/claims/validate" } ``` The answer (201) ```json { "id": "job_9e6d21650c98b40f5b5848da", "object": "job", "route": "POST /v1/claims/validate", "query": "", "mode": "test", "status": "awaiting_upload", "status_words": "Waiting for the request file: upload it with the signed form in `upload`. Nothing runs until it lands.", "created_at": "2026-10-20T19:12:00.142Z", "started_at": null, "finished_at": null, "upload": { "method": "POST", "url": "https://pw-jobs-example.s3.us-east-1.amazonaws.com/", "fields": { "key": "requests/pwk_0123456789abcdef01/job_9e6d21650c98b40f5b5848da.json", "Content-Type": "application/json", "x-amz-server-side-encryption": "aws:kms", "x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:us-east-1:111111111111:key/example", "x-amz-algorithm": "AWS4-HMAC-SHA256", "x-amz-credential": "AKIAEXAMPLE/20261020/us-east-1/s3/aws4_request", "x-amz-date": "20261020T191200Z", "policy": "eyJleHBpcmF0aW9uIjoiMjAyNi0xMC0yMFQyMDoxMjowMC4xNDJaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoicHctam9icy1leGFtcGxlIn0seyJrZXkiOiJyZXF1ZXN0cy9wd2tfMDEyMzQ1Njc4OWFiY2RlZjAxL2pvYl85ZTZkMjE2NTBjOThiNDBmNWI1ODQ4ZGEuanNvbiJ9LHsiQ29udGVudC1UeXBlIjoiYXBw…", "x-amz-signature": "492a2aacfb34613b509d8d879e7e11a35e16289190fffc040a722cebd08ffc59" }, "expires_at": "2026-10-20T20:12:00.142Z", "max_bytes": 50000000, "content_type": "application/json", "file_field": "file" }, "calls": null, "result": null, "result_kept_until": null, "error": null, "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…", "stored": true, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `id` | string | job_ and 24 lowercase hexadecimal characters. | | `object` | the value "job" | | | `route` | string | | | `query` | string | | | `mode` | one of "test", "live" | | | `status` | one of "awaiting_upload", "running", "completed", "partial", "failed", "canceled", "expired" | | | `status_words` | string | | | `created_at` | string | | | `started_at` | string or null | | | `finished_at` | string or null | | | `upload` | JobUpload or null | | | `calls` | object or null | | | `result` | object or null | | | `result_kept_until` | string or null | | | `error` | object or null | | | `cancel_requested` | boolean | Present while a cancel waits for the run to stop. | | `kept` | string | A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so after. Every other route answers and keeps nothing. | | `stored` | the value true | | | `transmitted` | the value false | | ### GET /v1/jobs **Your batch jobs.** This key’s jobs, newest first, at most 100: each one’s route, state and counts. A job whose result’s 24 hours are over is listed as expired, with nothing kept, until its record is removed. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/jobs" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getJobs(); ``` With the Python client ```python answer = pw.get_jobs() ``` The answer (200) ```json { "data": [ { "id": "job_9e6d21650c98b40f5b5848da", "object": "job", "route": "POST /v1/claims/validate", "query": "", "mode": "test", "status": "awaiting_upload", "status_words": "Waiting for the request file: upload it with the signed form in `upload`. Nothing runs until it lands.", "created_at": "2026-10-20T19:12:00.142Z", "started_at": null, "finished_at": null, "upload": null, "calls": null, "result": null, "result_kept_until": null, "error": null, "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…", "stored": true, "transmitted": false } ], "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `data` | list of Job | At most 100, newest first. | | `kept` | string | A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so after. Every other route answers and keeps nothing. | ### GET /v1/jobs/{id} **One batch job.** Its state (awaiting_upload, running, completed, partial, failed, canceled or expired), its calls by status, and once it has finished, a link to its result good for 15 minutes; ask again for a new one. The result is { answers: [ { index, status, body } ] }, each answer what that call would have answered. A job that ran out of time is partial: the answered calls are in the result and not_run_from is the first to send again. A job that failed says why in error. A job.completed or job.failed event reaches your webhook and destinations when it ends. A job whose file arrived but could not be started on our side within an hour fails with job_not_started. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getJobsById("job_9e6d21650c98b40f5b5848da"); ``` With the Python client ```python answer = pw.get_jobs_by_id("job_9e6d21650c98b40f5b5848da") ``` The answer (200) ```json { "id": "job_9e6d21650c98b40f5b5848da", "object": "job", "route": "POST /v1/claims/validate", "query": "", "mode": "test", "status": "awaiting_upload", "status_words": "Waiting for the request file: upload it with the signed form in `upload`. Nothing runs until it lands.", "created_at": "2026-10-20T19:12:00.142Z", "started_at": null, "finished_at": null, "upload": { "method": "POST", "url": "https://pw-jobs-example.s3.us-east-1.amazonaws.com/", "fields": { "key": "requests/pwk_0123456789abcdef01/job_9e6d21650c98b40f5b5848da.json", "Content-Type": "application/json", "x-amz-server-side-encryption": "aws:kms", "x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:us-east-1:111111111111:key/example", "x-amz-algorithm": "AWS4-HMAC-SHA256", "x-amz-credential": "AKIAEXAMPLE/20261020/us-east-1/s3/aws4_request", "x-amz-date": "20261020T191200Z", "policy": "eyJleHBpcmF0aW9uIjoiMjAyNi0xMC0yMFQyMDoxMTo1OS4xNDdaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0IjoicHctam9icy1leGFtcGxlIn0seyJrZXkiOiJyZXF1ZXN0cy9wd2tfMDEyMzQ1Njc4OWFiY2RlZjAxL2pvYl85ZTZkMjE2NTBjOThiNDBmNWI1ODQ4ZGEuanNvbiJ9LHsiQ29udGVudC1UeXBlIjoiYXBw…", "x-amz-signature": "2270f9df8728a7b0c33005d9d2100aa75795c1fb302c710b879b83442299f4a6" }, "expires_at": "2026-10-20T20:11:59.147Z", "max_bytes": 50000000, "content_type": "application/json", "file_field": "file" }, "calls": null, "result": null, "result_kept_until": null, "error": null, "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…", "stored": true, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `id` | string | job_ and 24 lowercase hexadecimal characters. | | `object` | the value "job" | | | `route` | string | | | `query` | string | | | `mode` | one of "test", "live" | | | `status` | one of "awaiting_upload", "running", "completed", "partial", "failed", "canceled", "expired" | | | `status_words` | string | | | `created_at` | string | | | `started_at` | string or null | | | `finished_at` | string or null | | | `upload` | JobUpload or null | | | `calls` | object or null | | | `result` | object or null | | | `result_kept_until` | string or null | | | `error` | object or null | | | `cancel_requested` | boolean | Present while a cancel waits for the run to stop. | | `kept` | string | A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so after. Every other route answers and keeps nothing. | | `stored` | the value true | | | `transmitted` | the value false | | ### POST /v1/jobs/{id}/cancel **Cancel a batch job.** Before its file is uploaded, nothing will run. While it runs, it stops between two calls and keeps no result. A finished job cannot be canceled: delete it. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da/cancel" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.postJobsByIdCancel("job_9e6d21650c98b40f5b5848da"); ``` With the Python client ```python answer = pw.post_jobs_by_id_cancel("job_9e6d21650c98b40f5b5848da") ``` The answer (200) ```json { "id": "job_9e6d21650c98b40f5b5848da", "object": "job", "route": "POST /v1/claims/validate", "query": "", "mode": "test", "status": "canceled", "status_words": "Canceled: nothing more runs and no result is kept.", "created_at": "2026-10-20T19:12:00.142Z", "started_at": null, "finished_at": "2026-10-20T19:12:00.150Z", "upload": null, "calls": null, "result": null, "result_kept_until": null, "error": null, "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…", "stored": true, "transmitted": false } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `id` | string | job_ and 24 lowercase hexadecimal characters. | | `object` | the value "job" | | | `route` | string | | | `query` | string | | | `mode` | one of "test", "live" | | | `status` | one of "awaiting_upload", "running", "completed", "partial", "failed", "canceled", "expired" | | | `status_words` | string | | | `created_at` | string | | | `started_at` | string or null | | | `finished_at` | string or null | | | `upload` | JobUpload or null | | | `calls` | object or null | | | `result` | object or null | | | `result_kept_until` | string or null | | | `error` | object or null | | | `cancel_requested` | boolean | Present while a cancel waits for the run to stop. | | `kept` | string | A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so after. Every other route answers and keeps nothing. | | `stored` | the value true | | | `transmitted` | the value false | | ### DELETE /v1/jobs/{id} **Delete a batch job.** Its file, its result and the job itself, now rather than after 24 hours. A running job must be canceled first. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteJobsById("job_9e6d21650c98b40f5b5848da"); ``` With the Python client ```python answer = pw.delete_jobs_by_id("job_9e6d21650c98b40f5b5848da") ``` The answer (200) ```json { "id": "job_9e6d21650c98b40f5b5848da", "deleted": true, "kept": "A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so af…" } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `id` | string | | | `deleted` | the value true | | | `kept` | string | A job keeps your request file until the job finishes, then deletes it, and keeps the result for 24 hours after the job finishes, fetched by a signed link; after that the job is gone and the files are deleted from storage in the day or so after. Every other route answers and keeps nothing. | ## Reading the answer - A job’s `status` is one of awaiting_upload, running, completed, partial, failed, canceled or expired, with its `status_words`. `upload` is the signed form while the upload window is open; post every field as given, then the file under `file_field`. Once it closes, a job still awaiting_upload says so in its `status_words`, and is called expired only 65 minutes after the window closed, because a file that landed in time may still be waiting to start. - Once it has finished, `calls` counts the answers by status and `result.url` is a link good for 15 minutes; ask again for a new one. The result is `{ answers: [ { index, status, body } ] }`, each `body` what that call would have answered. - A job that ran out of time is `partial`: the answered calls are in the result, and `not_run_from` is the first call to send again. A failed job says why in `error.code`. - A `job.completed` or `job.failed` event reaches your webhook and destinations when the job ends. It carries the job’s id, route, state and counts, never the result or its link. ## Troubleshooting - `jobs_not_offered` (501): Batch jobs are off on this host until Payerworks turns them on. A 501 is never retried by the clients: it will not pass by waiting. - `jobs_live_not_offered` (501): A job keeps your file and its result for a while, so it takes test keys only for now: use a test key, with invented data. - `invalid_job_route` (400): A job runs one of the routes listed in the message. A store route, a sandbox, a PDF or a model read is never a job. - `job_running` (409): A running job cannot be deleted: cancel it, then delete it once it says canceled. - `jobs_daily_limit` (429): An account’s keys together may make 50 jobs a UTC day. - One call is never made larger by a job: a call over 2 MB is answered 413 too_large at its index in the result. Split the file, or send many files as many calls. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_job` | 400 | A job is { route, query? }: route the one route it runs, query the query string every call carries (no leading ?), or left out. | | `invalid_job_id` | 400 | A job id is job_ and 24 letters or digits, as POST /v1/jobs answered it. | | `invalid_job_route` | 400 | route names the one route the job runs, as "POST /v1/claims/validate". A job runs: POST /v1/claims/validate, POST /v1/claims/837p, POST /v1/remittances/explain, POST /v1/eligibility/explain, POST /v1/claim-status/explain, POST /v1/analytics/summary, POST /v1/remittances/variance, POST /v1/claims/track, POST /v1/eligibility/270. | | `invalid_query` | 400 | A query parameter's name or value is over 200 characters, or holds a percent escape that does not decode. | | `job_not_found` | 404 | No job of this key has that id. A finished job is kept 24 hours. | | `job_not_cancellable` | 409 | This job has already finished, so there is nothing to cancel: DELETE it to forget it now. | | `job_running` | 409 | This job is running: cancel it first (POST /v1/jobs/{id}/cancel), then delete it once it says canceled. | | `jobs_daily_limit` | 429 | This account's keys have made the most jobs an account may make today (50 a UTC day, its keys together). Try again tomorrow. | | `jobs_live_not_offered` | 501 | Batch jobs answer test keys only for now, because a job keeps the request and its result for a while: use a test key, with invented data. | | `jobs_not_offered` | 501 | Batch jobs are not offered on this host: Payerworks turns them on when they are ready. This is not a passing fault, so sending the request again answers the same. | | `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. | ## Code lists ### Why a job failed (error.code) | Code | What it means | | --- | --- | | `job_file_not_json` | The uploaded file is not JSON. | | `job_file_shape` | The uploaded file is not this route's job: send { "calls": [ , ... ] }, each body what the route takes in one call (for POST /v1/eligibility/270, the route's own body with its `requests`). | | `job_too_many_calls` | The uploaded file holds more calls than one job runs: at most 1,000. Split it into more jobs. | | `job_too_many_requests` | The batch 270 holds more requests than one file is written from: at most 7,500. | | `job_file_too_large` | The uploaded file is larger than a job reads: at most 50,000,000 bytes. | | `job_key_not_usable` | The key that made this job was revoked, expired or replaced before the job ran, so nothing was run. | | `job_stopped` | The run stopped before it finished (on our side): nothing was kept. Make the job again. | | `job_internal` | Something failed on our side while the job ran: nothing was kept. Make the job again; if it keeps failing, write to us with the job id. | | `job_result_too_large` | Not even the first answer fits a job's result (250,000,000 bytes): send less in each call. | | `job_270_too_many_segments` | The batch 270 written from these requests holds more segments than one 270 is read in (`limit_segments`, our own reader's bound): nothing was kept. Send fewer requests in each job, or fewer procedures in each request. | | `job_not_started` | Your file arrived, but the job could not be started on our side within 60 minutes of its arrival: nothing was run and the file was deleted. Make the job again. | ## Example [jobs.mjs](https://payerworks.com/docs/examples/jobs.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. jobs.mjs ```js // Payerworks example: a batch job, many calls to one route from one file. Docs: https://payerworks.com/docs/jobs // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node jobs.mjs // A test key and invented data only. A job keeps its file until it finishes and its result for 24 hours. // Payerworks sends nothing to a payer: every file it writes is yours. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { CLAIM, copy, refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // Batch jobs are off until Payerworks turns them on. Where they are not, every jobs route answers // 501 jobs_not_offered (never retried: it will not pass by waiting), and each call is the same // work sent to its own route. let job = null; const off = await refusalOf(async () => { job = await pw.postJobs({ route: 'POST /v1/claims/validate' }); }, { PayerworksError }); if (off?.code === 'jobs_not_offered') return { offered: false, refused: off }; if (off) return { offered: null, refused: off }; // Where they are on: one file of calls, each exactly the body POST /v1/claims/validate takes. const read = await pw.uploadJobFile(job, { calls: [{ claim: copy(CLAIM) }, { claim: copy(CLAIM) }] }); // The failure path: a route a job never runs. const refused = await refusalOf(() => pw.postJobs({ route: 'POST /v1/remittances/from-image' }), { PayerworksError }); return { offered: true, status: read.status, kept: read.kept, refused }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "offered": false, "refused": { "status": 501, "code": "jobs_not_offered", "request_id": "req_1" } } ``` # Organizations and members > Keys call the API; people manage the account. Invite your team with a role, each signing in with an authenticator app, and decide who manages the keys. Page: https://payerworks.com/docs/organizations · Markdown: https://payerworks.com/docs/organizations.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Keys are how your systems call the API; members are how people manage the account. An organization holds one account (the keys one key started) and the people who manage it, each with a role: owner, admin, developer or viewer. A member signs in at payerworks.com/console with a password and a code from an authenticator app, always: there is no sign-in without the app. A sign-in never calls a claim, eligibility, file, sandbox, MCP or batch job route; those take a key. With a sign-in, a member manages the account’s keys, webhooks, enrollments, SFTP user and notice address, as far as their role allows, and reads its audit log. These routes take a sign-in’s ID token as the bearer, `Authorization: Bearer `, where every other route takes a key. Where the sign-in belongs to several organizations, the Payerworks-Organization header names one; a member’s call to one of the account’s routes (its keys, webhooks, events, enrollments, SFTP user, notice address or audit log) names the key it acts on in the Payerworks-Key header (its key id, as GET /v1/org/keys lists them), and is answered as that key would be answered, within the member’s role. Organizations are open only where Payerworks has turned them on; elsewhere nobody can sign in, a sign-in’s token is answered 501 `org_not_offered`, and keys work as before. ## Routes - [GET /v1/org/roles](#get-v1-org-roles): The roles - [GET /v1/org/memberships](#get-v1-org-memberships): Your organizations - [POST /v1/org](#post-v1-org): Make an organization - [GET /v1/org](#get-v1-org): Your organization - [PUT /v1/org](#put-v1-org): Change your organization - [GET /v1/org/members](#get-v1-org-members): The members - [PUT /v1/org/members/{id}](#put-v1-org-members-id): Change a member’s role - [DELETE /v1/org/members/{id}](#delete-v1-org-members-id): Remove a member - [GET /v1/org/invitations](#get-v1-org-invitations): The invitations waiting - [POST /v1/org/invitations](#post-v1-org-invitations): Invite a person - [DELETE /v1/org/invitations/{id}](#delete-v1-org-invitations-id): Cancel an invitation - [POST /v1/org/join](#post-v1-org-join): Accept an invitation - [GET /v1/org/keys](#get-v1-org-keys): The account’s keys Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown. ### GET /v1/org/roles **The roles.** The one role table: every role, every permission and the owner’s rules. Readable with a key or a sign-in. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org/roles" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrgRoles(); ``` With the Python client ```python answer = pw.get_org_roles() ``` The answer (200) ```json { "roles": [ { "role": "owner", "words": "Everything an admin does, and the owner's own decisions: owners, the organization's settings, and revoking a removed member's keys.", "permissions": [ "org.read", "members.read" ] }, { "role": "admin", "words": "Manages people (never an owner) and every key, webhook, enrollment and SFTP user, test or live.", "permissions": [ "org.read", "members.read" ] } ], "permissions": [ { "permission": "org.read", "words": "See the organization, its account and its settings." }, { "permission": "org.update", "words": "Rename the organization and choose who manages keys." } ], "owner_rules": [ "Only an owner invites an owner, makes a member an owner, or changes or removes an owner.", "An organization always keeps at least one owner: the last owner cannot be removed, leave, or be given another role." ] } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `roles` | list of object | Every role, the least last. | | `permissions` | list of object | | | `owner_rules` | list of string | The owner’s rules: 4 sentences. | ### GET /v1/org/memberships **Your organizations.** The organizations this sign-in belongs to, with its role in each. A sign-in only. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org/memberships" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrgMemberships(); ``` With the Python client ```python answer = pw.get_org_memberships() ``` The answer (200) ```json { "data": [ { "org_id": "pwo_25323f4c596673808d9aa7b4", "name": "Sample Dental", "role": "owner", "member_id": "pwm_4a5764717e8b98a5" } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `data` | list of object | At most 20. | ### POST /v1/org **Make an organization.** Make an organization for an account: name it and give one key of the account (api_key). The key proves the account and is checked and not kept; you become its owner. An account belongs to one organization. A sign-in only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/org" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postOrg(body); ``` With the Python client ```python answer = pw.post_org(body) ``` request.json ```json { "name": "Sample Dental", "api_key": "pw_test_qk3x..." } ``` The answer (201) ```json { "org_id": "pwo_25323f4c596673808d9aa7b4", "name": "Sample Dental", "account_id": "pwk_500de6d41a6eacebb6e34d08", "owners": 1, "members": 1, "key_management": "keys_and_members", "created_at": "2026-10-19T15:00:00.000Z", "you": { "member_id": "pwm_4a5764717e8b98a5", "role": "owner", "permissions": [ "org.read", "members.read" ] }, "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. | | `name` | string | 1 to 80 characters. | | `account_id` | string | The account it manages: the keys one key started. | | `owners` | integer | | | `members` | integer | | | `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). | | `created_at` | string | | | `updated_at` | string or null | | | `you` | OrgYou | | | `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. | | `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. | ### GET /v1/org **Your organization.** The organization, with your role and what it lets you do. Name it in the Payerworks-Organization header when your sign-in belongs to several. A sign-in only. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrg(); ``` With the Python client ```python answer = pw.get_org() ``` The answer (200) ```json { "org_id": "pwo_25323f4c596673808d9aa7b4", "name": "Sample Dental", "account_id": "pwk_500de6d41a6eacebb6e34d08", "owners": 1, "members": 1, "key_management": "keys_and_members", "created_at": "2026-10-19T15:00:00.000Z", "you": { "member_id": "pwm_4a5764717e8b98a5", "role": "owner", "permissions": [ "org.read", "members.read" ] }, "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call.", "pending_invitations": 0 } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. | | `name` | string | 1 to 80 characters. | | `account_id` | string | The account it manages: the keys one key started. | | `owners` | integer | | | `members` | integer | | | `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). | | `created_at` | string | | | `updated_at` | string or null | | | `you` | OrgYou | | | `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. | | `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. | ### PUT /v1/org **Change your organization.** Rename it, or choose who manages the account’s keys (key_management): keys_and_members, the default, where an API key of the account may still make, rotate and revoke its keys, or members_only, where only a signed-in member with the role for it may. An owner’s. Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/org" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putOrg(body); ``` With the Python client ```python answer = pw.put_org(body) ``` request.json ```json { "name": "Sample Dental Group" } ``` The answer (200) ```json { "org_id": "pwo_25323f4c596673808d9aa7b4", "name": "Sample Dental Group", "account_id": "pwk_500de6d41a6eacebb6e34d08", "owners": 1, "members": 1, "key_management": "keys_and_members", "created_at": "2026-10-19T15:00:00.000Z", "you": { "member_id": "pwm_4a5764717e8b98a5", "role": "owner", "permissions": [ "org.read", "members.read" ] }, "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call." } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. | | `name` | string | 1 to 80 characters. | | `account_id` | string | The account it manages: the keys one key started. | | `owners` | integer | | | `members` | integer | | | `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). | | `created_at` | string | | | `updated_at` | string or null | | | `you` | OrgYou | | | `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. | | `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. | ### GET /v1/org/members **The members.** Every member: the email address of the sign-in, the name, the role, when they joined and who invited them. A sign-in only. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org/members" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrgMembers(); ``` With the Python client ```python answer = pw.get_org_members() ``` The answer (200) ```json { "data": [ { "member_id": "pwm_4a5764717e8b98a5", "email": "jane.doe@sample-dental.com", "name": "Jane Doe", "role": "owner", "joined_at": "2026-10-19T15:00:00.000Z", "invited_by": null, "updated_at": null, "you": true }, { "member_id": "pwm_727f8c99a6b3c0cd", "email": "john.doe@sample-dental.com", "name": "John Doe", "role": "developer", "joined_at": "2026-10-19T15:00:00.000Z", "invited_by": "pwm_4a5764717e8b98a5", "updated_at": null, "you": false } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `data` | list of OrgMember | At most 200. | ### PUT /v1/org/members/{id} **Change a member’s role.** A new role: owner, admin, developer or viewer. Only an owner makes, changes or demotes an owner, and the last owner is never demoted. Request ```bash curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/org/members/pwm_727f8c99a6b3c0cd" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.putOrgMembersById("pwm_727f8c99a6b3c0cd", body); ``` With the Python client ```python answer = pw.put_org_members_by_id("pwm_727f8c99a6b3c0cd", body) ``` request.json ```json { "role": "viewer" } ``` The answer (200) ```json { "member_id": "pwm_727f8c99a6b3c0cd", "email": "john.doe@sample-dental.com", "name": "John Doe", "role": "viewer", "joined_at": "2026-10-19T15:00:00.000Z", "invited_by": "pwm_4a5764717e8b98a5", "updated_at": "2026-10-19T15:00:00.000Z", "you": false } ``` | Field | Type | What it is | | --- | --- | --- | | `member_id` | string | | | `email` | string | The sign-in’s verified email address. | | `name` | string | At most 120 characters. | | `role` | one of "owner", "admin", "developer", "viewer" | | | `joined_at` | string | | | `invited_by` | string or null | The member who invited them; null for the one who made the organization. | | `updated_at` | string or null | | | `you` | boolean | True for the member asking. | ### DELETE /v1/org/members/{id} **Remove a member.** The member loses access at once. revoke_keys=true, an owner’s choice, also revokes the keys they made or rotated in the console (each one whose secret they were shown); left out, those keys keep working and are listed in the answer. The last owner is never removed, and a member may remove themselves to leave. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/org/members/pwm_727f8c99a6b3c0cd" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteOrgMembersById("pwm_727f8c99a6b3c0cd"); ``` With the Python client ```python answer = pw.delete_org_members_by_id("pwm_727f8c99a6b3c0cd") ``` The answer (200) ```json { "removed": { "member_id": "pwm_727f8c99a6b3c0cd", "email": "john.doe@sample-dental.com", "name": "John Doe", "role": "viewer", "joined_at": "2026-10-19T15:00:00.000Z", "invited_by": "pwm_4a5764717e8b98a5", "updated_at": "2026-10-19T15:00:00.000Z" }, "left": false, "keys_made": [], "keys_rotated": [], "keys_revoked": [], "keys_not_revoked": [] } ``` | Field | Type | What it is | | --- | --- | --- | | `removed` | OrgMember | | | `left` | boolean | True when the member removed themselves. | | `keys_made` | list of string | The keys the member made in the console; null when they could not be read (keys_note says so). | | `keys_rotated` | list of string | The keys the member rotated in the console without making them; null when they could not be read. | | `keys_revoked` | list of string | Revoked by this removal, as an owner asked (revoke_keys=true): made or rotated. | | `keys_not_revoked` | list of string | Asked to be revoked (revoke_keys=true) and still working: the revocation failed, or the key was past the 25 one removal revokes. Revoke each with DELETE /v1/keys/{id}. | | `keys_note` | string | Present only when the member was removed but their keys could not be read: what to do next. | ### GET /v1/org/invitations **The invitations waiting.** Every invitation not yet accepted or canceled: the address, the role, who invited and until when the link works. A sign-in only. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org/invitations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrgInvitations(); ``` With the Python client ```python answer = pw.get_org_invitations() ``` The answer (200) ```json { "data": [ { "invitation_id": "pwi_03101d2a3744515e", "email": "john.doe@sample-dental.com", "role": "developer", "invited_by": "pwm_4a5764717e8b98a5", "created_at": "2026-10-19T15:00:00.000Z", "expires_at": "2026-10-26T15:00:00.000Z", "expired": false }, { "invitation_id": "pwi_4d5a6774818e9ba8", "email": "rick.roe@sample-dental.com", "role": "viewer", "invited_by": "pwm_4a5764717e8b98a5", "created_at": "2026-10-19T15:00:00.000Z", "expires_at": "2026-10-26T15:00:00.000Z", "expired": false } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `data` | list of OrgInvitation | At most 50 waiting. | ### POST /v1/org/invitations **Invite a person.** Invite a person by email with a role. Payerworks emails a link that works for 7 days, and only for a sign-in at that verified address; the email names neither the organization nor who invited. An organization sends at most 25 invitations a UTC day. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/org/invitations" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postOrgInvitations(body); ``` With the Python client ```python answer = pw.post_org_invitations(body) ``` request.json ```json { "email": "john.doe@sample-dental.com", "role": "developer" } ``` The answer (201) ```json { "invitation_id": "pwi_03101d2a3744515e", "email": "john.doe@sample-dental.com", "role": "developer", "invited_by": "pwm_4a5764717e8b98a5", "created_at": "2026-10-19T15:00:00.000Z", "expires_at": "2026-10-26T15:00:00.000Z", "expired": false, "emailed": true } ``` | Field | Type | What it is | | --- | --- | --- | | `invitation_id` | string | pwi_ and 16 lowercase hexadecimal characters. | | `email` | string | | | `role` | one of "owner", "admin", "developer", "viewer" | | | `invited_by` | string | | | `created_at` | string | | | `expires_at` | string | | | `expired` | boolean | The link works for 7 days. | | `emailed` | boolean | POST only: whether the email went. | | `join_url` | string | POST only, when the email did not go: the link to hand to the person yourself. | | `canceled` | the value true | DELETE only. | ### DELETE /v1/org/invitations/{id} **Cancel an invitation.** The link stops working at once. Request ```bash curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/org/invitations/pwi_4d5a6774818e9ba8" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.deleteOrgInvitationsById("pwi_4d5a6774818e9ba8"); ``` With the Python client ```python answer = pw.delete_org_invitations_by_id("pwi_4d5a6774818e9ba8") ``` The answer (200) ```json { "invitation_id": "pwi_4d5a6774818e9ba8", "email": "rick.roe@sample-dental.com", "role": "viewer", "invited_by": "pwm_4a5764717e8b98a5", "created_at": "2026-10-19T15:00:00.000Z", "expires_at": "2026-10-26T15:00:00.000Z", "expired": false, "canceled": true } ``` | Field | Type | What it is | | --- | --- | --- | | `invitation_id` | string | pwi_ and 16 lowercase hexadecimal characters. | | `email` | string | | | `role` | one of "owner", "admin", "developer", "viewer" | | | `invited_by` | string | | | `created_at` | string | | | `expires_at` | string | | | `expired` | boolean | The link works for 7 days. | | `emailed` | boolean | POST only: whether the email went. | | `join_url` | string | POST only, when the email did not go: the link to hand to the person yourself. | | `canceled` | the value true | DELETE only. | ### POST /v1/org/join **Accept an invitation.** Accept an invitation with the token from its link (the part after #join=). The sign-in’s verified email address must be the address the invitation was sent to. A sign-in only. Request ```bash curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/org/join" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" \ -H "Content-Type: application/json" \ -d @request.json ``` With the JavaScript client ```js const answer = await pw.postOrgJoin(body); ``` With the Python client ```python answer = pw.post_org_join(body) ``` request.json ```json { "token": "3uv4BRIfLDlGU2BteoeUoa67yNXi7_wJFiMwPUpXZHE" } ``` The answer (201) ```json { "organization": { "org_id": "pwo_25323f4c596673808d9aa7b4", "name": "Sample Dental Group", "account_id": "pwk_500de6d41a6eacebb6e34d08", "owners": 1, "members": 2, "key_management": "keys_and_members", "created_at": "2026-10-19T15:00:00.000Z", "you": { "member_id": "pwm_727f8c99a6b3c0cd", "role": "developer", "permissions": [ "org.read", "members.read" ] }, "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call." }, "member": { "member_id": "pwm_727f8c99a6b3c0cd", "email": "john.doe@sample-dental.com", "name": "John Doe", "role": "developer", "joined_at": "2026-10-19T15:00:00.000Z", "invited_by": "pwm_4a5764717e8b98a5", "updated_at": null, "you": true } } ``` Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json). | Field | Type | What it is | | --- | --- | --- | | `organization` | Organization | | | `member` | OrgMember | | ### GET /v1/org/keys **The account’s keys.** Every key of the organization’s account, test and live, as GET /v1/keys lists them, with who made each: a member in the console, or a key. Never a key itself. A sign-in only. Request ```bash curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/org/keys" \ -H "Authorization: Bearer $PAYERWORKS_API_KEY" ``` With the JavaScript client ```js const answer = await pw.getOrgKeys(); ``` With the Python client ```python answer = pw.get_org_keys() ``` The answer (200) ```json { "account_id": "pwk_500de6d41a6eacebb6e34d08", "data": [ { "key_id": "pwk_500de6d41a6eacebb6e34d08", "name": "Server", "mode": "test", "last_four": "4Lvg", "created_at": "2026-10-19T12:00:00.000Z", "expires_at": null, "expired": false, "revoked_at": null, "rotated_at": null, "previous_secret_accepted_until": null, "current": false, "made_by": { "kind": "key", "id": null } } ] } ``` | Field | Type | What it is | | --- | --- | --- | | `account_id` | string | | | `data` | list of object | Every key of the account, test and live; never a key itself. | ## Reading the answer - GET /v1/org answers the organization with `you`: your member id, your role and the permissions it gives. The console shows a control only for a permission the API answered. - GET /v1/org/roles is the one role table, the same for every organization, and readable with a key too. - A removal answers the keys the member made in the console: `keys_revoked` when an owner asked for revoke_keys=true, and `keys_not_revoked` otherwise, so nothing is revoked as a side effect. - An invitation that could not be emailed answers `emailed: false` with its link in `join_url`, to hand to the person yourself. ## Troubleshooting - `org_session_required` (401): The route needs a member’s console sign-in, not an API key. GET /v1/org/roles is the one a key may read. - `org_choose` (400): The sign-in belongs to several organizations: name one in the Payerworks-Organization header. - `org_role_forbidden` (403): Your role does not allow that; `needs` names the permission, and GET /v1/org/roles which roles hold it. - `org_last_owner` (409): An organization always keeps at least one owner. Make another member an owner first, then step down or leave. - `org_invitation_email_mismatch` (403): An invitation admits only a sign-in whose verified email address is the one it was sent to. Sign in with that address. - `org_acting_key_required` (400): A member’s call to one of the account’s routes names the key it acts on in the Payerworks-Key header. Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors). | Code | Status | What it means | | --- | --- | --- | | `invalid_org_invitation_id` | 400 | An invitation id is pwi_ followed by 16 lowercase hexadecimal characters. | | `invalid_org_member_id` | 400 | A member id is pwm_ followed by 16 lowercase hexadecimal characters. | | `invalid_org_request` | 400 | The request is not what this route takes: errors names each field and why. | | `org_acting_key_required` | 400 | Name the key this change is for in the Payerworks-Key header (its key id; GET /v1/org/keys lists them). | | `org_choose` | 400 | This sign-in belongs to several organizations: name one in the Payerworks-Organization header (GET /v1/org/memberships lists them). | | `org_session_invalid` | 401 | The sign-in could not be checked, or it has ended. Sign in again. | | `org_session_required` | 401 | This route needs a member's console sign-in, not an API key. Sign in at payerworks.com/console. | | `keys_managed_by_members` | 403 | This account's organization chose that only its signed-in members manage keys. Make, rotate or revoke the key at payerworks.com/console. | | `org_email_not_verified` | 403 | Confirm this sign-in's email address first: the code was emailed when the sign-in was made. | | `org_invitation_email_mismatch` | 403 | This invitation was sent to another address. Sign in with the address the email reached. | | `org_key_not_usable` | 403 | The api_key given is not a usable key (unknown, revoked or expired), so it proves no account. | | `org_not_a_member` | 403 | This sign-in belongs to no organization yet. Make one with POST /v1/org, or open an invitation's link. | | `org_owner_only` | 403 | Only an owner may do that: invite or make an owner, change or remove an owner, revoke a removed member's keys, or change the organization's settings. | | `org_role_forbidden` | 403 | Your role in this organization does not allow that. GET /v1/org/roles says which role does. | | `org_session_not_accepted` | 403 | A member's sign-in manages an account; it does not make this call. Call this route with an API key. | | `org_acting_key_not_found` | 404 | The organization's account has no usable key with that id. | | `org_invitation_not_found` | 404 | No pending invitation matches. It may have been accepted or canceled; ask for a new one. | | `org_member_not_found` | 404 | This organization has no member with that id. | | `org_not_found` | 404 | No organization with that id has this sign-in as a member. | | `org_account_taken` | 409 | That key's account already belongs to an organization. Ask one of its owners to invite you. | | `org_already_member` | 409 | That person is already a member of this organization. | | `org_changed_meanwhile` | 409 | The organization changed while this request was made. Read it again and send the change again. | | `org_invitation_pending` | 409 | An invitation to that address is already waiting. Cancel it to send another. | | `org_last_owner` | 409 | An organization always keeps at least one owner. Make another member an owner first. | | `org_limit_reached` | 409 | An organization holds at most 200 members and 50 pending invitations, and a sign-in belongs to at most 20 organizations. | | `org_invitation_expired` | 410 | An invitation's link works for 7 days. Ask a member of the organization for a new one. | | `org_invitations_daily_limit` | 429 | An organization sends at most 25 invitations a UTC day. Send more tomorrow. | | `org_not_offered` | 501 | Organizations and member sign-in are not open on this deployment yet. Keys still work as before. | | `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. | ## Code lists ### The roles | Role | What it may do | Permissions | | --- | --- | --- | | `owner` | Everything an admin does, and the owner's own decisions: owners, the organization's settings, and revoking a removed member's keys. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test`, `members.invite`, `members.change_role`, `members.remove`, `keys.write_live`, `webhooks.write_live`, `enrollments.write_live`, `sftp.write_live`, `contact.write`, `org.update` | | `admin` | Manages people (never an owner) and every key, webhook, enrollment and SFTP user, test or live. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test`, `members.invite`, `members.change_role`, `members.remove`, `keys.write_live`, `webhooks.write_live`, `enrollments.write_live`, `sftp.write_live`, `contact.write` | | `developer` | Builds with test keys: makes and rotates them, sets their webhooks, enrollments and SFTP users; reads everything live. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test` | | `viewer` | Reads everything (never a key itself, never a secret) and changes nothing. A seat for an auditor or a manager. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read` | ### The owner’s rules - Only an owner invites an owner, makes a member an owner, or changes or removes an owner. - An organization always keeps at least one owner: the last owner cannot be removed, leave, or be given another role. - Removing a member never revokes the keys they made or rotated in the console unless an owner says so in that removal (revoke_keys=true): a key whose secret they were shown, made or rotated, is theirs to revoke. - Only an owner decides whether an API key may still manage the account's keys (key_management). ### What an organization keeps An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. ## Example [organizations.mjs](https://payerworks.com/docs/examples/organizations.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it. organizations.mjs ```js // Payerworks example: organizations, members and roles. Docs: https://payerworks.com/docs/organizations // // Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs) // and sample-data.mjs beside this file: // PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL= node organizations.mjs // A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours. // // Keys call the API; members manage the account. A member signs in at payerworks.com/console with a // password and an authenticator app, and the organization routes take that sign-in. With a key, this // file reads the one role table and shows the refusal every other organization route gives a key. import { pathToFileURL } from 'node:url'; import { Payerworks, PayerworksError } from './payerworks.mjs'; import { refusalOf } from './sample-data.mjs'; /** @param {Payerworks} pw */ export async function run(pw) { // 1. The one role table, the same for every organization: readable with a key. const table = await pw.getOrgRoles(); // 2. The failure path: the organization itself answers a member's sign-in, never a key. const refused = await refusalOf(() => pw.getOrg(), { PayerworksError }); return { roles: table.roles.map((r) => r.role), permissions: table.permissions.length, owner_rules: table.owner_rules.length, refused, }; } // Run as a script: node , with PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL set. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { console.log(JSON.stringify(await run(new Payerworks()), null, 2)); } ``` What it answered, run against the API in Payerworks’ own tests (your request ids will differ) ```json { "roles": [ "owner", "admin", "developer", "viewer" ], "permissions": 22, "owner_rules": 4, "refused": { "status": 401, "code": "org_session_required", "request_id": "req_2" } } ``` # Every error > One shape for every refusal, one catalog of codes, and the few a client should wait on and send again. Page: https://payerworks.com/docs/errors · Markdown: https://payerworks.com/docs/errors.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Every refusal has one shape: an HTTP status, and a body `{ "error": { "code", "message", "request_id", ... } }`. The code is what to act on; the message is the sentence for a person, and may name the field, the figure or the kind; the request id is the one to quote when you write to us. A refusal of a request with several items (claims, questions, rows) lists each item’s own problems inside it. A refusal ```json { "error": { "code": "invalid_claims", "message": "One or more claims would be refused; nothing was written.", "claims": [ … ] } } ``` ## What to retry Retry only a wait: 429 (after the seconds retry-after names), 503, and 409 idempotency_in_flight, with the same Idempotency-Key. The JavaScript and Python clients do this for you, at most twice. Never retry a 400, a 404, a 422 or a 501 unchanged: they answer the same until the request changes, and a 501 code ending in _not_offered is a route switched off on this host. ## Every code The codes, grouped by where they are declared. A code is listed once, under the first part of the API that declares it, and means the same wherever it is answered. ### Any route that reads a claim or a file | Code | Status | What it means | | --- | --- | --- | | `no_claim` | 400 | The body has no claim: send `claim` (or `claims`) in the 837P shape the docs describe. | | `too_many_claims` | 400 | At most 100 claims a call. | | `invalid_claims` | 422 | One or more claims would be refused; nothing was written or run. Each claim's errors are listed. | | `duplicate_reference` | 422 | Two claims carry the same reference (patientControlNumber, CLM01); each must be unique so its answers match back. | | `no_x12` | 400 | Send the file's text as `x12`. | | `not_read_here` | 422 | This file is a kind Payerworks recognizes but does not read here yet; the error names its kind. | | `unrecognized_x12` | 422 | This is not an X12 file Payerworks reads; the message lists the kinds it reads. | | `not_an_835` | 422 | This is not an 835 remittance. | | `not_an_acknowledgment` | 422 | This is not a 999, a 277CA or a TA1. | | `invalid_code` | 400 | An adjustment reason code is one to four digits, or a letter and digits (such as 45, A1 or B13), optionally after its group. | | `invalid_category` | 400 | A claim status category is a letter and up to two letters or digits, such as A1 or F2. | | `invalid_path` | 400 | The path holds a percent escape that does not decode (for example %E0 or a lone %). Encode the value with encodeURIComponent. | | `invalid_sandbox_attachments` | 422 | A 275 sent back to the sandbox cannot be read; nothing was run. Each problem is listed with its field. | | `answered_by_the_door` | 501 | This route reads your key's account, so only the hosted API answers it, never the engine run on its own (in a browser or a test). | ### Any route that writes a file | Code | Status | What it means | | --- | --- | --- | | `invalid_sender_id` | 400 | sender_id is your submitter id at the receiver: 2 to 15 letters, digits or spaces. | | `invalid_receiver_id` | 400 | receiver_id is the receiver's interchange id: 2 to 15 letters, digits or spaces. | | `invalid_control_number` | 400 | control_number is the interchange control number you track, a whole number from 1 to 999999999. | | `invalid_usage` | 400 | usage is "T" (test) or "P" (production). | | `test_key_production_file` | 403 | A test key writes test interchanges only (ISA15 = T). Use a live key for a production file. | | `sandbox_test_only` | 400 | A sandbox payer answers test interchanges only: usage is "T" or left out. | | `invalid_query` | 400 | A query parameter's name or value is over 200 characters, or holds a percent escape that does not decode. | | `answer_too_large` | 413 | This answer is larger than one response can carry: send less in each call (split the file, or send fewer claims or requests). | | `file_too_large` | 413 | This file holds more than one call can read: the error names what was counted (`unit`) and the most one call reads (`limit`). Split it and send each part in its own call. | ### Any route that reads X12 | Code | Status | What it means | | --- | --- | --- | | `mixed_functional_groups` | 422 | This interchange holds functional groups of different kinds: send each kind in its own interchange, so no part of it goes unread. | ### Eligibility: the eligibility routes On [Eligibility](https://payerworks.com/docs/eligibility). | Code | Status | What it means | | --- | --- | --- | | `no_request` | 400 | The body has no eligibility request: send `requests` (a list), `request`, or the request itself in the JSON shape the docs describe. | | `too_many_requests` | 400 | At most 100 eligibility requests a call. | | `invalid_requests` | 422 | One or more eligibility requests would be refused; nothing was written. Each request's errors are listed. | | `duplicate_trace` | 422 | Two requests in one file carry the same trace (controlNumber); each must be unique so its answer matches back. | | `no_answer` | 400 | Send the 271 as `x12` (the file's text) or as `json` (the JSON answer). | | `not_a_271` | 422 | This is not an X12 271 eligibility answer: send the 271 with its ISA envelope. | | `not_a_271_json` | 422 | This JSON is not an eligibility answer: it holds no plans, benefitsInformation, errors or subscriber. | | `invalid_asked` | 400 | asked is { service_type_codes: [codes], date_of_service: YYYYMMDD }, each optional. | | `invalid_service_type` | 400 | A service type code is one to three letters or digits, such as 30 or 98. | | `invalid_aaa_code` | 400 | A reject reason code (AAA03) is one or two letters or digits, such as 72 or T4. | | `invalid_loop` | 400 | loop is the loop the reason was returned in, such as 2100C or 2110D. | | `invalid_include` | 400 | include names what to add to the answer: answer (the normalized read of the 271). | | `invalid_shape` | 400 | shape names how the answer that include=answer adds is written: normalized (the default) or plans (the grouped JSON shape). | ### Claim status: the claim-status routes On [Claim status](https://payerworks.com/docs/claim-status). | Code | Status | What it means | | --- | --- | --- | | `no_inquiry` | 400 | Send the claim status question as `inquiry` (or `inquiries`), in the JSON claim status shape. | | `too_many_inquiries` | 400 | At most 100 claim status questions a call. | | `invalid_inquiries` | 422 | One or more claim status questions would be refused; nothing was written. Each question's errors are listed. | | `duplicate_inquiry_trace` | 422 | Two questions in one file carry the same trace (encounter.trackingNumber); each must be unique so its answer matches back. | | `no_claim_status_answer` | 400 | Send the payer's claim status answer as `x12` (a 277) or `json` (the JSON answer). | | `ambiguous_answer` | 400 | Send the answer once: as `x12` or as `json`, not both. | | `invalid_claim_status_json` | 400 | `json` is the JSON claim status answer, an object with its `claims` list (or its errors). | | `not_a_claim_status_answer` | 422 | This is not a claim status answer Payerworks reads: send a 277 (005010X212) or the JSON answer. | | `invalid_status_asked` | 400 | `asked` describes the question you asked; one of its fields is not in the form it takes. | | `not_a_claim_status_question` | 422 | This is not a claim status question: send a 276 (005010X212) as `x12`, or the questions as `inquiry` or `inquiries`. | | `invalid_status_code` | 400 | A claim status code is one to five digits, such as 65 or 689. | | `ambiguous_question` | 400 | Send the claim status questions once: as `inquiry` or `inquiries`, or as `x12` (a 276), not both. | | `invalid_payer_name` | 400 | `payer_name` is the payer's name as the 276 writes it (2100A NM103): text of at most 60 characters. | | `invalid_submitter_name` | 400 | `submitter_name` is your organization's name as the 276 writes it (2100B NM103): text of at most 60 characters. | ### Remittances and denials: the denials routes On [Remittances and denials](https://payerworks.com/docs/remittances). | Code | Status | What it means | | --- | --- | --- | | `no_remittance` | 400 | Send an 835 as x12, or the claims as a list in claims. | | `two_inputs` | 400 | Send either x12 (an 835) or claims (a list), not both. | | `invalid_claim_facts` | 422 | One or more claims or facts are not usable; nothing was decided. Each problem is listed with its field. | | `invalid_letter_facts` | 422 | One or more facts are not usable; nothing was written. Each is listed with its field. | | `unknown_letter_kind` | 400 | kind is one of the letter kinds the answer lists, with the facts each requires. | | `missing_facts` | 422 | The letter needs facts that were not sent; the answer lists them. | | `letter_refused` | 422 | The letter was not written: a fact holds a placeholder in brackets, which the guard refuses. | | `invalid_letter` | 400 | Send the letter's text as letter, at most 20000 characters. | | `invalid_group_code` | 400 | A claim adjustment group code is two letters, such as CO or PR. | | `is_a_remark_code` | 400 | This is a remittance advice remark code (RARC), not a claim adjustment reason code (CARC). | ### Events and webhooks: the events routes On [Events and webhooks](https://payerworks.com/docs/events). | Code | Status | What it means | | --- | --- | --- | | `invalid_limit` | 400 | limit is a whole number from 1 to 100. | | `invalid_cursor` | 400 | starting_after is the next_cursor of an earlier page, or the id of one of this key's events. | | `invalid_event_type` | 400 | type is one of the event types the developers page lists. | | `invalid_event_id` | 400 | An event id is evt_ followed by 16 to 64 lowercase hexadecimal characters. | | `event_not_found` | 404 | No event of this key has that id. Events are kept for 30 days. | | `invalid_delivery_state` | 400 | The delivery state filter names one of the states the developers page lists. | | `invalid_created` | 400 | created_gte and created_lt are instants written like 2026-10-19T14:00:00Z, and created_gte comes before created_lt. | ### Acknowledgments: the claims routes On [Acknowledgments](https://payerworks.com/docs/acknowledgments). | Code | Status | What it means | | --- | --- | --- | | `no_claims_to_track` | 400 | Send the claims to track as `claims` (a list) or `x12_837` (an 837P interchange, or a list of them). | | `ambiguous_claims` | 400 | Send the claims once: as `claims` or as `x12_837`, not both. | | `invalid_track_request` | 400 | A field of the request is not in the shape it takes; each problem is listed with its field. | | `invalid_tracked_claims` | 422 | One or more claims cannot be tracked; nothing was read. Each problem is listed with its field. | | `not_a_tracked_file` | 422 | A file is not a kind its list takes: x12_837 holds 837P claims, acknowledgments holds 999s, 277CAs and 277 requests for additional information, remittances holds 835s, attachments holds solicited 275s. The error names the file and its kind. | | `too_much_to_track` | 413 | More was sent than one call tracks: the error names what was counted (`unit`) and the most one call takes (`limit`). Send fewer claims or files a call. | ### Enrollment: the enrollment-import routes On [Enrollment](https://payerworks.com/docs/enrollment). | Code | Status | What it means | | --- | --- | --- | | `invalid_import` | 422 | The CSV could not be read as an enrollment import, so no row was checked or saved. Each problem is listed; the columns are in the import template. | | `export_takes_no_paging` | 400 | An export holds every request that matches its filters, so it takes no limit or starting_after. | | `export_too_large` | 413 | Every request matching these filters does not fit in one export. Narrow the filters (a payer, a transaction, a created range) and export each part. | ### Enrollment: the enrollments routes On [Enrollment](https://payerworks.com/docs/enrollment). | Code | Status | What it means | | --- | --- | --- | | `invalid_provider` | 422 | One or more provider fields would be refused; nothing was saved. Each problem is listed with its field. | | `provider_exists` | 409 | This key already has a provider with this NPI and tax id; the error names it. | | `provider_limit_reached` | 409 | A key holds at most 500 providers (a test key 50). Delete one you no longer use. | | `invalid_provider_id` | 400 | A provider id is prv_ followed by 24 lowercase hexadecimal characters. | | `provider_not_found` | 404 | This key has no provider with that id. | | `provider_in_use` | 409 | The NPI and tax id can no longer change: a request for this provider has gone to a payer. The error names those requests. | | `provider_has_enrollments` | 409 | This provider has requests that are open or live; cancel them first. The error names them. | | `invalid_starting_after` | 400 | starting_after is the next_cursor of an earlier page, or the id of an item on an earlier page. | | `invalid_npi_filter` | 400 | npi is the provider’s 10-digit NPI, with a valid check digit. | | `invalid_tax_id_filter` | 400 | tax_id is the provider’s 9-digit EIN, with or without the hyphen after the second digit. | | `q_too_long` | 400 | q is at most 100 characters. | | `invalid_enrollment` | 422 | One or more enrollment fields would be refused; nothing was saved. Each problem is listed with its field. | | `enrollment_exists` | 409 | This provider already has an open request for this payer and transaction; the error names it and its status. | | `enrollment_limit_reached` | 409 | A key holds at most 5,000 enrollment requests (a test key 500). Delete drafts and ended requests you no longer need. | | `invalid_enrollment_id` | 400 | An enrollment id is enr_ followed by 24 lowercase hexadecimal characters. | | `enrollment_not_found` | 404 | This key has no enrollment request with that id. | | `enrollment_sent` | 409 | This request has gone to the payer, so its fields can no longer change. Cancel it and file a new one. | | `enrollment_ended` | 409 | This request is canceled, rejected or ended; nothing more can change. File a new request. | | `enrollment_not_deletable` | 409 | Only a draft, canceled or rejected request can be deleted. Cancel it first. | | `enrollment_not_draft` | 409 | Only a draft is submitted; this request already was. | | `invalid_cancel` | 422 | reason is text of at most 200 characters, with no run of eight or more digits. | | `invalid_status_filter` | 400 | status is one or more of the eight enrollment statuses, separated by commas. | | `invalid_transaction_filter` | 400 | transaction is eligibility, claim_status, professional_claims or remittances. | | `invalid_payer_filter` | 400 | payer_id is 1 to 80 letters and digits. | | `invalid_provider_filter` | 400 | provider_id is prv_ followed by 24 lowercase hexadecimal characters. | | `invalid_waiting_on_filter` | 400 | waiting_on is you, provider, payerworks or payer. | | `invalid_date_filter` | 400 | A date filter is a day, YYYY-MM-DD, or an instant, YYYY-MM-DDTHH:MM:SSZ. | | `invalid_task_id` | 400 | A task id is tsk_, the enrollment’s 24 hexadecimal characters, an underscore and two digits. | | `task_not_found` | 404 | This key has no task with that id. | | `task_not_open` | 409 | This request is not waiting on the provider, so its tasks cannot change now. | | `task_blocked` | 409 | A task ranked before this one is still open; the error names it, its rank and who holds it. Tasks are finished in rank order. | | `task_is_payerworks` | 409 | This task is Payerworks’ to do, not the provider’s. It is shown so you can see what Payerworks is waiting on. | | `invalid_task_values` | 422 | One or more task values would be refused; nothing was saved. Each problem is listed with its field. | | `invalid_document_request` | 422 | Send task_id, field_key and filename (ending .pdf, at most 120 characters); each problem is listed with its field. | | `field_not_a_document` | 422 | That field of the task does not take a document. | | `document_limit_reached` | 409 | An enrollment request holds at most 20 documents. Delete one you no longer need. | | `upload_unavailable` | 503 | No upload address could be made just now; nothing was saved. Retry in a moment. | | `invalid_document_id` | 400 | A document id is doc_, the enrollment’s 24 hexadecimal characters, an underscore and 8 more. | | `document_not_found` | 404 | This key has no document with that id. | | `upload_missing` | 409 | Nothing has arrived at the upload address yet. Upload the PDF, then confirm again. | | `not_a_pdf` | 422 | The uploaded file is not a PDF (it does not start %PDF-). It was deleted; upload the PDF again. | | `document_too_large` | 422 | A document is at most 10 MB. It was deleted; upload a smaller PDF. | | `document_not_uploaded` | 409 | This document has not been uploaded and confirmed, so there is nothing to download. | | `document_in_use` | 409 | A completed task names this document. Reopen the task first (complete: false), then delete it. | | `invalid_payer_id` | 400 | A payer id is 1 to 80 letters and digits. | | `sandbox_nothing_to_advance` | 409 | The sandbox payer has no move to make now: the next move is yours, the request has ended, or this scenario never moves. The error says whose turn it is. | | `changed_meanwhile` | 409 | This request changed while yours ran, so nothing was saved. Send it again. | ### Claims: the edits routes On [Claims](https://payerworks.com/docs/claims). | Code | Status | What it means | | --- | --- | --- | | `invalid_edit_filter` | 400 | A filter on GET /v1/edits is snip_type (1 to 7), severity (error or warning), kind (deterministic or code_set) or runs_in (edits, validateClaim, validateSecondaryClaim or correction-plan); the message names the one that is not. | | `invalid_edit_code` | 400 | An edit code is snake_case letters, digits and underscores, at most 64 characters, such as future_date. | | `edit_not_found` | 404 | No edit has this code: GET /v1/edits lists every edit Payerworks runs and the ones it does not. | ### Claims: the corrections routes On [Claims](https://payerworks.com/docs/claims). | Code | Status | What it means | | --- | --- | --- | | `invalid_change` | 400 | change is "replace" (correct a claim) or "void" (cancel it). | | `invalid_payer_claim_number` | 400 | payer_claim_number is the payer's own number for the claim, at most 50 characters and none of the X12 delimiters (~ * : ^). | | `payer_claim_number_needed` | 422 | The payer processed the claim, so the plan needs its claim number: it is CLP07 on the 835, REF*1K on the 277CA, or the number a claim status answer gives. | | `invalid_medicare_mac` | 400 | medicare_mac is true or false. | | `invalid_adjudicated` | 400 | adjudicated is true or false: true says the payer processed the claim even when its number is not at hand. | | `claim_type_not_planned` | 422 | claim_type is "professional" or "dental": an institutional claim carries its frequency in the type of bill, which this plan does not cover yet. | | `invalid_correction_claim` | 400 | original and replacement are each one claim in the 837P JSON shape POST /v1/claims/837p takes. | | `not_an_837p` | 422 | An x12 read as claims (original_x12 and replacement_x12 on POST /v1/claims/correction-plan, x12 on POST /v1/claims/validate) is an 837P interchange (GS08 005010X222A1). | | `several_claims_in_file` | 422 | original_x12 and replacement_x12 each hold exactly one claim: split a bulk file with POST /v1/x12/parse first. | | `adjudication_unknown` | 422 | Say whether the payer processed the claim: send payer_claim_number (CLP07 on the 835, REF*1K on the 277CA) when it did, or adjudicated: false when it was rejected before processing (a 999 or 277CA rejection). Sending a processed claim again as a new one is a duplicate. | | `not_an_837d` | 422 | With claim_type dental, original_x12 and replacement_x12 are each a one-claim 837D interchange (GS08 005010X224A2). | | `dental_not_checked_here` | 422 | A dental claim sent as an 837D is read where the API runs (POST /v1/claims/correction-plan), not on this page: send it as JSON here, or call the API. | ### Payer directory: the payers routes On [Payer directory](https://payerworks.com/docs/payers). | Code | Status | What it means | | --- | --- | --- | | `payer_not_in_directory` | 404 | The Payerworks directory holds no published facts about this payer. It lists payers and plan programs from public federal files, not every payer, and not the payers Payerworks can send to. | | `not_a_directory_row_id` | 400 | GET /v1/payers/{id} takes a directory row id (pwd_ and 16 hex), never a payer ID: search GET /v1/payers?q= to find a payer by its payer ID or name. | | `invalid_payer_query` | 400 | A search parameter is out of shape; the error names it in field (q, state, type, limit or starting_after). | | `payer_directory_unavailable` | 503 | The payer directory is not loaded where this was asked; the Payerworks API answers it. | ### Claim status: the follow-up routes On [Claim status](https://payerworks.com/docs/claim-status). | Code | Status | What it means | | --- | --- | --- | | `no_claims_to_follow` | 400 | Send the open claims as `claims` (each with its reference, payer_id and sent_on), or the files as `files` (x12_837, acknowledgments, remittances). | | `ambiguous_follow_up` | 400 | Send the claims once: as `claims` or as `files`, not both. | | `invalid_follow_up_request` | 400 | A field of the request is not in the shape it takes; each problem is listed with its field. | | `too_much_to_follow` | 413 | More was sent than one call takes: the error names what was counted (`unit`) and the most one call takes (`limit`). | ### Remittances and denials: the remittance-extras routes On [Remittances and denials](https://payerworks.com/docs/remittances). | Code | Status | What it means | | --- | --- | --- | | `no_remittances` | 400 | Send the 835s as remittances: a list of X12 files, each the file's text. | | `too_many_remittances` | 413 | More was sent than one call reads across its files (at most 50 files, 1000 checks, 5000 claims, 5000 provider-level adjustments and 2000 deposits): the error names the unit and the limit. Send fewer in each call. | | `deposits_required` | 400 | Send the bank's deposits as deposits: a list of { amount, effective_date, trace_number?, originator?, description?, id? }. | | `invalid_deposits` | 422 | One or more deposits are not usable; nothing was matched. Each problem is listed with its field. | | `invalid_split_by` | 400 | by is "payee" (the 2000 loop's provider, TS301) or "rendering_npi" (each claim's rendering provider, NM1*82). | | `value_holds_a_delimiter` | 422 | A value in the file holds one of the characters Payerworks writes as a delimiter (* : ^ ~), so it cannot be copied unchanged; nothing was written. | | `invalid_payer` | 400 | Name the payer by its slug from GET /v1/filing-limits, or by its name. | | `invalid_date_of_service` | 400 | date_of_service is a day, YYYY-MM-DD. | | `no_filing_limit_on_record` | 404 | No published filing window is on record for this payer. Ask the payer, or read your agreement with it; GET /v1/filing-limits lists every payer that has one. | ### Analytics: the analytics routes On [Analytics](https://payerworks.com/docs/analytics). | Code | Status | What it means | | --- | --- | --- | | `invalid_analytics_request` | 400 | A field of the request is not in the shape this route takes; each problem is listed with its field. | | `too_much_to_analyze` | 413 | More was sent than one call reads: the error names what was counted (`unit`) and the most one call takes (`limit`). Send fewer files a call. | | `nothing_to_summarize` | 400 | Send the claims (claims or x12_837) with their acknowledgments and remittances, or the remittances alone. | | `invalid_fee_schedule` | 422 | One or more fee schedule rows are not usable; nothing was judged. Each problem is listed with its field. | | `no_claim_to_check` | 400 | Send the claim to check as claim, in the 837P JSON shape POST /v1/claims/validate takes. | | `invalid_risk_claim` | 422 | The claim cannot be compared: each problem is listed with its field. Nothing was read. | | `invalid_outcomes` | 422 | One or more appeal outcomes are not usable; nothing was ranked. Each problem is listed with its field. | ### Eligibility: the eligibility-tools routes On [Eligibility](https://payerworks.com/docs/eligibility). | Code | Status | What it means | | --- | --- | --- | | `no_identity_request` | 400 | Send `request` (the eligibility request the payer refused, in the JSON POST /v1/eligibility/270 takes) and the reasons: `reasons` [{ code, loop }] or the refusing `answer` (x12 or json). | | `invalid_identity_request` | 422 | The request would be refused as a 270 before any retry is planned: its errors are listed. | | `invalid_reasons` | 400 | reasons is a list of { code, loop }: code an AAA reject reason (such as 75), loop where it was returned (such as 2100C), at most 20. | | `invalid_known` | 400 | known holds only text facts the caller gives: legal_first_name, previous_last_name, dependent_member_id, each at most 80 characters. | | `no_reasons` | 422 | Neither reasons nor the answer names a reject reason to plan from. | | `no_answer_to_read` | 400 | Send the 271 as `x12` (the file’s text) or as `json` (either JSON eligibility answer). | | `answer_not_read` | 422 | An answer could not be read as an eligibility answer: send an X12 271 with its envelope, or a JSON eligibility answer. | | `invalid_service_type_test` | 400 | Send { baseline, specific, service_type_code, baseline_service_type }: two answers (x12 or json), the specific service type code asked, and the baseline asked (30, or 35 for dental). | | `invalid_coordination` | 422 | The coordination question would not be answered as sent: each problem is listed. | | `invalid_discovery_verify` | 400 | Send { patient: { first_name, last_name, date_of_birth, address?, previous_addresses?, gender? }, coverages: [answers, x12 or json], date_of_service? }. | | `too_many_coverages` | 400 | Verify at most 10 coverages a call. | ### Prior authorization: the authorizations routes On [Prior authorization](https://payerworks.com/docs/prior-authorization). | Code | Status | What it means | | --- | --- | --- | | `no_authorization_request` | 400 | The body has no review request: send `requests` (a list), `request`, or the request itself in the JSON shape the docs describe. | | `too_many_authorization_requests` | 400 | At most 100 review requests a call. | | `invalid_authorization_requests` | 422 | One or more review requests would be refused; nothing was written. Each request's errors are listed. | | `duplicate_authorization_trace` | 422 | Two review requests in one file carry the same trace (controlNumber); each must be unique so its response matches back. | | `invalid_output` | 400 | output is "x12" (the default), "fhir" (the Da Vinci PAS bundle shape) or "both". | | `no_authorization_answer` | 400 | Send the 278 response as `x12` (the file's text), as `json` (Payerworks' own read of one) or as `fhir` (a Da Vinci PAS response Bundle or ClaimResponse). | | `not_a_278` | 422 | This is not an X12 278 request for review and response (005010X217): send the 278 with its ISA envelope. | | `not_a_278_response` | 422 | This 278 holds requests only (BHT02 13): send the payer's response (BHT02 11). | | `not_a_278_json` | 422 | This JSON is not a read 278: it holds no `reviews` with a decision or a rejection. | | `not_a_pas_response` | 422 | This FHIR resource is not a PAS response: send a Bundle holding a ClaimResponse, or the ClaimResponse, with a reviewAction or an error. | | `invalid_asked_requests` | 400 | asked is the review request, or the list of them, that the response answers. | | `invalid_explain_include` | 400 | include names what to add to the answer: answer (the read of the 278). | | `no_procedure_codes` | 400 | Send `procedure_codes`: a list of HCPCS or CPT codes, such as L1833 or 64490. | | `too_many_procedure_codes` | 400 | At most 100 procedure codes a call. | | `invalid_procedure_codes` | 400 | Each procedure code is a HCPCS Level II code (a letter and four digits) or a five-character CPT code. | | `invalid_payer_type` | 400 | payer_type is medicare_fee_for_service, medicare_advantage, medicaid, commercial or other. | | `invalid_state` | 400 | state is the two-letter state the patient lives in, such as CA. | | `invalid_setting` | 400 | setting is hospital_outpatient or other (where the service is furnished), or left out. | ### Patient estimates: the patient-money routes On [Patient estimates](https://payerworks.com/docs/patient-money). | Code | Status | What it means | | --- | --- | --- | | `no_estimate_lines` | 400 | Send lines: the planned services, each with the allowed amount you expect (allowed_amount), at most 50 a call. | | `too_many_estimate_lines` | 400 | At most 50 estimate lines a call. | | `invalid_estimate` | 422 | The estimate request has fields that cannot be read; nothing was figured. Each problem is listed. | | `member_required` | 422 | This 271 answers more than one patient: send member_index to say which one to estimate. | | `no_member` | 422 | This 271 answers no patient to estimate. | | `invalid_good_faith_estimate` | 422 | The Good Faith Estimate is missing elements 45 CFR 149.610 requires, or has fields that cannot be read; nothing was laid out. Each problem is listed. | | `no_dme_items` | 400 | Send items: each { hcpcs, delivered_on }, at most 100 a call. | | `too_many_dme_items` | 400 | At most 100 items a call. | | `invalid_dme_items` | 422 | One or more items cannot be read; nothing was answered. Each problem is listed. | | `invalid_fpl_request` | 422 | The request has fields that cannot be read; nothing was figured. Each problem is listed. | | `guideline_not_defined` | 422 | The HHS poverty guidelines are not defined for Puerto Rico or the other territories; send use_contiguous_table: true if your program uses the contiguous table there. | | `guideline_year_not_held` | 422 | Payerworks holds the 2026 HHS poverty guidelines only; send guideline_year 2026 or leave it out. | ### Documents and attachments: the documents routes On [Documents and attachments](https://payerworks.com/docs/documents). | Code | Status | What it means | | --- | --- | --- | | `invalid_document_option` | 400 | An option of this document is not one the route takes: the error names it and what it takes. | | `too_many_forms` | 400 | These claims need more CMS-1500 forms than one call draws (200, at six service lines a form): send fewer claims in each call. | | `no_remittance_file` | 400 | Send the 835 as x12 (the file's text). | | `invalid_attachments` | 422 | The 275 cannot be written as asked; nothing was written. Each problem is listed with its field. | | `documents_unavailable` | 503 | The PDF could not be drawn just now: the part of the API that draws it is not loaded. Retry in a moment. The engine run on its own, in a browser or a test, draws no PDF; the hosted Payerworks API, at the base URL shown with your key, does. | | `unknown_appeal_form` | 400 | form is redetermination (CMS-20027) or reconsideration (CMS-20033). | | `invalid_appeal_form_input` | 422 | One or more fields cannot be typed into the form; nothing was filled. Each problem is listed with its field. | | `claim_not_on_remittance` | 422 | The 835 holds no claim payment with this reference (CLP01); send the reference the denying claim payment carries. | | `not_medicare_fee_for_service` | 422 | These are Original Medicare's forms, and the 835 says this claim is not Medicare fee-for-service (CLP06 is not MA or MB): use the plan's own appeal, or POST /v1/appeals/letter. | | `form_asset_changed` | 503 | The form file on our side is not the one CMS published that we pinned, so nothing was filled. This is ours to fix. | ### Documents and attachments: the model-reads routes On [Documents and attachments](https://payerworks.com/docs/documents). | Code | Status | What it means | | --- | --- | --- | | `no_image` | 400 | Send the file as base64: file_base64 for a paper EOB, front_base64 (and back_base64 if you have it) for an insurance card. | | `invalid_image_base64` | 400 | The file is not base64: send the file's bytes base64 encoded (A-Z, a-z, 0-9, + and /, with = padding), with no data: prefix. | | `image_too_large` | 413 | The file is larger than one call reads: at most 1,400,000 bytes in all, before base64. Send a smaller copy (a lower resolution photo, or fewer pages). | | `unreadable_image` | 422 | The file could not be read as a picture or a PDF; the error says why (reason) and what to send instead. | | `too_many_pages` | 413 | The PDF holds more pages than one call reads: the error names the most (limit). Send fewer pages in each call. | | `model_read_not_offered` | 501 | Reading a document with a model is not offered on this host: Payerworks turns it on route by route. This is not a passing fault, so sending the request again answers the same. | | `model_read_test_keys_only` | 403 | This route answers test keys only, with invented documents, until an agreement covering the model is in place for live data. Use a test key. | | `model_read_unavailable` | 503 | The document reader could not be loaded just now, so nothing was read. Retry in a moment. The engine run on its own, in a browser or a test, has no reader; the hosted Payerworks API does. | | `model_read_timed_out` | 504 | The read did not finish inside one call's time. Send less: one page, or a crop of the part you need. | | `model_read_failed` | 502 | The reader gave no usable answer for this file, so nothing is answered from it. Send a clearer copy, or try again. | | `not_an_eob` | 422 | The reader did not see a payer's explanation of benefits or remittance advice in this file, so no draft was written. | | `not_an_insurance_card` | 422 | The reader did not see a health insurance card in this file, so nothing was looked up. | | `model_read_daily_limit` | 429 | This account's keys have made the most model-read calls an account may make on this route today (100 a UTC day, its keys together). Try again tomorrow. | ### Claims: the sandbox-checks routes On [Claims](https://payerworks.com/docs/claims). | Code | Status | What it means | | --- | --- | --- | | `invalid_seed` | 400 | seed is your own string for the run, 1 to 64 of A-Z, a-z, 0-9, _ and -: the same seed, claims and start give the same run. | | `invalid_instant` | 400 | start and as_of are moments written in ISO 8601 with a time and a zone (such as 2026-10-19T09:00:00Z), from the year 2000 to 2099, and as_of is not before start. | | `invalid_timeline_cursor` | 400 | cursor is the next_cursor an earlier timeline answer gave, unchanged. | | `cursor_other_run` | 400 | This cursor belongs to another timeline run: the claims, the seed or the start differ from the run that made it. | | `invalid_timeline_limit` | 400 | limit is a whole number from 1 to 500. | | `invalid_format` | 400 | format is "x12", "report" or "both". | | `timeline_not_offered` | 501 | The sandbox timeline is not offered where this was asked: it is not connected to the claim checks here, so nothing was run. This is not a passing fault, so sending the request again answers the same. | | `no_rules` | 400 | Send the rules as `rules`, a list of one or more rule objects. | | `too_many_rules` | 400 | At most 100 rules a call. | | `invalid_rules` | 422 | One or more rules are not written in the rule format; nothing was applied. Each rule's problems are listed. | | `no_mrf` | 400 | Send the file's text as `csv` (the tall or wide CSV template) or the file as `json` (an object, or its text). | | `mrf_too_large` | 413 | The file is longer than 1990000 characters, or holds more than 50000 rows or 50000 items: check it in pieces, the first three rows sent again with each piece and row_offset set. | | `mrf_not_json` | 422 | `json` is not a JSON document that can be read. | | `invalid_row_offset` | 400 | row_offset is a whole number from 0 to 1000000000: the rows of the whole file before the first data row of this piece. | ### Events and webhooks: the platform routes On [Events and webhooks](https://payerworks.com/docs/events). | Code | Status | What it means | | --- | --- | --- | | `invalid_destination_id` | 400 | A destination id is wd_ followed by 16 lowercase hexadecimal characters. | | `destination_not_found` | 404 | This key has no webhook destination with that id. | | `destination_limit_reached` | 409 | A key holds at most 10 webhook destinations. Remove one to add another. | | `invalid_destination_name` | 400 | A destination's name is 1 to 80 characters. | | `invalid_event_types` | 400 | event_types is a list of the event types the developers page lists; an empty list, or leaving it out, receives every type. | | `invalid_enabled` | 400 | enabled is true or false, or left out. | | `invalid_concurrency` | 400 | concurrency is a whole number from 1 to 10: how many deliveries this destination takes at once. | | `invalid_overlap_hours` | 400 | overlap_hours is a whole number from 0 to 720: how long the old secret is still accepted after a rotation. | | `destination_disabled` | 409 | This destination is disabled. Enable it with PUT /v1/webhook-destinations/{id} and `enabled: true` first. | | `destination_conflict` | 409 | The destination changed while this request ran, so nothing was saved. Send the request again. | | `not_at_destination` | 404 | That event was never sent to this destination (it was made before the destination was added, or its type is filtered out). A ping or a new event will be. | | `invalid_contact_email` | 400 | email is an address of at most 254 characters, or null to remove it. | | `contact_not_proved` | 403 | Notices go only to an address Payerworks has proved: the one that asked for this account's test key by email and opened the link we sent. Send that address, or null to stop the notices. To use another, write to us from it. | | `invalid_key_id` | 400 | A key id is pwk_ followed by 16 to 40 letters and digits. | | `key_not_found` | 404 | This account has no key with that id. | | `key_limit_reached` | 409 | An account holds at most 3 test keys or 25 live keys that are not revoked. Revoke one to make another. | | `invalid_key_name` | 400 | A key's name is 1 to 80 characters. | | `invalid_expires_in_days` | 400 | expires_in_days is a whole number from 1 to 365, or left out for a key that does not expire. | | `key_revoked` | 409 | That key is revoked, so it cannot be rotated. Make a new key with POST /v1/keys. | | `key_rotation_conflict` | 409 | Another rotation of this key finished while this one ran, so this one made nothing: the secret that other request showed is the key's current one. List the keys with GET /v1/keys, and rotate again if that secret was lost. | | `key_settings_out_of_range` | 409 | The key's stored rate or account name is outside what a key may hold, so nothing was made or rotated; reason says which. Ask Payerworks to correct the key: sending the request again answers the same. | ### SFTP file drop: the sftp routes On [SFTP file drop](https://payerworks.com/docs/sftp). | Code | Status | What it means | | --- | --- | --- | | `sftp_not_open` | 403 | The SFTP file drop is not open on this Payerworks deployment. Every API route still answers. | | `sftp_user_not_found` | 404 | This key has no SFTP user. Make one with PUT /v1/sftp. | | `invalid_acknowledgment_999` | 400 | acknowledgment_999 is "always", "errors_only", "never": when a 999 is written for a file you drop. | | `unknown_sftp_field` | 400 | PUT /v1/sftp takes acknowledgment_999 and nothing else. An SFTP user signs in with an SSH key only: add one with POST /v1/sftp/ssh-keys. | | `invalid_ssh_public_key` | 400 | public_key is one OpenSSH public key line (ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521), as the .pub file holds it. | | `private_key_sent` | 400 | That is a PRIVATE key. Nothing was kept: send the matching public key (the .pub file), and keep the private key on your own machine. | | `ssh_key_limit_reached` | 409 | An SFTP user holds at most 10 SSH keys. Remove one to add another. | | `ssh_key_exists` | 409 | This SFTP user already holds that SSH key. | | `ssh_key_refused` | 422 | The SFTP endpoint refused that SSH key, so nothing was added. Send another public key; an ssh-ed25519 key is the simplest. | | `invalid_ssh_key_id` | 400 | An SSH key id is key- followed by 17 lowercase hexadecimal characters, as GET /v1/sftp lists it. | | `ssh_key_not_found` | 404 | This SFTP user holds no SSH key with that id. | | `sftp_unavailable` | 503 | The SFTP user could not be read or changed just now, so nothing changed. Send the request again. | ### Batch jobs: the jobs routes On [Batch jobs](https://payerworks.com/docs/jobs). | Code | Status | What it means | | --- | --- | --- | | `jobs_not_offered` | 501 | Batch jobs are not offered on this host: Payerworks turns them on when they are ready. This is not a passing fault, so sending the request again answers the same. | | `jobs_live_not_offered` | 501 | Batch jobs answer test keys only for now, because a job keeps the request and its result for a while: use a test key, with invented data. | | `invalid_job_route` | 400 | route names the one route the job runs, as "POST /v1/claims/validate". A job runs: POST /v1/claims/validate, POST /v1/claims/837p, POST /v1/remittances/explain, POST /v1/eligibility/explain, POST /v1/claim-status/explain, POST /v1/analytics/summary, POST /v1/remittances/variance, POST /v1/claims/track, POST /v1/eligibility/270. | | `invalid_job` | 400 | A job is { route, query? }: route the one route it runs, query the query string every call carries (no leading ?), or left out. | | `invalid_job_id` | 400 | A job id is job_ and 24 letters or digits, as POST /v1/jobs answered it. | | `job_not_found` | 404 | No job of this key has that id. A finished job is kept 24 hours. | | `job_not_cancellable` | 409 | This job has already finished, so there is nothing to cancel: DELETE it to forget it now. | | `job_running` | 409 | This job is running: cancel it first (POST /v1/jobs/{id}/cancel), then delete it once it says canceled. | | `jobs_daily_limit` | 429 | This account's keys have made the most jobs an account may make today (50 a UTC day, its keys together). Try again tomorrow. | ### Documents and attachments: the attachment-requests routes On [Documents and attachments](https://payerworks.com/docs/documents). | Code | Status | What it means | | --- | --- | --- | | `no_information_request` | 400 | Send the payer's 277 request for additional information as x12 (the file's text). | | `not_an_information_request` | 422 | This file is not a 277 request for additional information (005010X213): the error names what it is, and the route that reads it. | | `no_requested_claims` | 422 | This request holds no claim (no TRN in a patient loop), so there is nothing to say or check. | | `invalid_information_request_call` | 400 | A field of the request is not in the shape this route takes; each problem is listed with its field. | | `no_attachment_response` | 400 | Send the solicited 275 to check as x12_275: the file's text, or a list of them. | | `not_an_attachment_response` | 422 | A file sent as x12_275 is not a 275 this route can read: the error names the file and why. | ### Keys and your account: the audit-log routes On [Keys and your account](https://payerworks.com/docs/keys). | Code | Status | What it means | | --- | --- | --- | | `invalid_audit_type` | 400 | type is one of the audit log's entry types, or the part before its dot (key, webhook, enrollment...), as the developers page lists them. | | `invalid_audit_cursor` | 400 | starting_after is the next_cursor of an earlier page of this audit log. | ### Organizations and members: the org routes On [Organizations and members](https://payerworks.com/docs/organizations). | Code | Status | What it means | | --- | --- | --- | | `org_not_offered` | 501 | Organizations and member sign-in are not open on this deployment yet. Keys still work as before. | | `org_session_required` | 401 | This route needs a member's console sign-in, not an API key. Sign in at payerworks.com/console. | | `org_session_invalid` | 401 | The sign-in could not be checked, or it has ended. Sign in again. | | `org_session_not_accepted` | 403 | A member's sign-in manages an account; it does not make this call. Call this route with an API key. | | `org_email_not_verified` | 403 | Confirm this sign-in's email address first: the code was emailed when the sign-in was made. | | `org_choose` | 400 | This sign-in belongs to several organizations: name one in the Payerworks-Organization header (GET /v1/org/memberships lists them). | | `org_not_found` | 404 | No organization with that id has this sign-in as a member. | | `org_not_a_member` | 403 | This sign-in belongs to no organization yet. Make one with POST /v1/org, or open an invitation's link. | | `org_role_forbidden` | 403 | Your role in this organization does not allow that. GET /v1/org/roles says which role does. | | `org_owner_only` | 403 | Only an owner may do that: invite or make an owner, change or remove an owner, revoke a removed member's keys, or change the organization's settings. | | `org_last_owner` | 409 | An organization always keeps at least one owner. Make another member an owner first. | | `org_member_not_found` | 404 | This organization has no member with that id. | | `invalid_org_member_id` | 400 | A member id is pwm_ followed by 16 lowercase hexadecimal characters. | | `org_invitation_not_found` | 404 | No pending invitation matches. It may have been accepted or canceled; ask for a new one. | | `invalid_org_invitation_id` | 400 | An invitation id is pwi_ followed by 16 lowercase hexadecimal characters. | | `org_invitation_expired` | 410 | An invitation's link works for 7 days. Ask a member of the organization for a new one. | | `org_invitation_email_mismatch` | 403 | This invitation was sent to another address. Sign in with the address the email reached. | | `org_already_member` | 409 | That person is already a member of this organization. | | `org_invitation_pending` | 409 | An invitation to that address is already waiting. Cancel it to send another. | | `org_account_taken` | 409 | That key's account already belongs to an organization. Ask one of its owners to invite you. | | `org_key_not_usable` | 403 | The api_key given is not a usable key (unknown, revoked or expired), so it proves no account. | | `org_limit_reached` | 409 | An organization holds at most 200 members and 50 pending invitations, and a sign-in belongs to at most 20 organizations. | | `org_invitations_daily_limit` | 429 | An organization sends at most 25 invitations a UTC day. Send more tomorrow. | | `invalid_org_request` | 400 | The request is not what this route takes: errors names each field and why. | | `org_acting_key_required` | 400 | Name the key this change is for in the Payerworks-Key header (its key id; GET /v1/org/keys lists them). | | `org_acting_key_not_found` | 404 | The organization's account has no usable key with that id. | | `keys_managed_by_members` | 403 | This account's organization chose that only its signed-in members manage keys. Make, rotate or revoke the key at payerworks.com/console. | | `org_changed_meanwhile` | 409 | The organization changed while this request was made. Read it again and send the change again. | ### Claims: the submit routes On [Claims](https://payerworks.com/docs/claims). | Code | Status | What it means | | --- | --- | --- | | `submit_not_offered` | 501 | Payerworks does not send this claim yet: `gates` names each gate and whether it is open. Nothing was written or sent; write the 837P with POST /v1/claims/837p and send it on the connection you have. | | `submit_envelope_assigned` | 400 | On a submission Payerworks sets the interchange: leave out sender_id, receiver_id, the two qualifiers, control_number and usage. | | `invalid_partner_id` | 400 | partner_id is a partner's id as GET /v1/partners lists it, or left out to send to the one partner taking the claims' payer. | | `reference_prefix_required` | 422 | Every claim's patientControlNumber must begin with your account's reference prefix, so each answer can be matched to you alone; the error names the prefix and the claims. | | `payer_not_at_partner` | 422 | A claim names a payer this partner does not take claims for; send it in a call of its own to the partner that does. | | `reference_in_flight` | 409 | A claim with this patientControlNumber was already sent to this partner and has not been rejected; a corrected claim (frequency 7 or 8) may reuse it. | | `submit_not_sent` | 503 | The file was written but the partner's connection did not take it just now, so nothing was sent (`transmitted: false`). Send the call again. | ### Every route: keys, limits, bodies and events | Code | Status | What it means | | --- | --- | --- | | `not_found` | 404 | No such route. The routes are listed at payerworks.com/developers. | | `method_not_allowed` | 405 | This route does not answer that method; the allow header names the ones it does. | | `no_key` | 401 | Send your API key as `Authorization: Bearer `. | | `invalid_key` | 401 | That key is not valid. Keys are issued by Payerworks; a revoked key stops within a minute. | | `rate_limited` | 429 | Over this key's calls a minute. Wait the seconds retry-after names. | | `too_large` | 413 | A request body may be at most 2 MB. | | `invalid_json` | 400 | The body must be a JSON object. | | `invalid_idempotency_key` | 400 | An Idempotency-Key is 1 to 255 visible characters, with no spaces, quotes or backslashes. A UUID is a good one. | | `idempotency_key_reused` | 422 | This Idempotency-Key was used in the last 24 hours for a different request. Send a new key for a new request. | | `idempotency_in_flight` | 409 | The first request with this Idempotency-Key is still running. Retry after the seconds retry-after names. | | `idempotency_superseded` | 409 | What this request set has since been changed or removed, so its answer cannot be given again. Send a new request with a new Idempotency-Key. | | `internal` | 500 | Something failed on our side. Retry; if it keeps failing, write to us with the request_id. | | `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. | | `queue_unavailable` | 503 | We could not queue the events for delivery just now. Retry in a moment. | | `invalid_webhook_url` | 400 | A webhook address is https, at a public hostname, on port 443, with no login, at most 500 characters. | | `invalid_rotate` | 400 | rotate is true or false, or left out. | | `webhook_conflict` | 409 | The webhook changed while this request ran, so nothing was saved. Send the request again. | | `no_webhook` | 409 | This key has no webhook yet: set one with PUT /v1/account/webhook first. | | `too_many_events` | 413 | This run made more events than one delivery carries, so none were queued. Send fewer claims a call. | | `webhook_rate_limited` | 429 | Over this key's webhook deliveries a minute, so nothing was queued. Wait for the next minute. | | `event_limit_reached` | 429 | This account's keys have recorded the 2,000 events an account keeps in a UTC day, so no more are recorded or sent this UTC day. Events are recorded again from 00:00 UTC; an event refused before then is never sent by itself. | # Compatibility and deprecation > What we promise about /v1, and the parts of that promise the code itself checks on every change. Page: https://payerworks.com/docs/compatibility · Markdown: https://payerworks.com/docs/compatibility.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Every route is under `/v1`. Within it, we change the API only in ways an integration written against it keeps working with, and when that cannot hold for a route, we say so in advance and give you at least 180 days. ## Changes that never break an integration - A new route. - A new optional field in a request, or a new value an optional field may take. - A new field in an answer, or a new value in a list of values an answer may hold (a new event type, a new state, a new verdict). - A new error code, for a refusal that had none. - A sentence in an answer’s words, a message or a note, worded better. Read the codes and fields, never the sentences. Build for these: ignore an answer field you do not know, treat an error code you do not know by its HTTP status, and treat an unknown value in a list as unknown rather than failing. ## What counts as breaking - A route removed or its path changed. - A field removed from an answer, renamed, or given another type or meaning. - A request field that was optional made required, or a value it took refused. - An error code answered with another HTTP status, or used for another meaning. - A refusal for a request that was answered before, when the request was right. A refusal of a request that was never right (a claim that breaks a published rule a check did not run before) is a correction, said in the changelog with what changed, never quietly. A refusal an account chooses for itself is its own setting, never a change to the API: since 10/20/2026 an organization’s owner may choose that only signed-in members make, rotate and revoke the account’s keys, and then those three key routes refuse the account’s keys with keys_managed_by_members, until an owner switches it back. ## How a deprecation is announced - A route to be removed is first listed as deprecated: in [/openapi.json](https://payerworks.com/openapi.json) its operation says `deprecated: true` with the day it ends, and the changelog says why and what to use instead. - From that day, every answer of the route carries the Deprecation header (RFC 9745), the Sunset header with the day it stops (RFC 8594), and a Link to this page. - The route keeps answering for at least 180 days after its deprecation is announced. - A change that cannot be made by adding ships under a new prefix, beside `/v1`, never in place. ## What the code checks What the code checks on every change: every route `/v1` served on 10/20/2026 is still served unless it was deprecated and its day has passed, and every error code it answered on that day keeps its HTTP status. Nothing is deprecated today. ## Deprecated now Nothing is deprecated. Every change, breaking or not, is on the [Changelog](https://payerworks.com/docs/changelog). # Changelog > One entry per dated change, newest first, in one fixed form. Also as a feed. Page: https://payerworks.com/docs/changelog · Markdown: https://payerworks.com/docs/changelog.md > Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient. Newest first. Also as an Atom feed: [/docs/changelog.atom](https://payerworks.com/docs/changelog.atom). What may change without notice, and how much notice a removal gets, is the [compatibility policy](https://payerworks.com/docs/compatibility). ## 10/20/2026: Organizations, members and roles, off until Payerworks turns them on What shipped: - An organization for an account: its members, each with a role (owner, admin, developer or viewer), invitations by email that work for 7 days, and one role table every organization shares (GET /v1/org/roles). - A member signs in at payerworks.com/console with a password and a code from an authenticator app, always, and manages the account’s keys, webhooks, enrollments, SFTP user and notice address as far as the role allows. A sign-in never makes a call that carries data: claims, eligibility, files, sandboxes, MCP and batch jobs take a key. - An owner may choose that only signed-in members make, rotate and revoke the account’s keys (key_management: members_only). Before: Every key of an account managed every other, and there were no members, roles or sign-in. Now: Keys still call the API. People manage the account through the console, each within a role, and the last owner of an organization is never removed or demoted. Sandbox support: Off until Payerworks turns organizations on: until then nobody can sign in, a sign-in’s token is answered 501 org_not_offered, and a key is answered 401 org_session_required on every route but GET /v1/org/roles, which answers any key. Existing integrations: None: thirteen new routes. Every key route answers as before unless an owner chooses members_only, and then only POST /v1/keys, DELETE /v1/keys/{id} and POST /v1/keys/{id}/rotate refuse a key, with keys_managed_by_members. Routes: [GET /v1/org/roles](https://payerworks.com/docs/organizations#get-v1-org-roles), [GET /v1/org/memberships](https://payerworks.com/docs/organizations#get-v1-org-memberships), [POST /v1/org](https://payerworks.com/docs/organizations#post-v1-org), [GET /v1/org](https://payerworks.com/docs/organizations#get-v1-org), [PUT /v1/org](https://payerworks.com/docs/organizations#put-v1-org), [GET /v1/org/members](https://payerworks.com/docs/organizations#get-v1-org-members), [PUT /v1/org/members/{id}](https://payerworks.com/docs/organizations#put-v1-org-members-id), [DELETE /v1/org/members/{id}](https://payerworks.com/docs/organizations#delete-v1-org-members-id), [GET /v1/org/invitations](https://payerworks.com/docs/organizations#get-v1-org-invitations), [POST /v1/org/invitations](https://payerworks.com/docs/organizations#post-v1-org-invitations), [DELETE /v1/org/invitations/{id}](https://payerworks.com/docs/organizations#delete-v1-org-invitations-id), [POST /v1/org/join](https://payerworks.com/docs/organizations#post-v1-org-join), [GET /v1/org/keys](https://payerworks.com/docs/organizations#get-v1-org-keys). ## 10/20/2026: The account’s audit log What shipped: - GET /v1/audit-log lists every change to the account, newest first: keys, the webhook and its destinations, the notice address, providers, enrollment requests, tasks and documents, the SFTP user and its SSH keys, batch jobs, the organization and its members. - Each entry names who made the change (a key, a member, Payerworks, or Payerworks automatically), when, through which door, the object, and the fields that changed, by name only and never a value. - GET /v1/audit-log/export gives the same entries as a CSV file, at most 5,000 a call. Before: Nothing on the account said who changed what, or when. Now: Every key of the account, and every member with the role for it, reads one history of the account, each entry kept 365 days and never changed before then. Sandbox support: Test keys and live keys alike: every change a test key makes is recorded on its own account. Existing integrations: None: two new routes that only read. Routes: [GET /v1/audit-log](https://payerworks.com/docs/keys#get-v1-audit-log), [GET /v1/audit-log/export](https://payerworks.com/docs/keys#get-v1-audit-log-export). ## 10/20/2026: Sending claims to a payer, not offered while Payerworks is connected to no payer What shipped: - POST /v1/claims/submit answers 501 submit_not_offered with each of its five gates and whether it is open: sending switched on, a live key, a signed business associate agreement recorded for the account, a trading partner for the payer, and that partner switched on. Nothing is written or sent. - GET /v1/partners lists the trading partners a submission may go to, and is empty today. - Three event types for a live key’s own sends, made only from a partner’s answer to that key’s file: submission.file.acknowledged, submission.claim.acknowledged and submission.remittance.received. Before: There was no route for a live key to send a claim, and every route that writes a file said transmitted: false. Now: The route is there and shut. Every route that writes a file still says transmitted: false, and Payerworks is connected to no payer. Sandbox support: None: run the whole loop with POST /v1/sandbox/claims, which answers a 999, a 277CA and an 835 for invented claims. Existing integrations: None: two new routes, and three event types no key receives until it sends through a trading partner. Routes: [POST /v1/claims/submit](https://payerworks.com/docs/claims#post-v1-claims-submit), [GET /v1/partners](https://payerworks.com/docs/claims#get-v1-partners). ## 10/20/2026: Batch jobs, off until Payerworks turns them on What shipped: - A batch job runs up to 1,000 calls to one route from one file you upload to a signed form (50 MB at most), off the API’s ten-second clock, and its result is a file fetched by a signed link for 24 hours. - A job runs claim validation, 837P writing, the remittance, eligibility and claim status explanations, analytics, payment variance and claim tracking; POST /v1/eligibility/270 as a job writes one 270 of up to 7,500 questions. - job.completed and job.failed events, through the account webhook and the webhook destinations. - uploadJobFile in the JavaScript client. Before: Every call had to fit one request: 2 MB and ten seconds. Now: The same routes, many calls at a time. A job is the one part of the API that keeps what you send for a while: the file until the job finishes and the result for 24 hours, in storage of its own that keeps no earlier copy and is in no backup. Test keys only, 50 jobs a UTC day for an account’s keys together. Sandbox support: Off until Payerworks turns batch jobs on: until then every jobs route answers 501 jobs_not_offered. Then for test keys only, with invented data. Existing integrations: None: five new routes, and two new event types a webhook receives only once it makes a job. Routes: [POST /v1/jobs](https://payerworks.com/docs/jobs#post-v1-jobs), [GET /v1/jobs](https://payerworks.com/docs/jobs#get-v1-jobs), [GET /v1/jobs/{id}](https://payerworks.com/docs/jobs#get-v1-jobs-id), [POST /v1/jobs/{id}/cancel](https://payerworks.com/docs/jobs#post-v1-jobs-id-cancel), [DELETE /v1/jobs/{id}](https://payerworks.com/docs/jobs#delete-v1-jobs-id). ## 10/20/2026: A payer’s request for more information, and the 275 that answers it What shipped: - POST /v1/attachments/requests/explain reads a 277 request for additional information (005010X213) claim by claim: each LOINC code asked for, about which claim and line, by when, and the solicited 275 that answers it. - POST /v1/attachments/requests/check holds a solicited 275 to the request it answers before you send it. - POST /v1/attachments/275 writes a solicited 275 from those values: the request’s REF*EJ and submitter id, the payer’s response contact, and each attachment echoing its own requested code and service line. - Claim tracking counts a request no 275 answers as information requested, with its due day, and takes your solicited 275s as attachments. Before: A 275 could be written unsolicited, or under a trace you typed, and nothing read the payer’s request or said whether a claim was waiting on you. Now: The request is read and its answer is laid out, written and checked, all as public sources show these files, and every answer says so. Nothing is stored or sent. Sandbox support: In the claims sandboxes (837P, 837I and 837D), a member ID ending in RFAI is held for a document: a 277 request for additional information follows the 277CA, and the same claims sent again with the solicited 275 (attachments_275) are paid. Existing integrations: Additive: every new key appears only when a request or a 275 is in the call, and every earlier answer is the same, byte for byte. Routes: [POST /v1/attachments/requests/explain](https://payerworks.com/docs/documents#post-v1-attachments-requests-explain), [POST /v1/attachments/requests/check](https://payerworks.com/docs/documents#post-v1-attachments-requests-check), [POST /v1/attachments/275](https://payerworks.com/docs/documents#post-v1-attachments-275), [POST /v1/claims/track](https://payerworks.com/docs/acknowledgments#post-v1-claims-track), [POST /v1/sandbox/claims](https://payerworks.com/docs/claims#post-v1-sandbox-claims), [POST /v1/sandbox/claims/837i](https://payerworks.com/docs/claims#post-v1-sandbox-claims-837i), [POST /v1/sandbox/claims/837d](https://payerworks.com/docs/claims#post-v1-sandbox-claims-837d). ## 10/20/2026: The picture reads get 25 seconds What shipped: - The two picture reads run on a function of their own, with 25 seconds to read under the gateway’s 29. Before: A read had 8 seconds inside the API’s own ten, and the more accurate reader often missed it. Now: A read has 25 seconds; past them the answer is still 504 model_read_timed_out. Every other rule is the same: off until Payerworks turns a read on, test keys only, 100 reads a UTC day for an account’s keys together, no Idempotency-Key, nothing kept. Sandbox support: Off until Payerworks turns a read on, as before. Existing integrations: None: the same routes, answers and codes. Routes: [POST /v1/remittances/from-image](https://payerworks.com/docs/documents#post-v1-remittances-from-image), [POST /v1/payers/from-card](https://payerworks.com/docs/documents#post-v1-payers-from-card). ## 10/20/2026: A status page, published limits and a page per payer What shipped: - payerworks.com/status: whether the API, the sandbox, webhook delivery and the site answered, measured every five minutes from outside, with this calendar month’s counts and any incident, and an Atom feed of incidents. No payer is a part of it: Payerworks is connected to none. - payerworks.com/limits, and /payerworks/limits.json: every route family’s limits, each read from the constant the API refuses by when the page is built. - A page for each of the payer directory’s rows, linked from its letter page. - /.well-known/security.txt and a disclosure section on the security page; reports come through the one form and are answered by email. Before: None of these pages existed. Now: A measurement is said only with how many there were, for the month to date; a part not measured yet says so, never up. Sandbox support: The sandbox is one of the parts the status page measures, with one invented claim under Payerworks’ own test key. Existing integrations: None: no route changed. ## 10/20/2026: Docs by job, a changelog and a compatibility policy What shipped: - payerworks.com/docs: one page per job, each with the request, how to read the answer, troubleshooting and the code lists; five quickstarts; a page for coding agents; every error; this changelog and its feed. - Every docs page as markdown at /docs/.md, a docs llms.txt and the whole set as /llms-full.txt, and a Copy for agent button on each page. - A runnable example file for every family of routes, each run against the API in Payerworks’ own tests. - A written compatibility and deprecation policy for /v1. Before: One developers page listed every route, and /openapi.json held the contract. Now: The same routes, documented by job and as markdown, with examples you can run. Running them found five places where /openapi.json did not fit the API’s real answers, now corrected: claim tracking takes a claim with no reference, a timeline day and a claim’s paid total (in dollars and in cents) may be null, and GET /v1/sftp names its 403, as the two picture reads now do too. Sandbox support: Every quickstart runs on the sandbox payers with a test key. Existing integrations: None: no route changed. A client generated from /openapi.json may need regenerating to accept the nulls the API already answered. ## 10/20/2026: The platform and the documents What shipped: - Webhook destinations, up to ten a key, each with its own event types and long retries (seven attempts over about three days), its deliveries and every attempt listed. - An account’s keys: named keys, expiry, revocation, and rotation that keeps the key id; a notice address. - The MCP server hosted by the API itself at POST /v1/mcp. - The enrollment import, export and durations; the SFTP file drop where it is open. - PDFs drawn from what you send: the CMS-1500, a remittance, an eligibility answer, and Medicare’s two appeal forms filled in; a claim’s attachments as an X12 275. - Two picture reads (a paper EOB into a draft 835, a card into the payer and member ID), built and off until Payerworks turns them on. - Your own claim rules, a hospital standard charges file check, and the claims sandbox over simulated days. Before: One webhook a key, keys made only by the signup page or by Payerworks, the MCP server as a file to run yourself, and no documents. Now: 34 more routes, every one answered by the API itself and none in the browser’s engine. A route switched off answers 501 with a code ending in _not_offered, which the clients never retry. Sandbox support: The claims sandbox over simulated days (POST /v1/sandbox/claims/timeline). The documents run on any test file. The picture reads, when on, answer test keys only, with invented documents. Existing integrations: Additive. A claim that announces an attachment sent electronically (PWK02 EL) now carries a warning on validate naming the 275 that must follow; it is still valid. Routes: [GET /v1/webhook-destinations](https://payerworks.com/docs/events#get-v1-webhook-destinations), [POST /v1/keys](https://payerworks.com/docs/keys#post-v1-keys), [POST /v1/keys/{id}/rotate](https://payerworks.com/docs/keys#post-v1-keys-id-rotate), [POST /v1/mcp](https://payerworks.com/docs/keys#post-v1-mcp), [POST /v1/enrollments/import](https://payerworks.com/docs/enrollment#post-v1-enrollments-import), [GET /v1/sftp](https://payerworks.com/docs/sftp#get-v1-sftp), [POST /v1/claims/cms1500](https://payerworks.com/docs/documents#post-v1-claims-cms1500), [POST /v1/remittances/pdf](https://payerworks.com/docs/documents#post-v1-remittances-pdf), [POST /v1/eligibility/pdf](https://payerworks.com/docs/documents#post-v1-eligibility-pdf), [POST /v1/appeals/form](https://payerworks.com/docs/documents#post-v1-appeals-form), [POST /v1/attachments/275](https://payerworks.com/docs/documents#post-v1-attachments-275), [POST /v1/remittances/from-image](https://payerworks.com/docs/documents#post-v1-remittances-from-image), [POST /v1/payers/from-card](https://payerworks.com/docs/documents#post-v1-payers-from-card), [POST /v1/edits/rules/check](https://payerworks.com/docs/claims#post-v1-edits-rules-check), [POST /v1/price-transparency/validate](https://payerworks.com/docs/patient-money#post-v1-price-transparency-validate), [POST /v1/sandbox/claims/timeline](https://payerworks.com/docs/claims#post-v1-sandbox-claims-timeline). ## 10/19/2026: After the claim, eligibility tools, prior authorization and patient money What shipped: - Following up: when each open claim’s remittance is due, learned from your own payment history with how many paid claims it rests on. - Remittances after they arrive: duplicates by trace number, deposits matched to checks, an 835 split by payee or rendering provider, recoupments matched to the claims they take back, and published filing windows. - Analytics from your own files, every figure with its count and a floor before anything is called a pattern. - Eligibility tools: the next questions after a no-match, whether a payer answers a service type, which plan pays first, and whether a discovered coverage is the patient’s. - Prior authorization written as a 278 and its response explained, its sandbox, and Medicare’s published lists. - Patient estimates from a 271, a Good Faith Estimate, an equipment replacement window and the poverty guideline. Before: Claims could be written, tracked and explained, but nothing worked across many files after they were sent, and there was no 278. Now: 23 more routes. POST /v1/x12/parse names a 278 as its own kind. Sandbox support: The prior authorization sandbox (POST /v1/sandbox/authorizations), answering from the same payer as the eligibility sandbox. Existing integrations: Additive. A 270 that uses none of the new procedure fields is written exactly as before. Routes: [POST /v1/claims/follow-up](https://payerworks.com/docs/claim-status#post-v1-claims-follow-up), [POST /v1/remittances/duplicates](https://payerworks.com/docs/remittances#post-v1-remittances-duplicates), [POST /v1/remittances/reassociate](https://payerworks.com/docs/remittances#post-v1-remittances-reassociate), [POST /v1/remittances/split](https://payerworks.com/docs/remittances#post-v1-remittances-split), [POST /v1/remittances/recoupments](https://payerworks.com/docs/remittances#post-v1-remittances-recoupments), [GET /v1/filing-limits](https://payerworks.com/docs/remittances#get-v1-filing-limits), [POST /v1/analytics/summary](https://payerworks.com/docs/analytics#post-v1-analytics-summary), [POST /v1/remittances/variance](https://payerworks.com/docs/analytics#post-v1-remittances-variance), [POST /v1/eligibility/identity-retry](https://payerworks.com/docs/eligibility#post-v1-eligibility-identity-retry), [POST /v1/eligibility/coordination](https://payerworks.com/docs/eligibility#post-v1-eligibility-coordination), [POST /v1/authorizations/278](https://payerworks.com/docs/prior-authorization#post-v1-authorizations-278), [POST /v1/authorizations/explain](https://payerworks.com/docs/prior-authorization#post-v1-authorizations-explain), [POST /v1/estimates](https://payerworks.com/docs/patient-money#post-v1-estimates), [POST /v1/estimates/good-faith](https://payerworks.com/docs/patient-money#post-v1-estimates-good-faith). ## 10/19/2026: Claims written right, and the payer directory What shipped: - One published edit table, run by every claim route, with formatting repairs made on a copy and listed, and every check that could not run said with why. - Secondary and tertiary claims with their coordination of benefits, corrected and void claims planned, and every 837 read back to JSON. - The 837I and 837D writers and readers, with their sandboxes. - The payer directory: payers and plan programs from public federal files, with a payer ID only where the payer publishes it. Before: A claim was checked by a shorter list of rules, and the API wrote professional claims only. Now: Every claim route runs the same checks from one table, and the file is written from the repaired copy. Sandbox support: The 837I and 837D sandboxes (POST /v1/sandbox/claims/837i and /837d). Existing integrations: A claim that validated before may now be refused: the deeper checks require what the guides require, such as the submitter’s contact and the patient’s address. In a written 837P, PWK02 is now BM rather than AA, and REF*9F is written before REF*G1; a primary claim is otherwise written byte for byte as before. Routes: [GET /v1/edits](https://payerworks.com/docs/claims#get-v1-edits), [POST /v1/claims/837i](https://payerworks.com/docs/claims#post-v1-claims-837i), [POST /v1/claims/837d](https://payerworks.com/docs/claims#post-v1-claims-837d), [POST /v1/claims/correction-plan](https://payerworks.com/docs/claims#post-v1-claims-correction-plan), [GET /v1/payers](https://payerworks.com/docs/payers#get-v1-payers). ## 10/19/2026: Eligibility, claim status, denials, durable events and the SDKs What shipped: - The 270 written and every 271 explained; the 276 written and every 277 explained; each denial as the next step, and appeal letters from your facts. - Events recorded before they are sent, kept 30 days, listed and sent again; the Standard Webhooks headers beside Payerworks-Signature; Idempotency-Key on writes. - Claim tracking from the files you hold, and transaction enrollment with providers, requests, tasks and documents. - A JavaScript client, a Python client and an MCP server, generated from the route list. - One error catalog, and request ids in every refusal. Before: The API read 835s, 999s and 277CAs and wrote the 837P; an event was posted to the webhook, with one retry, and not kept. Now: Every core transaction is written or read, each in a sandbox. POST /v1/x12/parse answers a 277CA as kind 277CA, and refuses an interchange that holds two kinds of file. Sandbox support: The eligibility and claim status sandboxes, and the enrollment sandbox payer PWSANDBOX. Existing integrations: POST /v1/x12/parse answered a 277CA as kind 277 before this change. An interchange whose groups are of two kinds, which was read by its first group only, is now refused 422 mixed_functional_groups. Webhook receivers that check Payerworks-Signature keep working: the Standard Webhooks headers are added beside it. Routes: [POST /v1/eligibility/270](https://payerworks.com/docs/eligibility#post-v1-eligibility-270), [POST /v1/eligibility/explain](https://payerworks.com/docs/eligibility#post-v1-eligibility-explain), [POST /v1/claim-status/276](https://payerworks.com/docs/claim-status#post-v1-claim-status-276), [POST /v1/claim-status/explain](https://payerworks.com/docs/claim-status#post-v1-claim-status-explain), [POST /v1/denials/next-step](https://payerworks.com/docs/remittances#post-v1-denials-next-step), [POST /v1/appeals/letter](https://payerworks.com/docs/remittances#post-v1-appeals-letter), [GET /v1/events](https://payerworks.com/docs/events#get-v1-events), [POST /v1/claims/track](https://payerworks.com/docs/acknowledgments#post-v1-claims-track), [POST /v1/providers](https://payerworks.com/docs/enrollment#post-v1-providers), [POST /v1/enrollments](https://payerworks.com/docs/enrollment#post-v1-enrollments). ## 10/19/2026: Test keys by email, the claims sandbox, webhooks and the console What shipped: - A test key by email from the developers page, for invented data only. - The claims sandbox payer: a claim answered as a 999, a 277CA and an 835, by how its member ID ends. - Your account’s rate and its usage this month, and a signed webhook. - The console at payerworks.com/console. Before: A key was issued by Payerworks on request, and there was no sandbox. Now: A developer can get a test key and run a claim end to end alone. Sandbox support: The claims sandbox (POST /v1/sandbox/claims). Existing integrations: Additive. Routes: [POST /v1/sandbox/claims](https://payerworks.com/docs/claims#post-v1-sandbox-claims), [GET /v1/account](https://payerworks.com/docs/keys#get-v1-account), [PUT /v1/account/webhook](https://payerworks.com/docs/events#put-v1-account-webhook), [DELETE /v1/account/webhook](https://payerworks.com/docs/events#delete-v1-account-webhook), [POST /v1/account/webhook/test](https://payerworks.com/docs/events#post-v1-account-webhook-test). ## 10/18/2026: The Payerworks API What shipped: - Validate claims and write an 837P; read 835, 999 and 277CA files; explain a remittance and every acknowledgment rejection; codes in words. - Bearer keys, a rate per key, one error shape and a request id on every answer. Before: There was no API. Now: The engine as an API under /v1. It writes and explains files and transmits nothing. Sandbox support: None yet. Existing integrations: None: the first release. Routes: [GET /v1/health](https://payerworks.com/docs/keys#get-v1-health), [POST /v1/claims/validate](https://payerworks.com/docs/claims#post-v1-claims-validate), [POST /v1/claims/837p](https://payerworks.com/docs/claims#post-v1-claims-837p), [POST /v1/x12/parse](https://payerworks.com/docs/claims#post-v1-x12-parse), [POST /v1/remittances/explain](https://payerworks.com/docs/remittances#post-v1-remittances-explain), [POST /v1/acknowledgments/explain](https://payerworks.com/docs/acknowledgments#post-v1-acknowledgments-explain), [GET /v1/codes/carc/{code}](https://payerworks.com/docs/remittances#get-v1-codes-carc-code), [GET /v1/codes/claim-status/{category}](https://payerworks.com/docs/claim-status#get-v1-codes-claim-status-category).