---
title: "Which API endpoints does RevenueDot Enterprise add?"
description: "RevenueDot Enterprise endpoints: licence status, organizations, custom roles and group role mappings, SAML and OpenID Connect single sign-on, SCIM 2.0 provisioning and signed compliance exports."
url: https://revenuedot.app/docs/api/enterprise
---

# Which API endpoints does RevenueDot Enterprise add?

These endpoints exist only on a server that runs [RevenueDot Enterprise](https://revenuedot.app/docs/guides/enterprise.md): one started with `REVENUEDOT_LICENSE_KEY`, or with `REVENUEDOT_EE_DEV=true` for development. Each group also needs its feature in the licence; without it the route answers 403. The open-source build has none of them.
Organization endpoints take a **dashboard session** (the `rd_session` cookie); secret API keys belong to one project and cannot call them. Writes from a browser must come from the dashboard's own site. The SCIM 2.0 service under `/scim/v2` takes a **SCIM token** instead. The `/sso` endpoints take no credentials: browsers and identity providers call them during sign-in.
Errors use the [REST API v2 format](https://revenuedot.app/docs/api/errors.md#rest-api-v2-error-types), except under `/scim/v2`, which answers RFC 7644 errors. The examples also read `ORG_ID` and `SCIM_TOKEN` from your shell.

Base URL: your server, for example `http://localhost:8787` or `https://revenuedot.example.com`. The examples read `REVENUEDOT_URL`, `PUBLIC_KEY`, `SECRET_KEY` and `PROJECT_ID` from your shell.

## Operations on this page (65)

- **Enterprise**: [Licence state and features](#licence-state-and-features)
- **Organizations**: [Organizations you belong to](#organizations-you-belong-to), [Create an organization](#create-an-organization), [Get an organization](#get-an-organization), [Update an organization](#update-an-organization), [Delete an organization](#delete-an-organization), [Organization with SSO and SCIM counts](#organization-with-sso-and-scim-counts), [List members](#list-members), [Add an existing account](#add-an-existing-account), [Change a member's organization role](#change-a-members-organization-role), [Remove a member, or leave](#remove-a-member-or-leave), [List the organization's projects](#list-the-organizations-projects), [Move a project into the organization](#move-a-project-into-the-organization), [Move a project out](#move-a-project-out), [Set a project's data location](#set-a-projects-data-location), [The organization audit log](#the-organization-audit-log)
- **Custom roles**: [Scopes a custom role can hold](#scopes-a-custom-role-can-hold), [List custom roles](#list-custom-roles), [Create a custom role](#create-a-custom-role), [Get a custom role](#get-a-custom-role), [Update a custom role](#update-a-custom-role), [Delete a custom role](#delete-a-custom-role), [A project's members with role names](#a-projects-members-with-role-names), [Give a project member a built-in or custom role](#give-a-project-member-a-built-in-or-custom-role), [List group role mappings](#list-group-role-mappings), [Map a group to a role in a project](#map-a-group-to-a-role-in-a-project), [Delete a group role mapping](#delete-a-group-role-mapping)
- **Single sign-on**: [List SSO connections](#list-sso-connections), [Create a SAML or OpenID Connect connection](#create-a-saml-or-openid-connect-connection), [Get an SSO connection](#get-an-sso-connection), [Update an SSO connection](#update-an-sso-connection), [Delete an SSO connection](#delete-an-sso-connection), [List email domains](#list-email-domains), [Add an email domain](#add-an-email-domain), [Check the domain's TXT record](#check-the-domains-txt-record), [Remove an email domain](#remove-an-email-domain), [Does this address sign in with SSO?](#does-this-address-sign-in-with-sso), [Start a sign-in for an email address](#start-a-sign-in-for-an-email-address), [Start a sign-in with one connection](#start-a-sign-in-with-one-connection), [SAML service provider metadata](#saml-service-provider-metadata), [SAML assertion consumer service](#saml-assertion-consumer-service), [OpenID Connect redirect URI](#openid-connect-redirect-uri)
- **SCIM 2.0**: [List SCIM tokens](#list-scim-tokens), [Create a SCIM token](#create-a-scim-token), [Revoke a SCIM token](#revoke-a-scim-token), [Groups the identity provider pushed](#groups-the-identity-provider-pushed), [Service provider configuration](#service-provider-configuration), [Resource types (User, Group)](#resource-types-user-group), [One resource type](#one-resource-type), [Schemas (User, Group, enterprise User)](#schemas-user-group-enterprise-user), [One schema](#one-schema), [List or find users](#list-or-find-users), [Create a user](#create-a-user), [Get a user](#get-a-user), [Replace a user](#replace-a-user), [Change a user](#change-a-user), [Delete a user](#delete-a-user), [List or find groups](#list-or-find-groups), [Create a group](#create-a-group), [Get a group](#get-a-group), [Replace a group](#replace-a-group), [Change a group](#change-a-group), [Delete a group](#delete-a-group)
- **Compliance exports**: [The public key exports are signed with](#the-public-key-exports-are-signed-with), [Download a signed audit log or access review](#download-a-signed-audit-log-or-access-review)

## Enterprise

Whether this server runs RevenueDot Enterprise, with which licence and features. See [Enterprise](https://revenuedot.app/docs/guides/enterprise.md).

### Licence state and features

`GET /v2/enterprise` · Auth: dashboard session · RevenueDot extension

Any signed-in user. The route exists only when the server was started with `REVENUEDOT_LICENSE_KEY` or `REVENUEDOT_EE_DEV=true`; the open-source build answers 404. With an invalid key it still answers, with `mode: invalid` and no features, and no other enterprise route exists.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/enterprise"
```

**Responses**

- **200**: The licence state. Returns [EnterpriseStatus](#enterprisestatus).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "enterprise",
  "mode": "development",
  "features": [
    "organizations",
    "custom_roles",
    "sso",
    "scim",
    "data_location",
    "audit_retention",
    "compliance_exports"
  ],
  "licensee": null,
  "expires_at": null,
  "message": "Development mode: for development and testing only, not for production (ee/LICENSE)."
}
```

## Organizations

Organizations own projects and hold members with owner, admin and member roles, seats, data location, audit retention and the organization audit log. Dashboard session only. See [Enterprise](https://revenuedot.app/docs/guides/enterprise.md).

### Organizations you belong to

`GET /v2/organizations` · Auth: dashboard session · RevenueDot extension

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations"
```

**Responses**

- **200**: Every organization where you are an active member, oldest first. Returns a list of [Organization](#organization).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "list",
  "items": [
    {
      "object": "organization",
      "id": "org_k2m9q4x7z1a8",
      "name": "Acme Inc.",
      "your_role": "owner",
      "region": "us",
      "region_name": "United States",
      "selectable_regions": [
        "us",
        "eu"
      ],
      "region_enforced": false,
      "audit_retention_days": 365,
      "sso_enforced": true,
      "seats": {
        "purchased": 50,
        "used": 23
      },
      "billing_email": "finance@acme.com",
      "member_count": 23,
      "project_count": 3,
      "features": [
        "organizations",
        "custom_roles",
        "sso",
        "scim",
        "data_location",
        "audit_retention",
        "compliance_exports"
      ],
      "created_at": 1790800914012,
      "updated_at": 1790887314012
    }
  ],
  "next_page": null,
  "url": "/v2/organizations"
}
```

### Create an organization

`POST /v2/organizations` · Auth: dashboard session · RevenueDot extension

You become its owner. `region` defaults to this deployment's region. A licence with an organization limit answers 403 once the server has that many organizations.

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `region` | `us`, `eu` | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations" \
  -H "Content-Type: application/json" -d '{"name":"Acme Inc."}'
```

**Responses**

- **201**: The new organization. Returns [Organization](#organization).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "organization",
  "id": "org_k2m9q4x7z1a8",
  "name": "Acme Inc.",
  "your_role": "owner",
  "region": "us",
  "region_name": "United States",
  "selectable_regions": [
    "us",
    "eu"
  ],
  "region_enforced": false,
  "audit_retention_days": 365,
  "sso_enforced": true,
  "seats": {
    "purchased": 50,
    "used": 23
  },
  "billing_email": "finance@acme.com",
  "member_count": 23,
  "project_count": 3,
  "features": [
    "organizations",
    "custom_roles",
    "sso",
    "scim",
    "data_location",
    "audit_retention",
    "compliance_exports"
  ],
  "created_at": 1790800914012,
  "updated_at": 1790887314012
}
```

### Get an organization

`GET /v2/organizations/{org_id}` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID"
```

**Responses**

- **200**: The organization. Returns [Organization](#organization).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "organization",
  "id": "org_k2m9q4x7z1a8",
  "name": "Acme Inc.",
  "your_role": "owner",
  "region": "us",
  "region_name": "United States",
  "selectable_regions": [
    "us",
    "eu"
  ],
  "region_enforced": false,
  "audit_retention_days": 365,
  "sso_enforced": true,
  "seats": {
    "purchased": 50,
    "used": 23
  },
  "billing_email": "finance@acme.com",
  "member_count": 23,
  "project_count": 3,
  "features": [
    "organizations",
    "custom_roles",
    "sso",
    "scim",
    "data_location",
    "audit_retention",
    "compliance_exports"
  ],
  "created_at": 1790800914012,
  "updated_at": 1790887314012
}
```

### Update an organization

`POST /v2/organizations/{org_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Changing `audit_retention_days`, `seats` or `billing_email` needs an owner. `sso_enforced: true` needs an enabled SSO connection and a verified domain (422 otherwise). `region` must be one of `selectable_regions`. Each field needs its feature in the licence (403 otherwise). Changes are recorded in the organization audit log as `organization_updated`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `region` | `us`, `eu` | no | Default data location for projects. |
| `audit_retention_days` | integer or null | no | 30 to 3650. Null keeps audit logs forever. |
| `sso_enforced` | boolean | no | Require single sign-on for the organization's verified domains. |
| `seats` | integer or null | no | Seats bought, 1 to 100000. |
| `billing_email` | string or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID" \
  -H "Content-Type: application/json" -d '{"audit_retention_days":365,"sso_enforced":true}'
```

**Responses**

- **200**: The updated organization. Returns [Organization](#organization).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "organization",
  "id": "org_k2m9q4x7z1a8",
  "name": "Acme Inc.",
  "your_role": "owner",
  "region": "us",
  "region_name": "United States",
  "selectable_regions": [
    "us",
    "eu"
  ],
  "region_enforced": false,
  "audit_retention_days": 365,
  "sso_enforced": true,
  "seats": {
    "purchased": 50,
    "used": 23
  },
  "billing_email": "finance@acme.com",
  "member_count": 23,
  "project_count": 3,
  "features": [
    "organizations",
    "custom_roles",
    "sso",
    "scim",
    "data_location",
    "audit_retention",
    "compliance_exports"
  ],
  "created_at": 1790800914012,
  "updated_at": 1790887314012
}
```

### Delete an organization

`DELETE /v2/organizations/{org_id}` · Auth: dashboard session · RevenueDot extension

Organization owners only. Move every project out first (422 otherwise). Its SSO connections, domains, SCIM tokens, custom roles, mappings and organization audit log are deleted.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID"
```

**Responses**

- **200**: Deleted.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "organization",
  "id": "org_k2m9q4x7z1a8",
  "deleted_at": 1790887314012
}
```

### Organization with SSO and SCIM counts

`GET /v2/organizations/{org_id}/overview` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/overview"
```

**Responses**

- **200**: The organization plus counts.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### List members

`GET /v2/organizations/{org_id}/members` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Includes deactivated members (`active: false`).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/members"
```

**Responses**

- **200**: Members, oldest first. Returns a list of [OrganizationMember](#organizationmember).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Add an existing account

`POST /v2/organizations/{org_id}/members` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Only owners add owners. The address must already have a RevenueDot account (404 otherwise); new people join through single sign-on, SCIM or a project invite. Owners and admins become Admins of every organization project; group role mappings apply at once.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | yes |  |
| `role` | `owner`, `admin`, `member` | no | Default member. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/members" \
  -H "Content-Type: application/json" -d '{"email":"sam@acme.com","role":"admin"}'
```

**Responses**

- **201**: The member. Returns [OrganizationMember](#organizationmember).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Change a member's organization role

`POST /v2/organizations/{org_id}/members/{user_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Only owners make or unmake owners, and the last owner cannot be demoted (400). Promoting to admin or owner adds Admin access to every organization project; demoting removes the access that came from the organization role.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `user_id` | string | yes | User id. |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | `owner`, `admin`, `member` | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/members/$USER_ID" \
  -H "Content-Type: application/json" -d '{"role":"member"}'
```

**Responses**

- **200**: The member. Returns [OrganizationMember](#organizationmember).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Remove a member, or leave

`DELETE /v2/organizations/{org_id}/members/{user_id}` · Auth: dashboard session · RevenueDot extension

Admins remove members; only owners remove owners; anyone may remove themselves. The last owner cannot leave (422). The person loses every membership in the organization's projects, including ones added by hand.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `user_id` | string | yes | User id. Your own to leave. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/members/$USER_ID"
```

**Responses**

- **200**: Removed.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

### List the organization's projects

`GET /v2/organizations/{org_id}/projects` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects"
```

**Responses**

- **200**: Projects with their region and member count. Returns a list of [OrganizationProject](#organizationproject).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Move a project into the organization

`POST /v2/organizations/{org_id}/projects` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). You must also be an Admin of the project (403 otherwise). A project belongs to one organization at a time (409). The project is recorded in this deployment's region; no data moves. Everyone on the project becomes an organization member, and organization owners and admins become its Admins.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects" \
  -H "Content-Type: application/json" -d '{"project_id":"proj18pzzkao"}'
```

**Responses**

- **201**: The project.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Move a project out

`DELETE /v2/organizations/{org_id}/projects/{project_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Memberships stay. Members with a custom role become Viewers, because the organization's roles no longer apply there.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `project_id` | string | yes | Project id (proj...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects/$PROJECT_ID"
```

**Responses**

- **200**: Moved out.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Set a project's data location

`POST /v2/organizations/{org_id}/projects/{project_id}/region` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Needs the `data_location` feature. On a deployment that enforces regions, a project stays in the region where its data is (422 with the other region's dashboard address, or "not available yet"); moving stored data is a support job. Elsewhere the region is recorded.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `project_id` | string | yes | Project id (proj...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `region` | `us`, `eu` | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects/$PROJECT_ID/region" \
  -H "Content-Type: application/json" -d '{"region":"eu"}'
```

**Responses**

- **200**: The project's region.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

### The organization audit log

`GET /v2/organizations/{org_id}/audit_logs` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Newest first. Project audit logs stay at `GET /v2/projects/{project_id}/audit_logs`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `start_time` | integer | no | Only entries at or after this time (epoch milliseconds). |
| `end_time` | integer | no | Only entries before this time (epoch milliseconds). |
| `limit` | integer | no | Page size. Values outside 1-100 are clamped, not rejected. |
| `starting_after` | string | no | Id of the last item of the previous page. Use `next_page` instead of building it. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/audit_logs"
```

**Responses**

- **200**: A page of entries. Returns a list of [OrganizationAuditLog](#organizationauditlog).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Custom roles

Roles built from API v2 scopes, their assignment to project members, and group role mappings for SSO and SCIM groups. See [Enterprise](https://revenuedot.app/docs/guides/enterprise.md#custom-roles).

### Scopes a custom role can hold

`GET /v2/organizations/{org_id}/scopes` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/scopes"
```

**Responses**

- **200**: 33 scopes in 5 groups.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "scope_catalogue",
  "groups": [
    {
      "group": "Customers",
      "scopes": [
        {
          "scope": "customer_information:customers:read",
          "label": "View customers"
        }
      ]
    }
  ]
}
```

### List custom roles

`GET /v2/organizations/{org_id}/roles` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/roles"
```

**Responses**

- **200**: Roles, oldest first. Returns a list of [CustomRole](#customrole).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "list",
  "items": [
    {
      "object": "custom_role",
      "id": "role_8f2kq0x1m3zv",
      "name": "Support agent",
      "description": "Refunds and customer lookups, no catalog changes.",
      "scopes": [
        "customer_information:customers:read",
        "customer_information:purchases:read_write",
        "customer_information:subscriptions:read_write"
      ],
      "project_id": null,
      "member_count": 4,
      "created_at": 1790800914012,
      "updated_at": 1790800914012
    }
  ],
  "next_page": null,
  "url": "/v2/organizations/org_k2m9q4x7z1a8/roles"
}
```

### Create a custom role

`POST /v2/organizations/{org_id}/roles` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Unknown scopes and `project_configuration:api_keys:read_write` are refused (400). A name already used in the organization answers 409.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `description` | string or null | no |  |
| `scopes` | array of string | yes |  |
| `project_id` | string or null | no | Limit the role to one project of the organization. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/roles" \
  -H "Content-Type: application/json" -d '{"name":"Support agent","description":"Refunds and customer lookups, no catalog changes.","scopes":["customer_information:customers:read","customer_information:purchases:read_write","customer_information:subscriptions:read_write"]}'
```

**Responses**

- **201**: The role. Returns [CustomRole](#customrole).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "custom_role",
  "id": "role_8f2kq0x1m3zv",
  "name": "Support agent",
  "description": "Refunds and customer lookups, no catalog changes.",
  "scopes": [
    "customer_information:customers:read",
    "customer_information:purchases:read_write",
    "customer_information:subscriptions:read_write"
  ],
  "project_id": null,
  "member_count": 4,
  "created_at": 1790800914012,
  "updated_at": 1790800914012
}
```

### Get a custom role

`GET /v2/organizations/{org_id}/roles/{role_id}` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `role_id` | string | yes | Role id (role_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/roles/$ROLE_ID"
```

**Responses**

- **200**: The role. Returns [CustomRole](#customrole).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "custom_role",
  "id": "role_8f2kq0x1m3zv",
  "name": "Support agent",
  "description": "Refunds and customer lookups, no catalog changes.",
  "scopes": [
    "customer_information:customers:read",
    "customer_information:purchases:read_write",
    "customer_information:subscriptions:read_write"
  ],
  "project_id": null,
  "member_count": 4,
  "created_at": 1790800914012,
  "updated_at": 1790800914012
}
```

### Update a custom role

`POST /v2/organizations/{org_id}/roles/{role_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). New scopes apply to everyone with the role on their next request. A role in use cannot change its project (400).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `role_id` | string | yes | Role id (role_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `description` | string or null | no |  |
| `scopes` | array of string | no |  |
| `project_id` | string or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/roles/$ROLE_ID" \
  -H "Content-Type: application/json" -d '{"scopes":["customer_information:customers:read","customer_information:purchases:read"]}'
```

**Responses**

- **200**: The role. Returns [CustomRole](#customrole).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "custom_role",
  "id": "role_8f2kq0x1m3zv",
  "name": "Support agent",
  "description": "Refunds and customer lookups, no catalog changes.",
  "scopes": [
    "customer_information:customers:read",
    "customer_information:purchases:read_write",
    "customer_information:subscriptions:read_write"
  ],
  "project_id": null,
  "member_count": 4,
  "created_at": 1790800914012,
  "updated_at": 1790800914012
}
```

### Delete a custom role

`DELETE /v2/organizations/{org_id}/roles/{role_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Everyone with the role becomes a Viewer, and group role mappings that gave it are deleted.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `role_id` | string | yes | Role id (role_...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/roles/$ROLE_ID"
```

**Responses**

- **200**: Deleted.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### A project's members with role names

`GET /v2/organizations/{org_id}/projects/{project_id}/members` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `project_id` | string | yes | Project id (proj...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects/$PROJECT_ID/members"
```

**Responses**

- **200**: The project's members. Returns a list of [OrganizationProjectMember](#organizationprojectmember).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Give a project member a built-in or custom role

`POST /v2/organizations/{org_id}/projects/{project_id}/members/{user_id}` · Auth: dashboard session · RevenueDot extension

Organization admins, or Admins of the project. The person must already be a member of the project (404). The role must be `admin`, `developer`, `viewer`, or a custom role of the organization for every project or for this one (400). The last Admin and the project owner stay Admins. A role set here is never changed by group role mappings.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `project_id` | string | yes | Project id (proj...). |
| `user_id` | string | yes | User id. |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | string | yes | `admin`, `developer`, `viewer` or a custom role id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/projects/$PROJECT_ID/members/$USER_ID" \
  -H "Content-Type: application/json" -d '{"role":"role_8f2kq0x1m3zv"}'
```

**Responses**

- **200**: The membership.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "project_member",
  "user_id": "usr_8f2kq0x1m3zv7a2b",
  "project_id": "proj18pzzkao",
  "role": "role_8f2kq0x1m3zv",
  "role_name": "Support agent"
}
```

### List group role mappings

`GET /v2/organizations/{org_id}/role_mappings` · Auth: dashboard session · RevenueDot extension

Any active member of the organization. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/role_mappings"
```

**Responses**

- **200**: Mappings by group. Returns a list of [RoleMapping](#rolemapping).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "list",
  "items": [
    {
      "object": "role_mapping",
      "id": "map_4f8k2m9q1x7z",
      "group": "RevenueDot Support",
      "project_id": "proj18pzzkao",
      "project_name": "Scanner",
      "role": "role_8f2kq0x1m3zv",
      "role_name": "Support agent",
      "created_at": 1790800914012
    }
  ],
  "next_page": null,
  "url": "/v2/organizations/org_k2m9q4x7z1a8/role_mappings"
}
```

### Map a group to a role in a project

`POST /v2/organizations/{org_id}/role_mappings` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). One mapping per group and project: saving the same pair again replaces its role (200). Every member's access is re-applied at once; `memberships_changed` counts the changes.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `group` | string | yes |  |
| `project_id` | string | yes |  |
| `role` | string | yes | `admin`, `developer`, `viewer` or a custom role id. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/role_mappings" \
  -H "Content-Type: application/json" -d '{"group":"RevenueDot Support","project_id":"proj18pzzkao","role":"role_8f2kq0x1m3zv"}'
```

**Responses**

- **200**: Replaced.
- **201**: Created.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Delete a group role mapping

`DELETE /v2/organizations/{org_id}/role_mappings/{mapping_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Memberships that the mapping created are removed or lowered at once.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `mapping_id` | string | yes | Mapping id (map_...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/role_mappings/$MAPPING_ID"
```

**Responses**

- **200**: Deleted.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Single sign-on

SAML 2.0 and OpenID Connect connections, verified email domains, and the public sign-in endpoints the browser and the identity provider call. See [Single sign-on](https://revenuedot.app/docs/guides/single-sign-on.md).

### List SSO connections

`GET /v2/organizations/{org_id}/sso/connections` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/connections"
```

**Responses**

- **200**: Connections, oldest first. Returns a list of [SsoConnection](#ssoconnection).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a SAML or OpenID Connect connection

`POST /v2/organizations/{org_id}/sso/connections` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Send `saml` for a SAML connection or `oidc` for OpenID Connect. A connection starts turned off unless `enabled` is true. The answer's `sp` holds the values to enter in the identity provider.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | `saml`, `oidc` | yes |  |
| `name` | string | yes |  |
| `enabled` | boolean | no |  |
| `jit` | boolean | no | Default true. |
| `saml` | object | no |  |
| `saml.metadata_xml` | string | no | The identity provider's metadata XML. Fills the entity ID, the HTTP-Redirect sign-in URL and the signing certificates. |
| `saml.idp_entity_id` | string | no |  |
| `saml.idp_sso_url` | string | no |  |
| `saml.idp_certificates` | array of string | no |  |
| `saml.allow_idp_initiated` | boolean | no |  |
| `saml.email_attribute` | string or null | no |  |
| `saml.first_name_attribute` | string or null | no |  |
| `saml.last_name_attribute` | string or null | no |  |
| `saml.groups_attribute` | string or null | no |  |
| `oidc` | object | no |  |
| `oidc.issuer` | string | no |  |
| `oidc.client_id` | string | no |  |
| `oidc.client_secret` | string or null | no | Stored encrypted, never returned. Null removes it. |
| `oidc.scopes` | array of string | no |  |
| `oidc.groups_claim` | string or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/connections" \
  -H "Content-Type: application/json" -d '{"kind":"oidc","name":"Google Workspace","oidc":{"issuer":"https://accounts.google.com","client_id":"1234567890-abc.apps.googleusercontent.com","client_secret":"GOCSPX-..."}}'
```

**Responses**

- **201**: The connection. Returns [SsoConnection](#ssoconnection).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "sso_connection",
  "id": "ssoc_p3k8x2m4q9z1",
  "org_id": "org_k2m9q4x7z1a8",
  "kind": "saml",
  "name": "Okta",
  "enabled": true,
  "jit": true,
  "created_at": 1790800914012,
  "updated_at": 1790800914012,
  "saml": {
    "idp_entity_id": "http://www.okta.com/exk1a2b3c4d5",
    "idp_sso_url": "https://acme.okta.com/app/acme_revenuedot_1/exk1a2b3c4d5/sso/saml",
    "idp_certificates": [
      "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
    ],
    "allow_idp_initiated": false,
    "email_attribute": null,
    "first_name_attribute": null,
    "last_name_attribute": null,
    "groups_attribute": "groups"
  },
  "sp": {
    "entity_id": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "acs_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/acs",
    "metadata_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "start_url": "https://app.revenuedot.app/sso/connections/ssoc_p3k8x2m4q9z1/start"
  }
}
```

### Get an SSO connection

`GET /v2/organizations/{org_id}/sso/connections/{connection_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `connection_id` | string | yes | Connection id (ssoc_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/connections/$CONNECTION_ID"
```

**Responses**

- **200**: The connection. Returns [SsoConnection](#ssoconnection).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "sso_connection",
  "id": "ssoc_p3k8x2m4q9z1",
  "org_id": "org_k2m9q4x7z1a8",
  "kind": "saml",
  "name": "Okta",
  "enabled": true,
  "jit": true,
  "created_at": 1790800914012,
  "updated_at": 1790800914012,
  "saml": {
    "idp_entity_id": "http://www.okta.com/exk1a2b3c4d5",
    "idp_sso_url": "https://acme.okta.com/app/acme_revenuedot_1/exk1a2b3c4d5/sso/saml",
    "idp_certificates": [
      "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
    ],
    "allow_idp_initiated": false,
    "email_attribute": null,
    "first_name_attribute": null,
    "last_name_attribute": null,
    "groups_attribute": "groups"
  },
  "sp": {
    "entity_id": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "acs_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/acs",
    "metadata_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "start_url": "https://app.revenuedot.app/sso/connections/ssoc_p3k8x2m4q9z1/start"
  }
}
```

### Update an SSO connection

`POST /v2/organizations/{org_id}/sso/connections/{connection_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Fields you leave out keep their value. An organization that requires SSO cannot turn off its last enabled connection (422).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `connection_id` | string | yes | Connection id (ssoc_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `enabled` | boolean | no |  |
| `jit` | boolean | no |  |
| `saml` | object | no |  |
| `saml.metadata_xml` | string | no | The identity provider's metadata XML. Fills the entity ID, the HTTP-Redirect sign-in URL and the signing certificates. |
| `saml.idp_entity_id` | string | no |  |
| `saml.idp_sso_url` | string | no |  |
| `saml.idp_certificates` | array of string | no |  |
| `saml.allow_idp_initiated` | boolean | no |  |
| `saml.email_attribute` | string or null | no |  |
| `saml.first_name_attribute` | string or null | no |  |
| `saml.last_name_attribute` | string or null | no |  |
| `saml.groups_attribute` | string or null | no |  |
| `oidc` | object | no |  |
| `oidc.issuer` | string | no |  |
| `oidc.client_id` | string | no |  |
| `oidc.client_secret` | string or null | no | Stored encrypted, never returned. Null removes it. |
| `oidc.scopes` | array of string | no |  |
| `oidc.groups_claim` | string or null | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/connections/$CONNECTION_ID" \
  -H "Content-Type: application/json" -d '{"enabled":true}'
```

**Responses**

- **200**: The connection. Returns [SsoConnection](#ssoconnection).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "sso_connection",
  "id": "ssoc_p3k8x2m4q9z1",
  "org_id": "org_k2m9q4x7z1a8",
  "kind": "saml",
  "name": "Okta",
  "enabled": true,
  "jit": true,
  "created_at": 1790800914012,
  "updated_at": 1790800914012,
  "saml": {
    "idp_entity_id": "http://www.okta.com/exk1a2b3c4d5",
    "idp_sso_url": "https://acme.okta.com/app/acme_revenuedot_1/exk1a2b3c4d5/sso/saml",
    "idp_certificates": [
      "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
    ],
    "allow_idp_initiated": false,
    "email_attribute": null,
    "first_name_attribute": null,
    "last_name_attribute": null,
    "groups_attribute": "groups"
  },
  "sp": {
    "entity_id": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "acs_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/acs",
    "metadata_url": "https://app.revenuedot.app/sso/saml/ssoc_p3k8x2m4q9z1/metadata",
    "start_url": "https://app.revenuedot.app/sso/connections/ssoc_p3k8x2m4q9z1/start"
  }
}
```

### Delete an SSO connection

`DELETE /v2/organizations/{org_id}/sso/connections/{connection_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). An organization that requires SSO cannot delete its last enabled connection (422).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `connection_id` | string | yes | Connection id (ssoc_...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/connections/$CONNECTION_ID"
```

**Responses**

- **200**: Deleted.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **422**: The request is valid but cannot be done in this state or for this store. Returns [V2Error](#v2error).

### List email domains

`GET /v2/organizations/{org_id}/sso/domains` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/domains"
```

**Responses**

- **200**: Domains, oldest first. Returns a list of [SsoDomain](#ssodomain).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Add an email domain

`POST /v2/organizations/{org_id}/sso/domains` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Answers the TXT record to publish. Public mail providers such as gmail.com are refused (400). A domain another organization verified answers 409; an unverified claim by another organization does not block you. Adding a domain you already have answers 200.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/domains" \
  -H "Content-Type: application/json" -d '{"domain":"acme.com"}'
```

**Responses**

- **200**: Already added. Returns [SsoDomain](#ssodomain).
- **201**: The domain, not verified yet. Returns [SsoDomain](#ssodomain).
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **409**: It already exists, or it conflicts with another object. Returns [V2Error](#v2error).

### Check the domain's TXT record

`POST /v2/organizations/{org_id}/sso/domains/{domain}/actions/verify` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Looks up `_revenuedot-sso.<domain>` with DNS over HTTPS (Cloudflare's resolver). At most 10 checks a minute per organization (429). A verified domain stays verified if a later check fails.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `domain` | string | yes | The domain, such as acme.com. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/domains/acme.com/actions/verify"
```

**Responses**

- **200**: The domain, with the TXT values found.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).
- **429**: Too many requests. Retry later. Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "sso_domain",
  "domain": "acme.com",
  "verified": true,
  "verified_at": 1790801000000,
  "txt_record": {
    "type": "TXT",
    "name": "_revenuedot-sso.acme.com",
    "value": "revenuedot-sso-verification=3f9a1c0e7b2d4a6f8e1c3b5a7d9f0e2c"
  },
  "last_checked_at": 1790801000000,
  "last_error": null,
  "created_at": 1790800914012,
  "found": {
    "txt": [
      "revenuedot-sso-verification=3f9a1c0e7b2d4a6f8e1c3b5a7d9f0e2c"
    ]
  }
}
```

### Remove an email domain

`DELETE /v2/organizations/{org_id}/sso/domains/{domain}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `domain` | string | yes | The domain, such as acme.com. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/sso/domains/acme.com"
```

**Responses**

- **200**: Removed.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Does this address sign in with SSO?

`POST /sso/lookup` · Auth: none · RevenueDot extension

No session. True when the address is on a domain an organization verified and that organization has an enabled connection.

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | yes |  |
| `next` | string | no | A path on this site to open after sign-in. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/sso/lookup" \
  -H "Content-Type: application/json" -d '{"email":"sam@acme.com"}'
```

**Responses**

- **200**: The answer.

Example 200 response:

```json
{
  "sso": true,
  "url": "/sso/start?email=sam%40acme.com"
}
```

### Start a sign-in for an email address

`GET /sso/start` · Auth: none · RevenueDot extension

Opens in the browser. Finds the organization that verified the address's domain and sends the browser to its first enabled connection (oldest first). At most 30 starts a minute per IP address. Failures redirect to `/login?sso_error=<code>`, one of `failed`, `connection_off`, `rate_limited`, `not_set_up`, `domain_not_verified`, `access_removed`, `not_a_member`, `other_browser` or `idp_error`; the sign-in page shows the message for the code, and the exact reason goes to the organization audit log as `sso_sign_in_failed`.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `email` | string | yes | The work email address. |
| `next` | string | no | A path on this site to open after sign-in. Anything else becomes `/`. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/sso/start"
```

**Responses**

- **303**: To the identity provider, or to `/login?sso_error=...`.

### Start a sign-in with one connection

`GET /sso/connections/{id}/start` · Auth: none · RevenueDot extension

The `start_url` of a connection. Failures redirect to `/login?sso_error=<code>`, one of `failed`, `connection_off`, `rate_limited`, `not_set_up`, `domain_not_verified`, `access_removed`, `not_a_member`, `other_browser` or `idp_error`; the sign-in page shows the message for the code, and the exact reason goes to the organization audit log as `sso_sign_in_failed`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Connection id (ssoc_...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `next` | string | no | A path on this site to open after sign-in. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/sso/connections/$ID/start"
```

**Responses**

- **303**: To the identity provider, or to `/login?sso_error=...`.

### SAML service provider metadata

`GET /sso/saml/{id}/metadata` · Auth: none · RevenueDot extension

This URL is also the connection's entity ID.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Connection id (ssoc_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/sso/saml/$ID/metadata"
```

**Responses**

- **200**: The metadata.
- **404**: No SAML connection with this id.

### SAML assertion consumer service

`POST /sso/saml/{id}/acs` · Auth: none · RevenueDot extension

The identity provider posts the SAML response here (HTTP-POST binding). RevenueDot checks the assertion's signature, audience, recipient, destination, issuer, validity times (60 seconds of leeway) and that the answer belongs to a sign-in this browser started; each assertion is accepted once. Responses without an InResponseTo are IdP-initiated and are refused unless the connection allows them. On success it sets the session cookie. Failures redirect to `/login?sso_error=<code>`, one of `failed`, `connection_off`, `rate_limited`, `not_set_up`, `domain_not_verified`, `access_removed`, `not_a_member`, `other_browser` or `idp_error`; the sign-in page shows the message for the code, and the exact reason goes to the organization audit log as `sso_sign_in_failed`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Connection id (ssoc_...). |

**Request body** (`application/x-www-form-urlencoded`)

| Field | Type | Required | Description |
|---|---|---|---|
| `SAMLResponse` | string | yes | Base64 SAML response. |
| `RelayState` | string | no | For IdP-initiated sign-in: a path to open after sign-in. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/sso/saml/$ID/acs"
```

**Responses**

- **303**: To the page the person started from, or to `/login?sso_error=...`.

### OpenID Connect redirect URI

`GET /sso/oidc/{id}/callback` · Auth: none · RevenueDot extension

The identity provider sends the browser back here. RevenueDot checks the state against the browser that started the sign-in, exchanges the code with PKCE, and verifies the ID token (issuer, audience, expiry, nonce, `azp`). The token must have an `email` claim, and `email_verified` must not be false. Failures redirect to `/login?sso_error=<code>`, one of `failed`, `connection_off`, `rate_limited`, `not_set_up`, `domain_not_verified`, `access_removed`, `not_a_member`, `other_browser` or `idp_error`; the sign-in page shows the message for the code, and the exact reason goes to the organization audit log as `sso_sign_in_failed`.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Connection id (ssoc_...). |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `state` | string | no |  |
| `code` | string | no |  |
| `error` | string | no |  |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/sso/oidc/$ID/callback"
```

**Responses**

- **303**: To the page the person started from, or to `/login?sso_error=...`.

## SCIM 2.0

SCIM tokens and groups in the dashboard API, and the SCIM 2.0 service (RFC 7643, RFC 7644) that Okta, Microsoft Entra ID and other identity providers call with a SCIM token. Bodies and errors use `application/scim+json`. See [SCIM](https://revenuedot.app/docs/guides/scim.md).

### List SCIM tokens

`GET /v2/organizations/{org_id}/scim/tokens` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Never returns the secret.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/scim/tokens"
```

**Responses**

- **200**: Tokens, revoked ones included. Returns a list of [ScimToken](#scimtoken).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Create a SCIM token

`POST /v2/organizations/{org_id}/scim/tokens` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). The answer holds the token (`rdscim_` and 64 hex characters) once; RevenueDot stores only its SHA-256. `base_url` is the SCIM base URL for the identity provider.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/v2/organizations/$ORG_ID/scim/tokens" \
  -H "Content-Type: application/json" -d '{"name":"Okta"}'
```

**Responses**

- **201**: The token, shown once.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 201 response:

```json
{
  "object": "scim_token",
  "id": "sct_m3k9q2x8z1a4",
  "name": "Okta",
  "prefix": "rdscim_4f8a1c",
  "created_by": "usr_8f2kq0x1m3zv7a2b",
  "created_at": 1790800914012,
  "last_used_at": null,
  "revoked_at": null,
  "token": "rdscim_4f8a1c…",
  "base_url": "https://app.revenuedot.app/scim/v2"
}
```

### Revoke a SCIM token

`DELETE /v2/organizations/{org_id}/scim/tokens/{token_id}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). Requests with the token answer 401 at once. Revoking twice answers the token again.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `token_id` | string | yes | Token id (sct_...). |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/v2/organizations/$ORG_ID/scim/tokens/$TOKEN_ID"
```

**Responses**

- **200**: The revoked token. Returns [ScimToken](#scimtoken).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Groups the identity provider pushed

`GET /v2/organizations/{org_id}/scim/groups` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). For picking groups in the role mapping editor.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/scim/groups"
```

**Responses**

- **200**: Groups by name.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

### Service provider configuration

`GET /scim/v2/ServiceProviderConfig` · Auth: SCIM token · RevenueDot extension

PATCH, filters (at most 200 results) and ETags are supported. Bulk, sorting and password changes are not.

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/ServiceProviderConfig" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: RFC 7643 §5.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).

### Resource types (User, Group)

`GET /scim/v2/ResourceTypes` · Auth: SCIM token · RevenueDot extension

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/ResourceTypes" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: A ListResponse of User and Group. Returns [ScimListResponse](#scimlistresponse).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).

### One resource type

`GET /scim/v2/ResourceTypes/{name}` · Auth: SCIM token · RevenueDot extension

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | User or Group. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/ResourceTypes/$NAME" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: The resource type.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).

### Schemas (User, Group, enterprise User)

`GET /scim/v2/Schemas` · Auth: SCIM token · RevenueDot extension

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Schemas" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: A ListResponse of schemas. Returns [ScimListResponse](#scimlistresponse).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).

### One schema

`GET /scim/v2/Schemas/{id}` · Auth: SCIM token · RevenueDot extension

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Schema URN. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Schemas/$ID" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: The schema.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).

### List or find users

`GET /scim/v2/Users` · Auth: SCIM token · RevenueDot extension

Okta and Entra look a user up with `filter=userName eq "..."` before creating them.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `filter` | string | no | RFC 7644 filter: `eq ne co sw ew pr gt ge lt le`, `and`, `or`, `not`, parentheses and value paths such as `emails[type eq "work"].value`. |
| `startIndex` | integer | no | 1-based. |
| `count` | integer | no | Page size, at most 200. |
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Users" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: A ListResponse of Users. Returns [ScimListResponse](#scimlistresponse).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:ListResponse"
  ],
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 1,
  "Resources": [
    {
      "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User"
      ],
      "id": "scu_9x2k4m8q1z7a3b5c",
      "externalId": "00u1a2b3c4d5",
      "userName": "sam@acme.com",
      "name": {
        "givenName": "Sam",
        "familyName": "Lee"
      },
      "emails": [
        {
          "value": "sam@acme.com",
          "type": "work",
          "primary": true
        }
      ],
      "active": true,
      "groups": [
        {
          "value": "scg_4k8m2q9x1z7a3b5c",
          "display": "RevenueDot Support"
        }
      ],
      "meta": {
        "resourceType": "User",
        "created": "2026-10-01T09:12:00.000Z",
        "lastModified": "2026-10-01T09:12:00.000Z",
        "location": "https://app.revenuedot.app/scim/v2/Users/scu_9x2k4m8q1z7a3b5c",
        "version": "W/\"1\""
      }
    }
  ]
}
```

### Create a user

`POST /scim/v2/Users` · Auth: SCIM token · RevenueDot extension

The email (primary, else work, else first address, else an email-shaped userName) must be on a domain the organization verified. An existing RevenueDot account with that address is linked; otherwise an account without a password is created. The person becomes an active organization member and their group role mappings apply. `password` is ignored.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | no |  |
| `id` | string | no | SCIM user id (scu_...). |
| `externalId` | string | no | The identity provider's id. |
| `userName` | string | yes | Unique in the organization, without regard to case. |
| `name` | object | no |  |
| `name.formatted` | string | no |  |
| `name.givenName` | string | no |  |
| `name.familyName` | string | no |  |
| `displayName` | string | no |  |
| `emails` | array of object | no | The primary address (else the work address, else the first) is the account's email. It must be on a verified domain. |
| `emails[].value` | string | no |  |
| `emails[].type` | string | no |  |
| `emails[].primary` | boolean | no |  |
| `active` | boolean | no | False deprovisions the person. |
| `groups` | array of object | no | Read only. |
| `groups[].value` | string | no | SCIM group id. |
| `groups[].display` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no | Weak ETag such as `W/"3"`. |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/scim/v2/Users" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"sam@acme.com","name":{"givenName":"Sam","familyName":"Lee"},"emails":[{"value":"sam@acme.com","type":"work","primary":true}],"active":true}'
```

**Responses**

- **201**: The user. Returns [ScimUser](#scimuser).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 201 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User"
  ],
  "id": "scu_9x2k4m8q1z7a3b5c",
  "externalId": "00u1a2b3c4d5",
  "userName": "sam@acme.com",
  "name": {
    "givenName": "Sam",
    "familyName": "Lee"
  },
  "emails": [
    {
      "value": "sam@acme.com",
      "type": "work",
      "primary": true
    }
  ],
  "active": true,
  "groups": [
    {
      "value": "scg_4k8m2q9x1z7a3b5c",
      "display": "RevenueDot Support"
    }
  ],
  "meta": {
    "resourceType": "User",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Users/scu_9x2k4m8q1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Get a user

`GET /scim/v2/Users/{id}` · Auth: SCIM token · RevenueDot extension

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-None-Match` | string | no | Optional. Answers 304 when the ETag still matches. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Users/$ID" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: The user. Returns [ScimUser](#scimuser).
- **304**: Not modified.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User"
  ],
  "id": "scu_9x2k4m8q1z7a3b5c",
  "externalId": "00u1a2b3c4d5",
  "userName": "sam@acme.com",
  "name": {
    "givenName": "Sam",
    "familyName": "Lee"
  },
  "emails": [
    {
      "value": "sam@acme.com",
      "type": "work",
      "primary": true
    }
  ],
  "active": true,
  "groups": [
    {
      "value": "scg_4k8m2q9x1z7a3b5c",
      "display": "RevenueDot Support"
    }
  ],
  "meta": {
    "resourceType": "User",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Users/scu_9x2k4m8q1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Replace a user

`PUT /scim/v2/Users/{id}` · Auth: SCIM token · RevenueDot extension

`active: false` deprovisions: the person loses every membership in the organization's projects and every session ends. `active: true` again restores group-mapped access. A new email, or a reactivation, must be on a verified domain. The organization's last owner cannot be deactivated (400).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | no |  |
| `id` | string | no | SCIM user id (scu_...). |
| `externalId` | string | no | The identity provider's id. |
| `userName` | string | yes | Unique in the organization, without regard to case. |
| `name` | object | no |  |
| `name.formatted` | string | no |  |
| `name.givenName` | string | no |  |
| `name.familyName` | string | no |  |
| `displayName` | string | no |  |
| `emails` | array of object | no | The primary address (else the work address, else the first) is the account's email. It must be on a verified domain. |
| `emails[].value` | string | no |  |
| `emails[].type` | string | no |  |
| `emails[].primary` | boolean | no |  |
| `active` | boolean | no | False deprovisions the person. |
| `groups` | array of object | no | Read only. |
| `groups[].value` | string | no | SCIM group id. |
| `groups[].display` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no | Weak ETag such as `W/"3"`. |

**Example request**

```bash
curl -s -X PUT "$REVENUEDOT_URL/scim/v2/Users/$ID" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"sam@acme.com","name":{"givenName":"Sam","familyName":"Lee"},"emails":[{"value":"sam@acme.com","type":"work","primary":true}],"active":true}'
```

**Responses**

- **200**: The user. Returns [ScimUser](#scimuser).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User"
  ],
  "id": "scu_9x2k4m8q1z7a3b5c",
  "externalId": "00u1a2b3c4d5",
  "userName": "sam@acme.com",
  "name": {
    "givenName": "Sam",
    "familyName": "Lee"
  },
  "emails": [
    {
      "value": "sam@acme.com",
      "type": "work",
      "primary": true
    }
  ],
  "active": true,
  "groups": [
    {
      "value": "scg_4k8m2q9x1z7a3b5c",
      "display": "RevenueDot Support"
    }
  ],
  "meta": {
    "resourceType": "User",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Users/scu_9x2k4m8q1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Change a user

`PATCH /scim/v2/Users/{id}` · Auth: SCIM token · RevenueDot extension

RFC 7644 PATCH. Also accepts Okta's `replace` without a path and Entra's capitalised `op` values, `"False"` strings and dotted keys. Same effects as PUT.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes | Must contain `urn:ietf:params:scim:api:messages:2.0:PatchOp`. |
| `Operations` | array of object | yes |  |
| `Operations[].op` | string | yes | add, replace or remove; any case. |
| `Operations[].path` | string | no | Optional for add and replace. Value filters such as `emails[type eq "work"].value` work. |
| `Operations[].value` | object | no |  |

**Example request**

```bash
curl -s -X PATCH "$REVENUEDOT_URL/scim/v2/Users/$ID" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","path":"active","value":false}]}'
```

**Responses**

- **200**: The user. Returns [ScimUser](#scimuser).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:User"
  ],
  "id": "scu_9x2k4m8q1z7a3b5c",
  "externalId": "00u1a2b3c4d5",
  "userName": "sam@acme.com",
  "name": {
    "givenName": "Sam",
    "familyName": "Lee"
  },
  "emails": [
    {
      "value": "sam@acme.com",
      "type": "work",
      "primary": true
    }
  ],
  "active": true,
  "groups": [
    {
      "value": "scg_4k8m2q9x1z7a3b5c",
      "display": "RevenueDot Support"
    }
  ],
  "meta": {
    "resourceType": "User",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Users/scu_9x2k4m8q1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Delete a user

`DELETE /scim/v2/Users/{id}` · Auth: SCIM token · RevenueDot extension

Deprovisions the person like `active: false`, then deletes the SCIM user. The RevenueDot account and its audit history stay.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/scim/v2/Users/$ID" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **204**: Deleted.
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).

### List or find groups

`GET /scim/v2/Groups` · Auth: SCIM token · RevenueDot extension

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `filter` | string | no | RFC 7644 filter: `eq ne co sw ew pr gt ge lt le`, `and`, `or`, `not`, parentheses and value paths such as `emails[type eq "work"].value`. |
| `startIndex` | integer | no | 1-based. |
| `count` | integer | no | Page size, at most 200. |
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Groups" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: A ListResponse of Groups. Returns [ScimListResponse](#scimlistresponse).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:api:messages:2.0:ListResponse"
  ],
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 1,
  "Resources": [
    {
      "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:Group"
      ],
      "id": "scg_4k8m2q9x1z7a3b5c",
      "displayName": "RevenueDot Support",
      "members": [
        {
          "value": "scu_9x2k4m8q1z7a3b5c",
          "display": "Sam Lee",
          "type": "User"
        }
      ],
      "meta": {
        "resourceType": "Group",
        "created": "2026-10-01T09:12:00.000Z",
        "lastModified": "2026-10-01T09:12:00.000Z",
        "location": "https://app.revenuedot.app/scim/v2/Groups/scg_4k8m2q9x1z7a3b5c",
        "version": "W/\"1\""
      }
    }
  ]
}
```

### Create a group

`POST /scim/v2/Groups` · Auth: SCIM token · RevenueDot extension

Members must be SCIM Users of the organization (400 otherwise). Group role mappings with this name apply to the members at once.

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | no |  |
| `id` | string | no | SCIM group id (scg_...). |
| `externalId` | string | no |  |
| `displayName` | string | yes | Unique in the organization. Role mappings match it. |
| `members` | array of object | no |  |
| `members[].value` | string | no | SCIM user id. |
| `members[].display` | string | no |  |
| `members[].type` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no |  |

**Example request**

```bash
curl -s -X POST "$REVENUEDOT_URL/scim/v2/Groups" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],"displayName":"RevenueDot Support","members":[{"value":"scu_9x2k4m8q1z7a3b5c"}]}'
```

**Responses**

- **201**: The group. Returns [ScimGroup](#scimgroup).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 201 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ],
  "id": "scg_4k8m2q9x1z7a3b5c",
  "displayName": "RevenueDot Support",
  "members": [
    {
      "value": "scu_9x2k4m8q1z7a3b5c",
      "display": "Sam Lee",
      "type": "User"
    }
  ],
  "meta": {
    "resourceType": "Group",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Groups/scg_4k8m2q9x1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Get a group

`GET /scim/v2/Groups/{id}` · Auth: SCIM token · RevenueDot extension

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-None-Match` | string | no | Optional. Answers 304 when the ETag still matches. |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/scim/v2/Groups/$ID" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **200**: The group. Returns [ScimGroup](#scimgroup).
- **304**: Not modified.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ],
  "id": "scg_4k8m2q9x1z7a3b5c",
  "displayName": "RevenueDot Support",
  "members": [
    {
      "value": "scu_9x2k4m8q1z7a3b5c",
      "display": "Sam Lee",
      "type": "User"
    }
  ],
  "meta": {
    "resourceType": "Group",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Groups/scg_4k8m2q9x1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Replace a group

`PUT /scim/v2/Groups/{id}` · Auth: SCIM token · RevenueDot extension

Replaces the name and the member list. Access is re-applied for everyone who joined or left, and for every member after a rename.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | no |  |
| `id` | string | no | SCIM group id (scg_...). |
| `externalId` | string | no |  |
| `displayName` | string | yes | Unique in the organization. Role mappings match it. |
| `members` | array of object | no |  |
| `members[].value` | string | no | SCIM user id. |
| `members[].display` | string | no |  |
| `members[].type` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no |  |

**Example request**

```bash
curl -s -X PUT "$REVENUEDOT_URL/scim/v2/Groups/$ID" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],"displayName":"RevenueDot Support","members":[{"value":"scu_9x2k4m8q1z7a3b5c"}]}'
```

**Responses**

- **200**: The group. Returns [ScimGroup](#scimgroup).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ],
  "id": "scg_4k8m2q9x1z7a3b5c",
  "displayName": "RevenueDot Support",
  "members": [
    {
      "value": "scu_9x2k4m8q1z7a3b5c",
      "display": "Sam Lee",
      "type": "User"
    }
  ],
  "meta": {
    "resourceType": "Group",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Groups/scg_4k8m2q9x1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Change a group

`PATCH /scim/v2/Groups/{id}` · Auth: SCIM token · RevenueDot extension

Add or remove members (`remove` with `members[value eq "..."]`), or rename.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `attributes` | string | no | Comma-separated attributes to return. |
| `excludedAttributes` | string | no | Comma-separated attributes to leave out (`members` on Groups is common). |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Request body** (`application/scim+json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes | Must contain `urn:ietf:params:scim:api:messages:2.0:PatchOp`. |
| `Operations` | array of object | yes |  |
| `Operations[].op` | string | yes | add, replace or remove; any case. |
| `Operations[].path` | string | no | Optional for add and replace. Value filters such as `emails[type eq "work"].value` work. |
| `Operations[].value` | object | no |  |

**Example request**

```bash
curl -s -X PATCH "$REVENUEDOT_URL/scim/v2/Groups/$ID" -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" -d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"add","path":"members","value":[{"value":"scu_9x2k4m8q1z7a3b5c"}]}]}'
```

**Responses**

- **200**: The group. Returns [ScimGroup](#scimgroup).
- **400**: The request, filter or PATCH is invalid. Returns [ScimError](#scimerror).
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **409**: The userName, email or group name is taken. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).
- **413**: The body is larger than 1 MB. Returns [ScimError](#scimerror).

Example 200 response:

```json
{
  "schemas": [
    "urn:ietf:params:scim:schemas:core:2.0:Group"
  ],
  "id": "scg_4k8m2q9x1z7a3b5c",
  "displayName": "RevenueDot Support",
  "members": [
    {
      "value": "scu_9x2k4m8q1z7a3b5c",
      "display": "Sam Lee",
      "type": "User"
    }
  ],
  "meta": {
    "resourceType": "Group",
    "created": "2026-10-01T09:12:00.000Z",
    "lastModified": "2026-10-01T09:12:00.000Z",
    "location": "https://app.revenuedot.app/scim/v2/Groups/scg_4k8m2q9x1z7a3b5c",
    "version": "W/\"1\""
  }
}
```

### Delete a group

`DELETE /scim/v2/Groups/{id}` · Auth: SCIM token · RevenueDot extension

Access that came from the group's role mappings is removed from its former members.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The SCIM id. |

**Headers**

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | Optional. The ETag you read; a changed resource answers 412. |

**Example request**

```bash
curl -s -X DELETE "$REVENUEDOT_URL/scim/v2/Groups/$ID" -H "Authorization: Bearer $SCIM_TOKEN"
```

**Responses**

- **204**: Deleted.
- **401**: No token, or a revoked or unknown one. Returns [ScimError](#scimerror).
- **404**: Not found in the token's organization. Returns [ScimError](#scimerror).
- **412**: If-Match does not match the current version. Returns [ScimError](#scimerror).

## Compliance exports

The audit log and an access review as CSV or JSON, signed with Ed25519. See [Audit retention and exports](https://revenuedot.app/docs/guides/audit-retention-and-exports.md).

### The public key exports are signed with

`GET /v2/organizations/{org_id}/exports/public_key` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise).

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/exports/public_key"
```

**Responses**

- **200**: The key.
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

Example 200 response:

```json
{
  "object": "export_signing_key",
  "algorithm": "Ed25519",
  "public_key": "kQ3s0x8b1m5Zt2Wq7yJcVhE9nR4uLpA6fG0dK3oT1sY=",
  "key_id": "4be1c0f29a7d3e58",
  "signs": true
}
```

### Download a signed audit log or access review

`GET /v2/organizations/{org_id}/exports/{kind}` · Auth: dashboard session · RevenueDot extension

Organization owners and admins. When the organization requires single sign-on, people on its verified domains other than owners need a session that began with its SSO (403 otherwise). At most 200,000 rows per file; more answers 400, so choose a shorter date range. CSV cells that start with `=`, `+`, `-`, `@`, a tab or a carriage return get a leading apostrophe. Each download is recorded in the organization audit log as `compliance_export_created` with its SHA-256.

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `org_id` | string | yes | Organization id (org_...). |
| `kind` | `audit_logs`, `access_review` | yes | `audit_logs`: the organization log and its projects' logs, oldest first. `access_review`: one row per person per organization project, plus members without project access. |

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `format` | `csv`, `json` | no | Default csv. |
| `start_time` | integer | no | Audit logs only: entries at or after this time (epoch milliseconds). |
| `end_time` | integer | no | Audit logs only: entries before this time (epoch milliseconds). |

**Example request**

```bash
curl -s "$REVENUEDOT_URL/v2/organizations/$ORG_ID/exports/$KIND"
```

**Responses**

- **200**: The file, as an attachment.
- **400**: The request is invalid. Returns [V2Error](#v2error).
- **401**: No API key, or an unknown one. Returns [V2Error](#v2error).
- **403**: The key lacks a permission, or a public key was used. Returns [V2Error](#v2error).
- **404**: Not found in this project (another project's ids also answer 404). Returns [V2Error](#v2error).

## Objects

The shapes the operations above send and return.

### CustomRole

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"custom_role"` | yes |  |
| `id` | string | yes | Role id (role_...). |
| `name` | string | yes | Unique in the organization. |
| `description` | string or null | no |  |
| `scopes` | array of string | yes | API v2 scopes from `GET /v2/organizations/{org_id}/scopes`, sorted. |
| `project_id` | string or null | no | The one project the role is for. Null: every project of the organization. |
| `member_count` | integer | no | Project memberships that use the role. |
| `created_at` | integer | no | Creation time. Epoch milliseconds. |
| `updated_at` | integer | no | Last change. Epoch milliseconds. |

### EnterpriseStatus

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"enterprise"` | yes |  |
| `mode` | `licensed`, `development`, `invalid` | yes | `licensed`: a valid licence key. `development`: `REVENUEDOT_EE_DEV=true`, for development and testing only. `invalid`: a key that failed or expired more than 14 days ago; no feature is on. |
| `features` | array of `organizations`, `custom_roles`, `sso`, `scim`, `data_location`, `audit_retention`, `compliance_exports` | yes | The features that are on. |
| `licensee` | string or null | no | Who the licence is for. |
| `expires_at` | integer or null | no | When the licence expires. Features keep working for 14 days after it. Epoch milliseconds, or null. |
| `message` | string or null | no | Why the licence is invalid, or a renewal warning. |

### Organization

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"organization"` | yes |  |
| `id` | string | yes | Organization id (org_...). |
| `name` | string | yes |  |
| `your_role` | `owner`, `admin`, `member` | yes | Your role in the organization. |
| `region` | `us`, `eu` | yes | Default data location for the organization's projects. |
| `region_name` | string | no | United States or European Union. |
| `selectable_regions` | array of `us`, `eu` | no | Regions this server accepts. A self-hosted server accepts both. A deployment that enforces regions accepts only its own. |
| `region_enforced` | boolean | no | True when this deployment refuses requests for projects stored in another region. |
| `audit_retention_days` | integer or null | no | Days audit log rows are kept (30 to 3650). Null keeps them forever. |
| `sso_enforced` | boolean | no | Whether people on the organization's verified domains must sign in with single sign-on. |
| `seats` | object | no |  |
| `seats.purchased` | integer or null | no | Seats bought. Recorded, not enforced. |
| `seats.used` | integer | no | People who are active members or members of one of the organization's projects. |
| `billing_email` | string or null | no |  |
| `member_count` | integer | no | Active members. |
| `project_count` | integer | no |  |
| `features` | array of string | no | The enterprise features this server's licence turns on. |
| `created_at` | integer | no | Creation time. Epoch milliseconds. |
| `updated_at` | integer | no | Last change. Epoch milliseconds. |

### OrganizationAuditLog

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"organization_audit_log"` | yes |  |
| `id` | string | yes |  |
| `action` | string | yes | For example `member_added`, `sso_sign_in`, `scim_user_deactivated`, `audit_logs_purged`. |
| `actor` | object | yes |  |
| `actor.type` | `user`, `scim`, `sso`, `system` | no |  |
| `actor.id` | string or null | no | User id, SCIM token id or SSO connection id. |
| `actor.email` | string or null | no | The user's email. |
| `target` | object | yes |  |
| `target.type` | string | no |  |
| `target.id` | string or null | no |  |
| `data` | object | no | Details. Never secrets or SAML assertions. |
| `occurred_at` | integer | yes | When it happened. Epoch milliseconds. |

### OrganizationMember

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"organization_member"` | yes |  |
| `user_id` | string | yes |  |
| `email` | string | yes |  |
| `name` | string or null | no |  |
| `role` | `owner`, `admin`, `member` | yes |  |
| `source` | `manual`, `sso`, `scim`, `project` | yes | How the person joined: added by hand, first sign-in with SSO, SCIM, or through a project of the organization. |
| `active` | boolean | yes | False after SCIM deprovisioning. |
| `sso_groups` | array of string | no | The groups the identity provider sent at the last SSO sign-in. |
| `last_sso_at` | integer or null | no | The last SSO sign-in. Epoch milliseconds, or null. |
| `password_sign_in` | boolean | no | Whether the account has a password. |
| `created_at` | integer | no | When the person joined. Epoch milliseconds. |

### OrganizationProject

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"organization_project"` | yes |  |
| `id` | string | yes | Project id. |
| `name` | string | yes |  |
| `region` | `us`, `eu` | yes |  |
| `region_name` | string | no |  |
| `member_count` | integer | no | Project members. |
| `your_role` | string or null | no | Your role in the project, or null. |
| `added_at` | integer | no | When the project joined the organization. Epoch milliseconds. |

### OrganizationProjectMember

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"project_member"` | yes |  |
| `user_id` | string | yes |  |
| `email` | string | no |  |
| `name` | string or null | no |  |
| `role` | string | yes | `admin`, `developer`, `viewer` or a custom role id (role_...). |
| `role_name` | string | yes | Admin, Developer, Viewer, the custom role's name, or "No access (role removed)". |
| `source` | `manual`, `org`, `idp` | no | `org`: an organization owner or admin. `idp`: a group role mapping. `manual`: set by hand; provisioning leaves it alone. |

### RoleMapping

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"role_mapping"` | yes |  |
| `id` | string | yes | Mapping id (map_...). |
| `group` | string | yes | Group name, matched without regard to case against SCIM group names and the SSO groups attribute or claim. |
| `project_id` | string | yes |  |
| `project_name` | string or null | no |  |
| `role` | string | yes | `admin`, `developer`, `viewer` or a custom role id. |
| `role_name` | string | no |  |
| `created_at` | integer | no | Creation time. Epoch milliseconds. |

### ScimError

RFC 7644 error body, sent as `application/scim+json`.

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes |  |
| `status` | string | yes | The HTTP status as a string. |
| `scimType` | `uniqueness`, `invalidFilter`, `invalidValue`, `invalidSyntax`, `invalidPath`, `noTarget`, `mutability`, `tooMany` | no |  |
| `detail` | string | yes | What went wrong. |

### ScimGroup

A SCIM 2.0 Group. Members are SCIM Users of the organization; nested groups are refused.

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes |  |
| `id` | string | yes | SCIM group id (scg_...). |
| `externalId` | string | no |  |
| `displayName` | string | yes | Unique in the organization. Role mappings match it. |
| `members` | array of object | no |  |
| `members[].value` | string | no | SCIM user id. |
| `members[].display` | string | no |  |
| `members[].type` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no |  |

### ScimListResponse

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes |  |
| `totalResults` | integer | yes |  |
| `startIndex` | integer | yes | 1-based. |
| `itemsPerPage` | integer | yes |  |
| `Resources` | array of object | yes |  |

### ScimPatchOp

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes | Must contain `urn:ietf:params:scim:api:messages:2.0:PatchOp`. |
| `Operations` | array of object | yes |  |
| `Operations[].op` | string | yes | add, replace or remove; any case. |
| `Operations[].path` | string | no | Optional for add and replace. Value filters such as `emails[type eq "work"].value` work. |
| `Operations[].value` | object | no |  |

### ScimToken

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"scim_token"` | yes |  |
| `id` | string | yes | Token id (sct_...). |
| `name` | string | yes |  |
| `prefix` | string | yes | The first 13 characters, to tell tokens apart. |
| `created_by` | string | no | User id. |
| `created_at` | integer | no | Creation time. Epoch milliseconds. |
| `last_used_at` | integer or null | no | Last request, to the minute. Epoch milliseconds, or null. |
| `revoked_at` | integer or null | no | When it was revoked. Epoch milliseconds, or null. |

### ScimUser

A SCIM 2.0 User (RFC 7643). Other attributes you send, such as `phoneNumbers` and the enterprise extension, are stored and returned.

| Field | Type | Required | Description |
|---|---|---|---|
| `schemas` | array of string | yes |  |
| `id` | string | yes | SCIM user id (scu_...). |
| `externalId` | string | no | The identity provider's id. |
| `userName` | string | yes | Unique in the organization, without regard to case. |
| `name` | object | no |  |
| `name.formatted` | string | no |  |
| `name.givenName` | string | no |  |
| `name.familyName` | string | no |  |
| `displayName` | string | no |  |
| `emails` | array of object | no | The primary address (else the work address, else the first) is the account's email. It must be on a verified domain. |
| `emails[].value` | string | no |  |
| `emails[].type` | string | no |  |
| `emails[].primary` | boolean | no |  |
| `active` | boolean | yes | False deprovisions the person. |
| `groups` | array of object | no | Read only. |
| `groups[].value` | string | no | SCIM group id. |
| `groups[].display` | string | no |  |
| `meta` | object | no |  |
| `meta.resourceType` | string | no |  |
| `meta.created` | string | no |  |
| `meta.lastModified` | string | no |  |
| `meta.location` | string | no |  |
| `meta.version` | string | no | Weak ETag such as `W/"3"`. |

### SsoConnection

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"sso_connection"` | yes |  |
| `id` | string | yes | Connection id (ssoc_...). |
| `org_id` | string | yes |  |
| `kind` | `saml`, `oidc` | yes |  |
| `name` | string | yes |  |
| `enabled` | boolean | yes | Sign-ins work only when true. New connections start off. |
| `jit` | boolean | yes | Create accounts and organization memberships at first sign-in. Default true. |
| `saml` | object | no |  |
| `saml.idp_entity_id` | string | no | The identity provider's entity ID (Issuer). |
| `saml.idp_sso_url` | string | no | The identity provider's sign-in URL for the HTTP-Redirect binding. https on RevenueDot Cloud. |
| `saml.idp_certificates` | array of string | no | 1 to 5 signing certificates as PEM. Several while the identity provider rotates its certificate. |
| `saml.allow_idp_initiated` | boolean | no | Accept sign-ins started from the identity provider's app dashboard. Default false. |
| `saml.email_attribute` | string or null | no | Attribute that holds the email address. Null: `email`, `mail`, `emailaddress`, Microsoft's emailaddress claim, `urn:oid:0.9.2342.19200300.100.1.3`, else an email-shaped NameID. |
| `saml.first_name_attribute` | string or null | no | Attribute for the first name. Null: common names such as `givenname` and `firstname`. |
| `saml.last_name_attribute` | string or null | no | Attribute for the last name. Null: common names such as `sn` and `surname`. |
| `saml.groups_attribute` | string or null | no | Attribute that lists the person's groups. Null: `groups` or Microsoft's groups claim. |
| `oidc` | object | no |  |
| `oidc.issuer` | string | no | The issuer URL. RevenueDot reads `<issuer>/.well-known/openid-configuration`. |
| `oidc.client_id` | string | no | The client ID registered with the identity provider. |
| `oidc.scopes` | array of string | no | Default `openid email profile`. `openid` is always added. |
| `oidc.groups_claim` | string or null | no | The ID token claim that lists the person's groups. Null: groups are not read. |
| `oidc.has_client_secret` | boolean | no | Whether a client secret is stored. The secret itself is never returned. |
| `sp` | object | yes | The values to enter in the identity provider, built from `REVENUEDOT_PUBLIC_URL` or the request's address. |
| `sp.entity_id` | string | no | SAML: the entity ID (audience) to enter in the identity provider. |
| `sp.acs_url` | string | no | SAML: the assertion consumer service URL. |
| `sp.metadata_url` | string | no | SAML: RevenueDot's service provider metadata. |
| `sp.redirect_uri` | string | no | OpenID Connect: the redirect URI to register. |
| `sp.start_url` | string | yes | A link that starts a sign-in with this connection. |
| `created_at` | integer | no | Creation time. Epoch milliseconds. |
| `updated_at` | integer | no | Last change. Epoch milliseconds. |

### SsoDomain

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"sso_domain"` | yes |  |
| `domain` | string | yes |  |
| `verified` | boolean | yes |  |
| `verified_at` | integer or null | no | When the TXT record was first found. Epoch milliseconds, or null. |
| `txt_record` | object | yes |  |
| `txt_record.type` | `"TXT"` | no |  |
| `txt_record.name` | string | no | `_revenuedot-sso.<domain>` |
| `txt_record.value` | string | no | `revenuedot-sso-verification=<token>` |
| `last_checked_at` | integer or null | no | The last check. Epoch milliseconds, or null. |
| `last_error` | string or null | no | Why the last check failed. |
| `created_at` | integer | no | When it was added. Epoch milliseconds. |

### V2Error

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | `"error"` | yes |  |
| `type` | `parameter_error`, `resource_already_exists`, `resource_missing`, `idempotency_error`, `rate_limit_error`, `authentication_error`, `authorization_error`, `store_error`, `server_error`, `resource_locked_error`, `unprocessable_entity_error`, `invalid_request`, `entity_references_archived_entities` | yes |  |
| `message` | string | yes | What went wrong. |
| `param` | string | no | The request field at fault, when there is one. |
| `doc_url` | string | yes | Link to the error's section of the errors page. |
| `retryable` | boolean | yes | True when retrying the same request can succeed. |

## Related

- [API overview](https://revenuedot.app/docs/api.md)
- [Authentication](https://revenuedot.app/docs/api/authentication.md)
- [Errors](https://revenuedot.app/docs/api/errors.md)
- [OpenAPI document](https://revenuedot.app/docs/api/openapi.yaml)
