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/jobsMake a batch job - GET
/v1/jobsYour batch jobs - GET
/v1/jobs/{id}One batch job - POST
/v1/jobs/{id}/cancelCancel a batch job - 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/jobs" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postJobs(body);With the Python client
answer = pw.post_jobs(body){
"route": "POST /v1/claims/validate"
}{
"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.
| 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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/jobs" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getJobs();With the Python client
answer = pw.get_jobs(){
"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.
| 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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getJobsById("job_9e6d21650c98b40f5b5848da");With the Python client
answer = pw.get_jobs_by_id("job_9e6d21650c98b40f5b5848da"){
"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.
| 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da/cancel" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.postJobsByIdCancel("job_9e6d21650c98b40f5b5848da");With the Python client
answer = pw.post_jobs_by_id_cancel("job_9e6d21650c98b40f5b5848da"){
"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.
| 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
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/jobs/job_9e6d21650c98b40f5b5848da" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.deleteJobsById("job_9e6d21650c98b40f5b5848da");With the Python client
answer = pw.delete_jobs_by_id("job_9e6d21650c98b40f5b5848da"){
"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.
| 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
statusis one of awaiting_upload, running, completed, partial, failed, canceled or expired, with itsstatus_words.uploadis the signed form while the upload window is open; post every field as given, then the file underfile_field. Once it closes, a job still awaiting_upload says so in itsstatus_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,
callscounts the answers by status andresult.urlis a link good for 15 minutes; ask again for a new one. The result is{ answers: [ { index, status, body } ] }, eachbodywhat that call would have answered. - A job that ran out of time is
partial: the answered calls are in the result, andnot_run_fromis the first call to send again. A failed job says why inerror.code. - A
job.completedorjob.failedevent 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.
| 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": [ <body>, ... ] }, 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 runs every step below against the API with a test key. It needs payerworks.mjs and sample-data.mjs beside it.
// 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=<the base URL shown with your key> 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 <this file>, 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));
}
{
"offered": false,
"refused": {
"status": 501,
"code": "jobs_not_offered",
"request_id": "req_1"
}
}