# Organizations and members

> Keys call the API; people manage the account. Invite your team with a role, each signing in with an authenticator app, and decide who manages the keys.

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

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

Keys are how your systems call the API; members are how people manage the account. An organization holds one account (the keys one key started) and the people who manage it, each with a role: owner, admin, developer or viewer.

A member signs in at payerworks.com/console with a password and a code from an authenticator app, always: there is no sign-in without the app. A sign-in never calls a claim, eligibility, file, sandbox, MCP or batch job route; those take a key. With a sign-in, a member manages the account’s keys, webhooks, enrollments, SFTP user and notice address, as far as their role allows, and reads its audit log.

These routes take a sign-in’s ID token as the bearer, `Authorization: Bearer <ID token>`, where every other route takes a key. Where the sign-in belongs to several organizations, the Payerworks-Organization header names one; a member’s call to one of the account’s routes (its keys, webhooks, events, enrollments, SFTP user, notice address or audit log) names the key it acts on in the Payerworks-Key header (its key id, as GET /v1/org/keys lists them), and is answered as that key would be answered, within the member’s role. Organizations are open only where Payerworks has turned them on; elsewhere nobody can sign in, a sign-in’s token is answered 501 `org_not_offered`, and keys work as before.

## Routes

- [GET /v1/org/roles](#get-v1-org-roles): The roles
- [GET /v1/org/memberships](#get-v1-org-memberships): Your organizations
- [POST /v1/org](#post-v1-org): Make an organization
- [GET /v1/org](#get-v1-org): Your organization
- [PUT /v1/org](#put-v1-org): Change your organization
- [GET /v1/org/members](#get-v1-org-members): The members
- [PUT /v1/org/members/{id}](#put-v1-org-members-id): Change a member’s role
- [DELETE /v1/org/members/{id}](#delete-v1-org-members-id): Remove a member
- [GET /v1/org/invitations](#get-v1-org-invitations): The invitations waiting
- [POST /v1/org/invitations](#post-v1-org-invitations): Invite a person
- [DELETE /v1/org/invitations/{id}](#delete-v1-org-invitations-id): Cancel an invitation
- [POST /v1/org/join](#post-v1-org-join): Accept an invitation
- [GET /v1/org/keys](#get-v1-org-keys): The account’s keys

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

### GET /v1/org/roles

**The roles.** The one role table: every role, every permission and the owner’s rules. Readable with a key or a sign-in.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "roles": [
    {
      "role": "owner",
      "words": "Everything an admin does, and the owner's own decisions: owners, the organization's settings, and revoking a removed member's keys.",
      "permissions": [
        "org.read",
        "members.read"
      ]
    },
    {
      "role": "admin",
      "words": "Manages people (never an owner) and every key, webhook, enrollment and SFTP user, test or live.",
      "permissions": [
        "org.read",
        "members.read"
      ]
    }
  ],
  "permissions": [
    {
      "permission": "org.read",
      "words": "See the organization, its account and its settings."
    },
    {
      "permission": "org.update",
      "words": "Rename the organization and choose who manages keys."
    }
  ],
  "owner_rules": [
    "Only an owner invites an owner, makes a member an owner, or changes or removes an owner.",
    "An organization always keeps at least one owner: the last owner cannot be removed, leave, or be given another role."
  ]
}
```

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

| Field | Type | What it is |
| --- | --- | --- |
| `roles` | list of object | Every role, the least last. |
| `permissions` | list of object |  |
| `owner_rules` | list of string | The owner’s rules: 4 sentences. |

### GET /v1/org/memberships

**Your organizations.** The organizations this sign-in belongs to, with its role in each. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "data": [
    {
      "org_id": "pwo_25323f4c596673808d9aa7b4",
      "name": "Sample Dental",
      "role": "owner",
      "member_id": "pwm_4a5764717e8b98a5"
    }
  ]
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `data` | list of object | At most 20. |

### POST /v1/org

**Make an organization.** Make an organization for an account: name it and give one key of the account (api_key). The key proves the account and is checked and not kept; you become its owner. An account belongs to one organization. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

request.json

```json
{
  "name": "Sample Dental",
  "api_key": "pw_test_qk3x..."
}
```

The answer (201)

```json
{
  "org_id": "pwo_25323f4c596673808d9aa7b4",
  "name": "Sample Dental",
  "account_id": "pwk_500de6d41a6eacebb6e34d08",
  "owners": 1,
  "members": 1,
  "key_management": "keys_and_members",
  "created_at": "2026-10-19T15:00:00.000Z",
  "you": {
    "member_id": "pwm_4a5764717e8b98a5",
    "role": "owner",
    "permissions": [
      "org.read",
      "members.read"
    ]
  },
  "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call."
}
```

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

| Field | Type | What it is |
| --- | --- | --- |
| `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. |
| `name` | string | 1 to 80 characters. |
| `account_id` | string | The account it manages: the keys one key started. |
| `owners` | integer |  |
| `members` | integer |  |
| `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). |
| `created_at` | string |  |
| `updated_at` | string or null |  |
| `you` | OrgYou |  |
| `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. |
| `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. |

### GET /v1/org

**Your organization.** The organization, with your role and what it lets you do. Name it in the Payerworks-Organization header when your sign-in belongs to several. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "org_id": "pwo_25323f4c596673808d9aa7b4",
  "name": "Sample Dental",
  "account_id": "pwk_500de6d41a6eacebb6e34d08",
  "owners": 1,
  "members": 1,
  "key_management": "keys_and_members",
  "created_at": "2026-10-19T15:00:00.000Z",
  "you": {
    "member_id": "pwm_4a5764717e8b98a5",
    "role": "owner",
    "permissions": [
      "org.read",
      "members.read"
    ]
  },
  "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call.",
  "pending_invitations": 0
}
```

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

| Field | Type | What it is |
| --- | --- | --- |
| `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. |
| `name` | string | 1 to 80 characters. |
| `account_id` | string | The account it manages: the keys one key started. |
| `owners` | integer |  |
| `members` | integer |  |
| `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). |
| `created_at` | string |  |
| `updated_at` | string or null |  |
| `you` | OrgYou |  |
| `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. |
| `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. |

### PUT /v1/org

**Change your organization.** Rename it, or choose who manages the account’s keys (key_management): keys_and_members, the default, where an API key of the account may still make, rotate and revoke its keys, or members_only, where only a signed-in member with the role for it may. An owner’s.

Request

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

With the JavaScript client

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

With the Python client

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

request.json

```json
{
  "name": "Sample Dental Group"
}
```

The answer (200)

```json
{
  "org_id": "pwo_25323f4c596673808d9aa7b4",
  "name": "Sample Dental Group",
  "account_id": "pwk_500de6d41a6eacebb6e34d08",
  "owners": 1,
  "members": 1,
  "key_management": "keys_and_members",
  "created_at": "2026-10-19T15:00:00.000Z",
  "you": {
    "member_id": "pwm_4a5764717e8b98a5",
    "role": "owner",
    "permissions": [
      "org.read",
      "members.read"
    ]
  },
  "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call."
}
```

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

| Field | Type | What it is |
| --- | --- | --- |
| `org_id` | string | pwo_ and 24 lowercase hexadecimal characters. |
| `name` | string | 1 to 80 characters. |
| `account_id` | string | The account it manages: the keys one key started. |
| `owners` | integer |  |
| `members` | integer |  |
| `key_management` | one of "keys_and_members", "members_only" | keys_and_members: An API key of the account may still make, rotate and revoke the account's keys, as before the organization; members may too. members_only: Only a signed-in member with the role for it makes, rotates and revokes the account's keys; an API key is refused (keys_managed_by_members). |
| `created_at` | string |  |
| `updated_at` | string or null |  |
| `you` | OrgYou |  |
| `pending_invitations` | integer | GET /v1/org only: invitations not yet accepted or canceled. |
| `kept` | string | An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call. |

### GET /v1/org/members

**The members.** Every member: the email address of the sign-in, the name, the role, when they joined and who invited them. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "data": [
    {
      "member_id": "pwm_4a5764717e8b98a5",
      "email": "jane.doe@sample-dental.com",
      "name": "Jane Doe",
      "role": "owner",
      "joined_at": "2026-10-19T15:00:00.000Z",
      "invited_by": null,
      "updated_at": null,
      "you": true
    },
    {
      "member_id": "pwm_727f8c99a6b3c0cd",
      "email": "john.doe@sample-dental.com",
      "name": "John Doe",
      "role": "developer",
      "joined_at": "2026-10-19T15:00:00.000Z",
      "invited_by": "pwm_4a5764717e8b98a5",
      "updated_at": null,
      "you": false
    }
  ]
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `data` | list of OrgMember | At most 200. |

### PUT /v1/org/members/{id}

**Change a member’s role.** A new role: owner, admin, developer or viewer. Only an owner makes, changes or demotes an owner, and the last owner is never demoted.

Request

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

With the JavaScript client

```js
const answer = await pw.putOrgMembersById("pwm_727f8c99a6b3c0cd", body);
```

With the Python client

```python
answer = pw.put_org_members_by_id("pwm_727f8c99a6b3c0cd", body)
```

request.json

```json
{
  "role": "viewer"
}
```

The answer (200)

```json
{
  "member_id": "pwm_727f8c99a6b3c0cd",
  "email": "john.doe@sample-dental.com",
  "name": "John Doe",
  "role": "viewer",
  "joined_at": "2026-10-19T15:00:00.000Z",
  "invited_by": "pwm_4a5764717e8b98a5",
  "updated_at": "2026-10-19T15:00:00.000Z",
  "you": false
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `member_id` | string |  |
| `email` | string | The sign-in’s verified email address. |
| `name` | string | At most 120 characters. |
| `role` | one of "owner", "admin", "developer", "viewer" |  |
| `joined_at` | string |  |
| `invited_by` | string or null | The member who invited them; null for the one who made the organization. |
| `updated_at` | string or null |  |
| `you` | boolean | True for the member asking. |

### DELETE /v1/org/members/{id}

**Remove a member.** The member loses access at once. revoke_keys=true, an owner’s choice, also revokes the keys they made or rotated in the console (each one whose secret they were shown); left out, those keys keep working and are listed in the answer. The last owner is never removed, and a member may remove themselves to leave.

Request

```bash
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/org/members/pwm_727f8c99a6b3c0cd" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.deleteOrgMembersById("pwm_727f8c99a6b3c0cd");
```

With the Python client

```python
answer = pw.delete_org_members_by_id("pwm_727f8c99a6b3c0cd")
```

The answer (200)

```json
{
  "removed": {
    "member_id": "pwm_727f8c99a6b3c0cd",
    "email": "john.doe@sample-dental.com",
    "name": "John Doe",
    "role": "viewer",
    "joined_at": "2026-10-19T15:00:00.000Z",
    "invited_by": "pwm_4a5764717e8b98a5",
    "updated_at": "2026-10-19T15:00:00.000Z"
  },
  "left": false,
  "keys_made": [],
  "keys_rotated": [],
  "keys_revoked": [],
  "keys_not_revoked": []
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `removed` | OrgMember |  |
| `left` | boolean | True when the member removed themselves. |
| `keys_made` | list of string | The keys the member made in the console; null when they could not be read (keys_note says so). |
| `keys_rotated` | list of string | The keys the member rotated in the console without making them; null when they could not be read. |
| `keys_revoked` | list of string | Revoked by this removal, as an owner asked (revoke_keys=true): made or rotated. |
| `keys_not_revoked` | list of string | Asked to be revoked (revoke_keys=true) and still working: the revocation failed, or the key was past the 25 one removal revokes. Revoke each with DELETE /v1/keys/{id}. |
| `keys_note` | string | Present only when the member was removed but their keys could not be read: what to do next. |

### GET /v1/org/invitations

**The invitations waiting.** Every invitation not yet accepted or canceled: the address, the role, who invited and until when the link works. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "data": [
    {
      "invitation_id": "pwi_03101d2a3744515e",
      "email": "john.doe@sample-dental.com",
      "role": "developer",
      "invited_by": "pwm_4a5764717e8b98a5",
      "created_at": "2026-10-19T15:00:00.000Z",
      "expires_at": "2026-10-26T15:00:00.000Z",
      "expired": false
    },
    {
      "invitation_id": "pwi_4d5a6774818e9ba8",
      "email": "rick.roe@sample-dental.com",
      "role": "viewer",
      "invited_by": "pwm_4a5764717e8b98a5",
      "created_at": "2026-10-19T15:00:00.000Z",
      "expires_at": "2026-10-26T15:00:00.000Z",
      "expired": false
    }
  ]
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `data` | list of OrgInvitation | At most 50 waiting. |

### POST /v1/org/invitations

**Invite a person.** Invite a person by email with a role. Payerworks emails a link that works for 7 days, and only for a sign-in at that verified address; the email names neither the organization nor who invited. An organization sends at most 25 invitations a UTC day.

Request

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

With the JavaScript client

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

With the Python client

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

request.json

```json
{
  "email": "john.doe@sample-dental.com",
  "role": "developer"
}
```

The answer (201)

```json
{
  "invitation_id": "pwi_03101d2a3744515e",
  "email": "john.doe@sample-dental.com",
  "role": "developer",
  "invited_by": "pwm_4a5764717e8b98a5",
  "created_at": "2026-10-19T15:00:00.000Z",
  "expires_at": "2026-10-26T15:00:00.000Z",
  "expired": false,
  "emailed": true
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `invitation_id` | string | pwi_ and 16 lowercase hexadecimal characters. |
| `email` | string |  |
| `role` | one of "owner", "admin", "developer", "viewer" |  |
| `invited_by` | string |  |
| `created_at` | string |  |
| `expires_at` | string |  |
| `expired` | boolean | The link works for 7 days. |
| `emailed` | boolean | POST only: whether the email went. |
| `join_url` | string | POST only, when the email did not go: the link to hand to the person yourself. |
| `canceled` | the value true | DELETE only. |

### DELETE /v1/org/invitations/{id}

**Cancel an invitation.** The link stops working at once.

Request

```bash
curl -sS -X DELETE "$PAYERWORKS_BASE_URL/v1/org/invitations/pwi_4d5a6774818e9ba8" \
  -H "Authorization: Bearer $PAYERWORKS_API_KEY"
```

With the JavaScript client

```js
const answer = await pw.deleteOrgInvitationsById("pwi_4d5a6774818e9ba8");
```

With the Python client

```python
answer = pw.delete_org_invitations_by_id("pwi_4d5a6774818e9ba8")
```

The answer (200)

```json
{
  "invitation_id": "pwi_4d5a6774818e9ba8",
  "email": "rick.roe@sample-dental.com",
  "role": "viewer",
  "invited_by": "pwm_4a5764717e8b98a5",
  "created_at": "2026-10-19T15:00:00.000Z",
  "expires_at": "2026-10-26T15:00:00.000Z",
  "expired": false,
  "canceled": true
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `invitation_id` | string | pwi_ and 16 lowercase hexadecimal characters. |
| `email` | string |  |
| `role` | one of "owner", "admin", "developer", "viewer" |  |
| `invited_by` | string |  |
| `created_at` | string |  |
| `expires_at` | string |  |
| `expired` | boolean | The link works for 7 days. |
| `emailed` | boolean | POST only: whether the email went. |
| `join_url` | string | POST only, when the email did not go: the link to hand to the person yourself. |
| `canceled` | the value true | DELETE only. |

### POST /v1/org/join

**Accept an invitation.** Accept an invitation with the token from its link (the part after #join=). The sign-in’s verified email address must be the address the invitation was sent to. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

request.json

```json
{
  "token": "3uv4BRIfLDlGU2BteoeUoa67yNXi7_wJFiMwPUpXZHE"
}
```

The answer (201)

```json
{
  "organization": {
    "org_id": "pwo_25323f4c596673808d9aa7b4",
    "name": "Sample Dental Group",
    "account_id": "pwk_500de6d41a6eacebb6e34d08",
    "owners": 1,
    "members": 2,
    "key_management": "keys_and_members",
    "created_at": "2026-10-19T15:00:00.000Z",
    "you": {
      "member_id": "pwm_727f8c99a6b3c0cd",
      "role": "developer",
      "permissions": [
        "org.read",
        "members.read"
      ]
    },
    "kept": "An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call."
  },
  "member": {
    "member_id": "pwm_727f8c99a6b3c0cd",
    "email": "john.doe@sample-dental.com",
    "name": "John Doe",
    "role": "developer",
    "joined_at": "2026-10-19T15:00:00.000Z",
    "invited_by": "pwm_4a5764717e8b98a5",
    "updated_at": null,
    "you": true
  }
}
```

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

| Field | Type | What it is |
| --- | --- | --- |
| `organization` | Organization |  |
| `member` | OrgMember |  |

### GET /v1/org/keys

**The account’s keys.** Every key of the organization’s account, test and live, as GET /v1/keys lists them, with who made each: a member in the console, or a key. Never a key itself. A sign-in only.

Request

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

With the JavaScript client

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

With the Python client

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

The answer (200)

```json
{
  "account_id": "pwk_500de6d41a6eacebb6e34d08",
  "data": [
    {
      "key_id": "pwk_500de6d41a6eacebb6e34d08",
      "name": "Server",
      "mode": "test",
      "last_four": "4Lvg",
      "created_at": "2026-10-19T12:00:00.000Z",
      "expires_at": null,
      "expired": false,
      "revoked_at": null,
      "rotated_at": null,
      "previous_secret_accepted_until": null,
      "current": false,
      "made_by": {
        "kind": "key",
        "id": null
      }
    }
  ]
}
```

| Field | Type | What it is |
| --- | --- | --- |
| `account_id` | string |  |
| `data` | list of object | Every key of the account, test and live; never a key itself. |

## Reading the answer

- GET /v1/org answers the organization with `you`: your member id, your role and the permissions it gives. The console shows a control only for a permission the API answered.
- GET /v1/org/roles is the one role table, the same for every organization, and readable with a key too.
- A removal answers the keys the member made in the console: `keys_revoked` when an owner asked for revoke_keys=true, and `keys_not_revoked` otherwise, so nothing is revoked as a side effect.
- An invitation that could not be emailed answers `emailed: false` with its link in `join_url`, to hand to the person yourself.

## Troubleshooting

- `org_session_required` (401): The route needs a member’s console sign-in, not an API key. GET /v1/org/roles is the one a key may read.
- `org_choose` (400): The sign-in belongs to several organizations: name one in the Payerworks-Organization header.
- `org_role_forbidden` (403): Your role does not allow that; `needs` names the permission, and GET /v1/org/roles which roles hold it.
- `org_last_owner` (409): An organization always keeps at least one owner. Make another member an owner first, then step down or leave.
- `org_invitation_email_mismatch` (403): An invitation admits only a sign-in whose verified email address is the one it was sent to. Sign in with that address.
- `org_acting_key_required` (400): A member’s call to one of the account’s routes names the key it acts on in the Payerworks-Key header.

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

| Code | Status | What it means |
| --- | --- | --- |
| `invalid_org_invitation_id` | 400 | An invitation id is pwi_ followed by 16 lowercase hexadecimal characters. |
| `invalid_org_member_id` | 400 | A member id is pwm_ followed by 16 lowercase hexadecimal characters. |
| `invalid_org_request` | 400 | The request is not what this route takes: errors names each field and why. |
| `org_acting_key_required` | 400 | Name the key this change is for in the Payerworks-Key header (its key id; GET /v1/org/keys lists them). |
| `org_choose` | 400 | This sign-in belongs to several organizations: name one in the Payerworks-Organization header (GET /v1/org/memberships lists them). |
| `org_session_invalid` | 401 | The sign-in could not be checked, or it has ended. Sign in again. |
| `org_session_required` | 401 | This route needs a member's console sign-in, not an API key. Sign in at payerworks.com/console. |
| `keys_managed_by_members` | 403 | This account's organization chose that only its signed-in members manage keys. Make, rotate or revoke the key at payerworks.com/console. |
| `org_email_not_verified` | 403 | Confirm this sign-in's email address first: the code was emailed when the sign-in was made. |
| `org_invitation_email_mismatch` | 403 | This invitation was sent to another address. Sign in with the address the email reached. |
| `org_key_not_usable` | 403 | The api_key given is not a usable key (unknown, revoked or expired), so it proves no account. |
| `org_not_a_member` | 403 | This sign-in belongs to no organization yet. Make one with POST /v1/org, or open an invitation's link. |
| `org_owner_only` | 403 | Only an owner may do that: invite or make an owner, change or remove an owner, revoke a removed member's keys, or change the organization's settings. |
| `org_role_forbidden` | 403 | Your role in this organization does not allow that. GET /v1/org/roles says which role does. |
| `org_session_not_accepted` | 403 | A member's sign-in manages an account; it does not make this call. Call this route with an API key. |
| `org_acting_key_not_found` | 404 | The organization's account has no usable key with that id. |
| `org_invitation_not_found` | 404 | No pending invitation matches. It may have been accepted or canceled; ask for a new one. |
| `org_member_not_found` | 404 | This organization has no member with that id. |
| `org_not_found` | 404 | No organization with that id has this sign-in as a member. |
| `org_account_taken` | 409 | That key's account already belongs to an organization. Ask one of its owners to invite you. |
| `org_already_member` | 409 | That person is already a member of this organization. |
| `org_changed_meanwhile` | 409 | The organization changed while this request was made. Read it again and send the change again. |
| `org_invitation_pending` | 409 | An invitation to that address is already waiting. Cancel it to send another. |
| `org_last_owner` | 409 | An organization always keeps at least one owner. Make another member an owner first. |
| `org_limit_reached` | 409 | An organization holds at most 200 members and 50 pending invitations, and a sign-in belongs to at most 20 organizations. |
| `org_invitation_expired` | 410 | An invitation's link works for 7 days. Ask a member of the organization for a new one. |
| `org_invitations_daily_limit` | 429 | An organization sends at most 25 invitations a UTC day. Send more tomorrow. |
| `org_not_offered` | 501 | Organizations and member sign-in are not open on this deployment yet. Keys still work as before. |
| `store_unavailable` | 503 | We could not read or write this key's records just now; the message says what was not done. Retry in a moment. |

## Code lists

### The roles

| Role | What it may do | Permissions |
| --- | --- | --- |
| `owner` | Everything an admin does, and the owner's own decisions: owners, the organization's settings, and revoking a removed member's keys. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test`, `members.invite`, `members.change_role`, `members.remove`, `keys.write_live`, `webhooks.write_live`, `enrollments.write_live`, `sftp.write_live`, `contact.write`, `org.update` |
| `admin` | Manages people (never an owner) and every key, webhook, enrollment and SFTP user, test or live. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test`, `members.invite`, `members.change_role`, `members.remove`, `keys.write_live`, `webhooks.write_live`, `enrollments.write_live`, `sftp.write_live`, `contact.write` |
| `developer` | Builds with test keys: makes and rotates them, sets their webhooks, enrollments and SFTP users; reads everything live. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read`, `keys.write_test`, `webhooks.write_test`, `enrollments.write_test`, `sftp.write_test` |
| `viewer` | Reads everything (never a key itself, never a secret) and changes nothing. A seat for an auditor or a manager. | `org.read`, `members.read`, `keys.read`, `webhooks.read`, `events.read`, `enrollments.read`, `sftp.read`, `usage.read`, `audit.read` |

### The owner’s rules

- Only an owner invites an owner, makes a member an owner, or changes or removes an owner.
- An organization always keeps at least one owner: the last owner cannot be removed, leave, or be given another role.
- Removing a member never revokes the keys they made or rotated in the console unless an owner says so in that removal (revoke_keys=true): a key whose secret they were shown, made or rotated, is theirs to revoke.
- Only an owner decides whether an API key may still manage the account's keys (key_management).

### What an organization keeps

An organization is kept while it exists: its name, its members (each sign-in's email address, name and role) and its pending invitations (an email address and a role, for 7 days). Never a claim, a patient or the body of a call.

## Example

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

organizations.mjs

```js
// Payerworks example: organizations, members and roles. Docs: https://payerworks.com/docs/organizations
//
// Run it with Node 18 or later, with the JavaScript client (https://payerworks.com/sdk/payerworks.mjs)
// and sample-data.mjs beside this file:
//   PAYERWORKS_API_KEY=pw_test_... PAYERWORKS_BASE_URL=<the base URL shown with your key> node organizations.mjs
// A test key and invented data only. Payerworks sends nothing to a payer: every file it writes is yours.
//
// Keys call the API; members manage the account. A member signs in at payerworks.com/console with a
// password and an authenticator app, and the organization routes take that sign-in. With a key, this
// file reads the one role table and shows the refusal every other organization route gives a key.
import { pathToFileURL } from 'node:url';
import { Payerworks, PayerworksError } from './payerworks.mjs';
import { refusalOf } from './sample-data.mjs';

/** @param {Payerworks} pw */
export async function run(pw) {
  // 1. The one role table, the same for every organization: readable with a key.
  const table = await pw.getOrgRoles();

  // 2. The failure path: the organization itself answers a member's sign-in, never a key.
  const refused = await refusalOf(() => pw.getOrg(), { PayerworksError });

  return {
    roles: table.roles.map((r) => r.role),
    permissions: table.permissions.length,
    owner_rules: table.owner_rules.length,
    refused,
  };
}

// Run as a script: node <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
{
  "roles": [
    "owner",
    "admin",
    "developer",
    "viewer"
  ],
  "permissions": 22,
  "owner_rules": 4,
  "refused": {
    "status": 401,
    "code": "org_session_required",
    "request_id": "req_2"
  }
}
```
