# Keys and your account

> How a call is authorized, what each answer carries, and the routes that manage your keys and your account.

Page: https://payerworks.com/docs/keys · Markdown: https://payerworks.com/docs/keys.md

> Payerworks writes and explains files; it sends nothing to a payer. Every file a route writes says `transmitted: false`, and is yours to send on your own connection. A test key is for invented data only, never a real patient.

Every call but GET /v1/health sends a key as `Authorization: Bearer pw_test_…` or `pw_live_…`. A test key is free from the developers page, for invented data only, and writes test files only; a live key needs a signed business associate agreement first. Your key is a bearer key: keep it on your server, never in browser code.

Every answer carries `x-request-id`, and every refusal carries the same id as `request_id` inside the error. Quote it when you write to us. A POST, and PUT /v1/account/webhook, may carry an Idempotency-Key, unless its route says it takes none: a retry with the same key and request within 24 hours is answered as the first call was, by running it again, because Payerworks never keeps an answer.

## Routes

- [GET /v1/health](#get-v1-health): Health
- [GET /v1/account](#get-v1-account): Your account
- [GET /v1/account/contact](#get-v1-account-contact): Your notice address
- [PUT /v1/account/contact](#put-v1-account-contact): Set your notice address
- [GET /v1/keys](#get-v1-keys): Your account’s keys
- [POST /v1/keys](#post-v1-keys): Make a key
- [DELETE /v1/keys/{id}](#delete-v1-keys-id): Revoke a key
- [POST /v1/keys/{id}/rotate](#post-v1-keys-id-rotate): Rotate a key
- [POST /v1/mcp](#post-v1-mcp): The MCP server, hosted
- [GET /v1/audit-log](#get-v1-audit-log): Your account’s audit log
- [GET /v1/audit-log/export](#get-v1-audit-log-export): Download the audit log

Each route shows its call with curl and with the two clients, then its real answer, shortened, and the fields it holds. The client calls assume one client made once, `const pw = new Payerworks()` in JavaScript or `pw = payerworks.Payerworks()` in Python, each reading PAYERWORKS_API_KEY and PAYERWORKS_BASE_URL, and `body` is the request.json shown.

### GET /v1/health

**Health.** Whether the API is answering. No key needed.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/health"
```

With the JavaScript client

```js
const answer = await pw.getHealth();
```

With the Python client

```python
answer = pw.get_health()
```

The answer (200)

```json
{
  "ok": true,
  "service": "Payerworks",
  "version": "v1",
  "transmits": false
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `ok` | boolean |  |
| `service` | string |  |
| `version` | string |  |
| `transmits` | the value false | No route transmits to a payer yet. |

### GET /v1/account

**Your account.** The key’s account, label, mode and rate, the calls it made on each day of this month (counted by UTC day) and the calls refused before they ran, and its webhook if one is set, with what its last delivery did.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/account" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.getAccount();
```

With the Python client

```python
answer = pw.get_account()
```

The answer (200)

```json
{
  "key_id": "pwk_0123456789abcdef",
  "account": "Sample Billing Co",
  "label": "Self-serve",
  "mode": "test",
  "rpm": 60,
  "usage": {
    "month": "2026-10",
    "days": [
      {
        "date": "2026-10-18",
        "calls": 12,
        "refused": 0
      },
      {
        "date": "2026-10-19",
        "calls": 4,
        "refused": 1
      }
    ],
    "total": 16,
    "refused": 1
  },
  "webhook": {
    "url": "https://hooks.example.com/payerworks/events",
    "secret_last_four": "Osk=",
    "created_at": "2026-10-19T15:00:00.000Z",
    "previous_secret_expires_at": null,
    "last_delivery": {
      "at": "2026-10-19T15:00:00.000Z",
      "trigger": "sandbox",
      "first_event_id": "evt_cc264f7536a9315a0c6fe09f",
      "events": 3,
      "delivered": 3,
      "failed": 0,
      "status": 200,
      "error": null
    }
  }
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `key_id` | string |  |
| `account` | string |  |
| `label` | string |  |
| `mode` | one of "test", "live" |  |
| `rpm` | integer | Calls a minute. |
| `usage` | object | This month to date, by UTC day. |
| `webhook` | object or null |  |

### GET /v1/account/contact

**Your notice address.** Where Payerworks emails you about this key: when one of its webhook destinations keeps failing, and when one of its enrollment requests moves without your doing it. A test key from this page starts with the address its link was sent to, the only address it may set.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/account/contact" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.getAccountContact();
```

With the Python client

```python
answer = pw.get_account_contact()
```

The answer (200)

```json
{
  "email": "dev@example.com",
  "updated_at": "2026-10-20T19:12:00.052Z"
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `email` | string or null |  |
| `updated_at` | string or null |  |

### PUT /v1/account/contact

**Set your notice address.** The address told when a destination has failed for 2 hours, for 24 hours, and when it was disabled after its last attempt, and sent one email for every enrollment request that moved without your doing it inside the same 15 minutes. Send email: null to remove it, and nothing is emailed. Only the address that asked for this account’s test key by email (any key of the account may set it), or null; any other address is refused 403 contact_not_proved. A key issued by Payerworks has its address set by us: write to us.

Request

```bash
curl -sS -X PUT "$PAYERWORKS_BASE_URL/v1/account/contact" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

With the JavaScript client

```js
const answer = await pw.putAccountContact(body);
```

With the Python client

```python
answer = pw.put_account_contact(body)
```

request.json

```json
{
  "email": "dev@example.com"
}
```

The answer (200)

```json
{
  "email": "dev@example.com",
  "updated_at": "2026-10-20T19:12:00.052Z"
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `email` | string or null |  |
| `updated_at` | string or null |  |

### GET /v1/keys

**Your account’s keys.** Every key of your account in your key’s own mode (a test key lists the test keys, a live key the live ones): its name, mode, last four characters, when it was made, when it expires, when it was last rotated, until when its previous secret is accepted, whether it has expired, when it was revoked, and which one is the key asking, beside the account’s id. A revoked key is listed too. Never a key itself.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/keys" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.getKeys();
```

With the Python client

```python
answer = pw.get_keys()
```

The answer (200)

```json
{
  "account_id": "pwk_0123456789abcdef01",
  "data": [
    {
      "key_id": "pwk_0123456789abcdef01",
      "name": "Server",
      "mode": "test",
      "last_four": "aaaa",
      "created_at": "2026-10-19T12:00:00.000Z",
      "expires_at": null,
      "expired": false,
      "revoked_at": null,
      "rotated_at": null,
      "previous_secret_accepted_until": null,
      "current": true
    },
    {
      "key_id": "pwk_153493376889aa3239d7aab0",
      "name": "CI runner",
      "mode": "test",
      "last_four": "khYc",
      "created_at": "2026-10-20T19:12:00.100Z",
      "expires_at": "2027-01-18T19:12:00.100Z",
      "expired": false,
      "revoked_at": null,
      "rotated_at": null,
      "previous_secret_accepted_until": null,
      "current": false
    }
  ]
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `account_id` | string | The account: the keys one key started. |
| `data` | list of AccountKey |  |

### POST /v1/keys

**Make a key.** A named key of your key’s own mode and rate, shown once. expires_in_days (1 to 365) makes it stop on its own. A key never outlives the key that made it: its expiry is the sooner of expires_in_days and the making key’s own, and the making key’s when none is asked. An account holds 3 test keys or 25 live ones that are not revoked. Every key of an account may manage its keys, unless the account’s organization chose members only.

Request

```bash
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/keys" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

With the JavaScript client

```js
const answer = await pw.postKeys(body);
```

With the Python client

```python
answer = pw.post_keys(body)
```

request.json

```json
{
  "name": "CI runner",
  "expires_in_days": 90
}
```

The answer (201)

```json
{
  "key_id": "pwk_153493376889aa3239d7aab0",
  "name": "CI runner",
  "mode": "test",
  "last_four": "khYc",
  "created_at": "2026-10-20T19:12:00.100Z",
  "expires_at": "2027-01-18T19:12:00.100Z",
  "expired": false,
  "revoked_at": null,
  "rotated_at": null,
  "previous_secret_accepted_until": null,
  "current": false,
  "key": "pw_test_pBP8..."
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `key_id` | string |  |
| `name` | string |  |
| `mode` | one of "test", "live" |  |
| `last_four` | string |  |
| `created_at` | string |  |
| `expires_at` | string or null |  |
| `expired` | boolean |  |
| `revoked_at` | string or null |  |
| `rotated_at` | string or null |  |
| `previous_secret_accepted_until` | string or null |  |
| `current` | boolean | True for the key this call was made with. |
| `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). |
| `overlap_hours` | integer |  |

### DELETE /v1/keys/{id}

**Revoke a key.** The key stops working within a minute, and its SFTP user, if it has one, is removed at once. Revoking a key that is already revoked is not an error.

Request

```bash
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/keys/pwk_153493376889aa3239d7aab0" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.deleteKeysById("pwk_153493376889aa3239d7aab0");
```

With the Python client

```python
answer = pw.delete_keys_by_id("pwk_153493376889aa3239d7aab0")
```

The answer (200)

```json
{
  "key_id": "pwk_153493376889aa3239d7aab0",
  "name": "CI runner",
  "mode": "test",
  "last_four": "6NXM",
  "created_at": "2026-10-20T19:12:00.100Z",
  "expires_at": "2027-01-18T19:12:00.100Z",
  "expired": false,
  "revoked_at": "2026-10-20T19:12:00.111Z",
  "rotated_at": "2026-10-20T19:12:00.108Z",
  "previous_secret_accepted_until": "2026-10-21T19:12:00.108Z",
  "current": false
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `key_id` | string |  |
| `name` | string |  |
| `mode` | one of "test", "live" |  |
| `last_four` | string |  |
| `created_at` | string |  |
| `expires_at` | string or null |  |
| `expired` | boolean |  |
| `revoked_at` | string or null |  |
| `rotated_at` | string or null |  |
| `previous_secret_accepted_until` | string or null |  |
| `current` | boolean | True for the key this call was made with. |
| `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). |
| `overlap_hours` | integer |  |

### POST /v1/keys/{id}/rotate

**Rotate a key.** A new secret for the same key id, shown once, so its webhooks, events, usage and enrollments carry on. The old secret is still accepted for overlap_hours, 0 to 720, 24 by default, and stops at once if the key is revoked. Two rotations at once: the first finishes, and the other answers 409 key_rotation_conflict and makes nothing.

Request

```bash
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/keys/pwk_153493376889aa3239d7aab0/rotate" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

With the JavaScript client

```js
const answer = await pw.postKeysByIdRotate("pwk_153493376889aa3239d7aab0", body);
```

With the Python client

```python
answer = pw.post_keys_by_id_rotate("pwk_153493376889aa3239d7aab0", body)
```

request.json

```json
{
  "overlap_hours": 24
}
```

The answer (200)

```json
{
  "key_id": "pwk_153493376889aa3239d7aab0",
  "name": "CI runner",
  "mode": "test",
  "last_four": "6NXM",
  "created_at": "2026-10-20T19:12:00.100Z",
  "expires_at": "2027-01-18T19:12:00.100Z",
  "expired": false,
  "revoked_at": null,
  "rotated_at": "2026-10-20T19:12:00.108Z",
  "previous_secret_accepted_until": "2026-10-21T19:12:00.108Z",
  "current": false,
  "key": "pw_test_OOHH...",
  "overlap_hours": 24
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `key_id` | string |  |
| `name` | string |  |
| `mode` | one of "test", "live" |  |
| `last_four` | string |  |
| `created_at` | string |  |
| `expires_at` | string or null |  |
| `expired` | boolean |  |
| `revoked_at` | string or null |  |
| `rotated_at` | string or null |  |
| `previous_secret_accepted_until` | string or null |  |
| `current` | boolean | True for the key this call was made with. |
| `key` | string | pw_test_ or pw_live_ and the secret: only in the answer that made it (POST /v1/keys, or a rotation). |
| `overlap_hours` | integer |  |

### POST /v1/mcp

**The MCP server, hosted.** The same tools as the downloadable MCP server, over MCP’s Streamable HTTP transport (2026-07-28, and the 2025 handshakes), with your key as the bearer. Each tool call is a call to its route under your key. With a live key no tool is called unless the request carries Payerworks-MCP-Allow-Live-Key: 1. Payerworks transmits nothing.

Request

```bash
curl -sS -X POST "$PAYERWORKS_BASE_URL/v1/mcp" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

With the JavaScript client

```js
const answer = await pw.postMcp(body);
```

With the Python client

```python
answer = pw.post_mcp(body)
```

request.json

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "example-client",
      "version": "1"
    }
  }
}
```

The answer (200)

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": false
      },
      "prompts": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "payerworks",
      "title": "Payerworks",
      "version": "1.0.0"
    },
    "instructions": "Each tool calls one route of the Payerworks API with the key this connection presents. Payerworks validates claims, writes and reads X12 files and explains each answer in plain words. It transmits nothing: no file reaches a payer. post_sand…"
  }
}
```

Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json).

| Field | Type | What it is |
| --- | --- | --- |
| `jsonrpc` | the value "2.0" |  |
| `id` | object |  |
| `result` | object |  |
| `error` | object | A JSON-RPC error: never the API’s error shape. |

### GET /v1/audit-log

**Your account’s audit log.** Every change to this account, newest first: keys made, rotated, revoked or expired; the webhook, destinations and the notice address; providers, enrollment requests, tasks and documents; the SFTP user and its SSH keys; batch jobs; the organization and its members. Each entry says what, when, who (a key, a member, Payerworks, or Payerworks automatically), through which door, the object’s id, and the fields that changed by name only, never a value; a web address by its host. limit 1 to 100 (25), type (an entry type, or its family before the dot), created_gte and created_lt, starting_after. Each entry is kept 365 days, then deleted, and never changed before then. Every key of the account reads the same log.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/audit-log" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.getAuditLog();
```

With the Python client

```python
answer = pw.get_audit_log()
```

The answer (200)

```json
{
  "object": "list",
  "data": [
    {
      "id": "aud_aeb4acb2fdf610d4bc531306",
      "type": "webhook_destination.removed",
      "words": "Webhook destination removed",
      "at": "2026-10-20T19:12:00.114Z",
      "actor": {
        "type": "key",
        "id": "pwk_0123456789abcdef01"
      },
      "door": "api",
      "object": {
        "kind": "webhook_destination",
        "id": "wd_4e1687f5d8638cbd"
      },
      "changed": [],
      "url_host": null,
      "count": null,
      "request_id": "req_example_15"
    },
    {
      "id": "aud_a08c0b0cd9c2ce25b9ef59ab",
      "type": "key.revoked",
      "words": "API key revoked",
      "at": "2026-10-20T19:12:00.111Z",
      "actor": {
        "type": "key",
        "id": "pwk_0123456789abcdef01"
      },
      "door": "api",
      "object": {
        "kind": "key",
        "id": "pwk_153493376889aa3239d7aab0"
      },
      "changed": [],
      "url_host": null,
      "count": null,
      "request_id": "req_example_14"
    }
  ],
  "has_more": true,
  "next_cursor": "acur_412331373932353233353230313038236175645f663663623031613433613161613366356130353463653832",
  "retention_days": 365
}
```

Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json).

| Field | Type | What it is |
| --- | --- | --- |
| `object` | the value "list" |  |
| `data` | list of AuditEntry | Newest first. |
| `has_more` | boolean |  |
| `next_cursor` | string or null | Pass as starting_after. |
| `retention_days` | the value 365 | Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is. |

### GET /v1/audit-log/export

**Download the audit log.** The same entries and filters as a CSV file in the answer (csv, filename, columns), at most 5,000 entries a call; next_cursor names where the rest start. No cell opens as a formula in a spreadsheet.

Request

```bash
curl -sS -X GET "$PAYERWORKS_BASE_URL/v1/audit-log/export" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.getAuditLogExport();
```

With the Python client

```python
answer = pw.get_audit_log_export()
```

The answer (200)

```json
{
  "object": "audit_log_export",
  "content_type": "text/csv",
  "filename": "payerworks-audit-log-2026-10-20.csv",
  "row_count": 7,
  "columns": [
    "id",
    "at"
  ],
  "csv": "id,at,type,words,actor_type,actor_id,door,object_kind,object_id,changed,url_host,count,request_id\r\naud_aeb4acb2fdf610d4bc531306,2026-10-20T19:12:00.114Z,webhook_destination.removed,Webhook destination removed,key,pwk_0123456789abcdef01,api,…",
  "has_more": false,
  "next_cursor": null,
  "retention_days": 365
}
```

Shortened here: lists show their first two items and long strings are cut. The whole answer is this route’s example in [/openapi.json](https://payerworks.com/openapi.json).

| Field | Type | What it is |
| --- | --- | --- |
| `object` | the value "audit_log_export" |  |
| `content_type` | the value "text/csv" |  |
| `filename` | string |  |
| `row_count` | integer | At most 5,000. |
| `columns` | list of one of "id", "at", "type", "words", "actor_type", "actor_id", "door", "object_kind", "object_id", "changed", "url_host", "count", "request_id" |  |
| `csv` | string | The file, CRLF line ends; no cell opens as a formula in a spreadsheet. |
| `has_more` | boolean |  |
| `next_cursor` | string or null | Pass as starting_after for the rest. |
| `retention_days` | the value 365 | Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is. |

## Reading the answer

- GET /v1/account answers the key’s rate a minute and its usage this month to date, by UTC day, with refused calls counted apart.
- A rotation (POST /v1/keys/{id}/rotate) keeps the key id, so webhooks, events, usage and enrollments carry on; the old secret is accepted only inside the overlap you choose. Every key of an account may manage its keys, unless the account’s organization chose that only its signed-in members do (see [Organizations and members](https://payerworks.com/docs/organizations)).
- GET /v1/audit-log answers who changed what on the account, when and through which door, by field name only, never a value; each entry is kept one year. GET /v1/audit-log/export gives the same entries as a CSV file.

## Troubleshooting

- `invalid_key` (401): The key is not valid, or was revoked: a revoked key stops within a minute.
- `rate_limited` (429): Over the key’s calls a minute. Wait the seconds retry-after names; the clients do this for you, at most twice.
- `idempotency_key_reused` (422): That Idempotency-Key was used in the last 24 hours for a different request. Make a new key for each new request.
- `too_large` (413): A request body is at most 2 MB. Larger work is split across calls.
- `invalid_audit_type` (400): type names an entry type (key.created) or its family (key).
- `keys_managed_by_members` (403): The account’s organization chose that only its signed-in members make, rotate and revoke keys: do it in the console, or ask an owner to switch it back.

Every refusal these routes may answer, with its status. Any route may also answer the codes every route shares, on [Every error](https://payerworks.com/docs/errors).

| Code | Status | What it means |
| --- | --- | --- |
| `invalid_audit_cursor` | 400 | starting_after is the next_cursor of an earlier page of this audit log. |
| `invalid_audit_type` | 400 | type is one of the audit log's entry types, or the part before its dot (key, webhook, enrollment...), as the developers page lists them. |
| `invalid_concurrency` | 400 | concurrency is a whole number from 1 to 10: how many deliveries this destination takes at once. |
| `invalid_contact_email` | 400 | email is an address of at most 254 characters, or null to remove it. |
| `invalid_destination_id` | 400 | A destination id is wd_ followed by 16 lowercase hexadecimal characters. |
| `invalid_destination_name` | 400 | A destination's name is 1 to 80 characters. |
| `invalid_enabled` | 400 | enabled is true or false, or left out. |
| `invalid_event_types` | 400 | event_types is a list of the event types the developers page lists; an empty list, or leaving it out, receives every type. |
| `invalid_expires_in_days` | 400 | expires_in_days is a whole number from 1 to 365, or left out for a key that does not expire. |
| `invalid_idempotency_key` | 400 | An Idempotency-Key is 1 to 255 visible characters, with no spaces, quotes or backslashes. A UUID is a good one. |
| `invalid_json` | 400 | The body must be a JSON object. |
| `invalid_key_id` | 400 | A key id is pwk_ followed by 16 to 40 letters and digits. |
| `invalid_key_name` | 400 | A key's name is 1 to 80 characters. |
| `invalid_overlap_hours` | 400 | overlap_hours is a whole number from 0 to 720: how long the old secret is still accepted after a rotation. |
| `invalid_key` | 401 | That key is not valid. Keys are issued by Payerworks; a revoked key stops within a minute. |
| `no_key` | 401 | Send your API key as `Authorization: Bearer <key>`. |
| `contact_not_proved` | 403 | Notices go only to an address Payerworks has proved: the one that asked for this account's test key by email and opened the link we sent. Send that address, or null to stop the notices. To use another, write to us from it. |
| `keys_managed_by_members` | 403 | This account's organization chose that only its signed-in members manage keys. Make, rotate or revoke the key at payerworks.com/console. |
| `destination_not_found` | 404 | This key has no webhook destination with that id. |
| `key_not_found` | 404 | This account has no key with that id. |
| `not_at_destination` | 404 | That event was never sent to this destination (it was made before the destination was added, or its type is filtered out). A ping or a new event will be. |
| `destination_conflict` | 409 | The destination changed while this request ran, so nothing was saved. Send the request again. |
| `destination_disabled` | 409 | This destination is disabled. Enable it with PUT /v1/webhook-destinations/{id} and `enabled: true` first. |
| `destination_limit_reached` | 409 | A key holds at most 10 webhook destinations. Remove one to add another. |
| `idempotency_in_flight` | 409 | The first request with this Idempotency-Key is still running. Retry after the seconds retry-after names. |
| `key_limit_reached` | 409 | An account holds at most 3 test keys or 25 live keys that are not revoked. Revoke one to make another. |
| `key_revoked` | 409 | That key is revoked, so it cannot be rotated. Make a new key with POST /v1/keys. |
| `key_rotation_conflict` | 409 | Another rotation of this key finished while this one ran, so this one made nothing: the secret that other request showed is the key's current one. List the keys with GET /v1/keys, and rotate again if that secret was lost. |
| `key_settings_out_of_range` | 409 | The key's stored rate or account name is outside what a key may hold, so nothing was made or rotated; reason says which. Ask Payerworks to correct the key: sending the request again answers the same. |
| `too_large` | 413 | A request body may be at most 2 MB. |
| `idempotency_key_reused` | 422 | This Idempotency-Key was used in the last 24 hours for a different request. Send a new key for a new request. |
| `rate_limited` | 429 | Over this key's calls a minute. Wait the seconds retry-after names. |
| `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. |

## Code lists

### The headers on every answer

| Header | What it says |
| --- | --- |
| `x-request-id` | The request’s id. Quote it when you write to Payerworks. |
| `x-ratelimit-limit` | The key’s calls a minute. |
| `x-ratelimit-remaining` | Calls left in this UTC minute. |
| `x-ratelimit-reset` | Seconds until the window resets. |
| `RateLimit` | The same figures in the IETF draft’s form. |
| `RateLimit-Policy` | The key’s policy in the IETF draft’s form. |
| `idempotent-replayed` | true when this is the first answer to an Idempotency-Key request, given again. |
| `retry-after` | Seconds to wait before a retry. |

### The audit log’s entry types

An entry names what changed by field name only, never a value: no name, address, note or answer is kept. A web address is kept as its host alone. Each audit log entry is kept for 365 days (one year) from the moment of the change, then deleted. No entry is overwritten: each is written once, only where no entry of its id is.

| Type | What changed | Fields it may name |
| --- | --- | --- |
| `key.created` | API key created | `name`, `mode`, `rpm`, `expires_at` |
| `key.rotated` | API key secret rotated | `secret`, `overlap_hours` |
| `key.revoked` | API key revoked | none |
| `key.expired` | API key expired | none |
| `webhook.set` | Account webhook set | `url`, `secret` |
| `webhook.changed` | Account webhook changed | `url`, `secret`, `overlap_hours` |
| `webhook.removed` | Account webhook removed | none |
| `webhook_destination.created` | Webhook destination added | `name`, `url`, `event_types`, `enabled`, `concurrency`, `secret` |
| `webhook_destination.changed` | Webhook destination changed | `name`, `url`, `event_types`, `enabled`, `concurrency`, `secret`, `overlap_hours` |
| `webhook_destination.removed` | Webhook destination removed | none |
| `contact.changed` | Notice contact set | `email` |
| `contact.removed` | Notice contact removed | `email` |
| `provider.created` | Provider added | `name`, `npi`, `tax_id`, `tax_id_type`, `contacts` |
| `provider.changed` | Provider changed | `name`, `npi`, `tax_id`, `tax_id_type`, `contacts` |
| `provider.removed` | Provider removed | none |
| `enrollment.created` | Enrollment request created | `provider_id`, `payer`, `transaction`, `primary_contact`, `notify_email`, `requested_effective_date`, `era_aggregation`, `ptan`, `note` |
| `enrollment.changed` | Enrollment request changed | `provider_id`, `payer`, `transaction`, `primary_contact`, `notify_email`, `requested_effective_date`, `era_aggregation`, `ptan`, `note` |
| `enrollment.submitted` | Enrollment request submitted | none |
| `enrollment.withdrawn` | Enrollment request withdrawn | `reason` |
| `enrollment.withdrawal_requested` | Withdrawal of an enrollment request asked of Payerworks | `reason` |
| `enrollment.removed` | Enrollment request deleted | none |
| `enrollment.status_changed` | Enrollment request status changed | `status` |
| `enrollment.imported` | Enrollment requests imported from a file | none |
| `enrollment.note_added` | Note added to an enrollment request | none |
| `enrollment_task.answered` | Enrollment task answered | `values` |
| `enrollment_task.added` | Enrollment task added | none |
| `enrollment_task.superseded` | Enrollment task replaced by another | none |
| `enrollment_task.completed` | Enrollment task marked done | none |
| `enrollment_document.requested` | Enrollment document upload started | none |
| `enrollment_document.added` | Enrollment document added | none |
| `enrollment_document.removed` | Enrollment document removed | none |
| `sftp_user.added` | SFTP user added | `acknowledgment_999` |
| `sftp_user.changed` | SFTP user changed | `acknowledgment_999` |
| `sftp_user.removed` | SFTP user removed | none |
| `sftp_ssh_key.added` | SFTP SSH key added | none |
| `sftp_ssh_key.removed` | SFTP SSH key removed | none |
| `job.created` | Batch job created | `route` |
| `job.canceled` | Batch job canceled | none |
| `job.removed` | Batch job and its files deleted | none |
| `account_baa.recorded` | Business associate agreement recorded | `signer_name`, `signer_title`, `organization`, `signed_on`, `baa_version`, `reference_prefix` |
| `account_baa.withdrawn` | Business associate agreement withdrawn | none |
| `org.created` | Organization created | `name` |
| `org.updated` | Organization changed | `name`, `key_management` |
| `member.invited` | Member invited | `email`, `role` |
| `member.invitation_canceled` | Member invitation canceled | none |
| `member.joined` | Member joined | none |
| `member.role_changed` | Member role changed | `role` |
| `member.removed` | Member removed | `keys_revoked` |

## Example

[keys.mjs](https://payerworks.com/docs/examples/keys.mjs) runs every step below against the API with a test key. It needs [payerworks.mjs](https://payerworks.com/sdk/payerworks.mjs) and [sample-data.mjs](https://payerworks.com/docs/examples/sample-data.mjs) beside it.

keys.mjs

```js
// Payerworks example: keys and your account. Docs: https://payerworks.com/docs/keys
//
// Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs)
// and sample-data.mjs beside this file:
//   PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL=<the base URL shown with your key> node keys.mjs
// A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours.
import { pathToFileURL } from 'node:url';
import { Payerworks, PayerworksError } from './payerworks.mjs';
import { refusalOf } from './sample-data.mjs';

/** @param {Payerworks} pw */
export async function run(pw) {
  // 1. Health needs no key, and says the API transmits nothing.
  const health = await pw.getHealth();

  // 2. Your key's rate a minute, and its calls this month to date by UTC day.
  const account = await pw.getAccount();

  // 3. A named key for one job, shown once, then revoked. Every key of an account may manage its keys,
  //    unless the account's organization chose that only its signed-in members do.
  const made = await pw.postKeys({ name: 'Example job', expires_in_days: 1 });
  const revoked = await pw.deleteKeysById(made.key_id);

  // 4. The account's audit log: who made and revoked that key, when, and through which door, newest first.
  const log = await pw.getAuditLog({ query: { limit: 2 } });

  // 5. The failure path: a key id this account never had.
  const refused = await refusalOf(() => pw.deleteKeysById('pwk_0000000000000000'), { PayerworksError });

  return {
    transmits: health.transmits,
    mode: account.mode,
    rpm: account.rpm,
    made: { key_id: made.key_id.startsWith('pwk_'), shown_once: typeof made.key === 'string' },
    revoked: Boolean(revoked),
    audit: log.data.map((e) => ({ type: e.type, by: e.actor.type, door: e.door })),
    refused,
  };
}

// Run as a script: node <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));
}
```

What it answered, run against the API in Payerworks’ own tests (your request ids will differ)

```json
{
  "transmits": false,
  "mode": "test",
  "rpm": 600,
  "made": {
    "key_id": true,
    "shown_once": true
  },
  "revoked": true,
  "audit": [
    {
      "type": "key.revoked",
      "by": "key",
      "door": "api"
    },
    {
      "type": "key.created",
      "by": "key",
      "door": "api"
    }
  ],
  "refused": {
    "status": 404,
    "code": "key_not_found",
    "request_id": "req_6"
  }
}
```
