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.
Routes
- POST
/v1/eligibility/270Write a 270 - POST
/v1/eligibility/explainExplain an eligibility answer - POST
/v1/sandbox/eligibilityRun the eligibility sandbox - GET
/v1/codes/service-type/{code}A service type code - GET
/v1/codes/aaa/{code}A reject reason code - POST
/v1/eligibility/identity-retryFind the patient again - POST
/v1/eligibility/service-type-testDoes a payer answer a service type - POST
/v1/eligibility/coordinationWhich plan pays first - POST
/v1/eligibility/discovery/verifyIs 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/270" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postEligibility270(body);With the Python client
answer = pw.post_eligibility_270(body){
"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"
}
}{
"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.
| 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/explain" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postEligibilityExplain(body);With the Python client
answer = pw.post_eligibility_explain(body){
"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"
}
}{
"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.
| 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/sandbox/eligibility" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postSandboxEligibility(body);With the Python client
answer = pw.post_sandbox_eligibility(body){
"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"
}
}{
"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.
| 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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/service-type/98" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getCodesServiceTypeByCode("98");With the Python client
answer = pw.get_codes_service_type_by_code("98"){
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/codes/aaa/72" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getCodesAaaByCode("72");With the Python client
answer = pw.get_codes_aaa_by_code("72"){
"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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/identity-retry" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postEligibilityIdentityRetry(body);With the Python client
answer = pw.post_eligibility_identity_retry(body){
"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"
}
}{
"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.
| 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
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.jsonWith the JavaScript client
const answer = await pw.postEligibilityServiceTypeTest(body);With the Python client
answer = pw.post_eligibility_service_type_test(body){
"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"
}{
"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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/coordination" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postEligibilityCoordination(body);With the Python client
answer = pw.post_eligibility_coordination(body){
"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
}
]
}{
"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.
| 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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/eligibility/discovery/verify" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postEligibilityDiscoveryVerify(body);With the Python client
answer = pw.post_eligibility_discovery_verify(body){
"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"
}{
"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.
| Field | Type | What it is |
|---|---|---|
results | list of object | |
summary | object | |
notes | list of string | |
scope | string |
Reading the answer
- Read
members[].coverage.statefirst: active, inactive or not stated, for the day inasked. A figure the payer did not state isnull, never 0. - Each service type is under
services[], with its figures split byin_networkandout_of_network, thenindividualandfamily. 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_periodsorby_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.
| 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/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 runs every step below against the API with a test key. It needs payerworks.mjs and sample-data.mjs beside it.
// 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=<the base URL shown with your key> 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 <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));
}
{
"written": {
"transmitted": false,
"starts": "ISA"
},
"coverage": "active",
"not_found": "subscriber_not_found",
"refused": {
"status": 422,
"code": "invalid_requests",
"request_id": "req_5"
}
}