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.
Routes
- PUT
/v1/account/webhookSet your webhook - DELETE
/v1/account/webhookRemove your webhook - POST
/v1/account/webhook/testSend a test event - GET
/v1/eventsList your events - GET
/v1/events/{id}One event - POST
/v1/events/{id}/redeliverSend an event again - GET
/v1/events/{id}/attemptsEvery attempt for an event - GET
/v1/webhook-destinationsList your webhook destinations - POST
/v1/webhook-destinationsAdd a webhook destination - GET
/v1/webhook-destinations/{id}One webhook destination - PUT
/v1/webhook-destinations/{id}Change a webhook destination - DELETE
/v1/webhook-destinations/{id}Remove a webhook destination - POST
/v1/webhook-destinations/{id}/pingPing a webhook destination - GET
/v1/webhook-destinations/{id}/deliveriesWhat 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
curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/account/webhook" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.putAccountWebhook(body);With the Python client
answer = pw.put_account_webhook(body){
"url": "https://hooks.example.com/payerworks/events"
}{
"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
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/account/webhook" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.deleteAccountWebhook();With the Python client
answer = pw.delete_account_webhook(){
"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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/account/webhook/test" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.postAccountWebhookTest();With the Python client
answer = pw.post_account_webhook_test(){
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getEvents();With the Python client
answer = pw.get_events(){
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events/evt_4ba3d86a9cb07e1b79f6e144" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getEventsById("evt_4ba3d86a9cb07e1b79f6e144");With the Python client
answer = pw.get_events_by_id("evt_4ba3d86a9cb07e1b79f6e144"){
"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
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.jsonWith the JavaScript client
const answer = await pw.postEventsByIdRedeliver("evt_4ba3d86a9cb07e1b79f6e144", body);With the Python client
answer = pw.post_events_by_id_redeliver("evt_4ba3d86a9cb07e1b79f6e144", body){
"destination_id": "wd_4e1687f5d8638cbd"
}{
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/events/evt_298da382d80136d9249cf920/attempts" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getEventsByIdAttempts("evt_298da382d80136d9249cf920");With the Python client
answer = pw.get_events_by_id_attempts("evt_298da382d80136d9249cf920"){
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getWebhookDestinations();With the Python client
answer = pw.get_webhook_destinations(){
"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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/webhook-destinations" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonWith the JavaScript client
const answer = await pw.postWebhookDestinations(body);With the Python client
answer = pw.post_webhook_destinations(body){
"name": "Primary",
"url": "https://hooks.example.com/payerworks/primary",
"event_types": [
"sandbox.claim.acknowledged",
"sandbox.remittance.received"
],
"concurrency": 2
}{
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getWebhookDestinationsById("wd_4e1687f5d8638cbd");With the Python client
answer = pw.get_webhook_destinations_by_id("wd_4e1687f5d8638cbd"){
"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
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.jsonWith the JavaScript client
const answer = await pw.putWebhookDestinationsById("wd_4e1687f5d8638cbd", body);With the Python client
answer = pw.put_webhook_destinations_by_id("wd_4e1687f5d8638cbd", body){
"rotate": true,
"overlap_hours": 48
}{
"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
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.deleteWebhookDestinationsById("wd_4e1687f5d8638cbd");With the Python client
answer = pw.delete_webhook_destinations_by_id("wd_4e1687f5d8638cbd"){
"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
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd/ping" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.postWebhookDestinationsByIdPing("wd_4e1687f5d8638cbd");With the Python client
answer = pw.post_webhook_destinations_by_id_ping("wd_4e1687f5d8638cbd"){
"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
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/webhook-destinations/wd_4e1687f5d8638cbd/deliveries" \
-H "Authorization: Bearer $PAYERWORKS_API_KEY"With the JavaScript client
const answer = await pw.getWebhookDestinationsByIdDeliveries("wd_4e1687f5d8638cbd");With the Python client
answer = pw.get_webhook_destinations_by_id_deliveries("wd_4e1687f5d8638cbd"){
"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.
| 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.
| 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 runs every step below against the API with a test key. It needs payerworks.mjs and sample-data.mjs beside it.
// 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=<the base URL shown with your key> 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<string, string>} 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 <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));
}
{
"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"
}
}