> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting REST Applications with Lumos

Connect any REST API to Lumos without writing code. You fill out connection settings that describe the target API. Where its data lives, how to log in, and what the responses look like. Lumos does the rest.

Think of it as filling out a form about the API. Once the form is right, Lumos can list the app's users, see who has what access, and even grant or remove access, all through the API you described.

This page explains every setting and every option inside each configuration block. Worked configurations for real APIs (Databricks, ADP, Salesforce, GitHub, Datadog, HubSpot, and more) are on [Universal REST Configuration Examples](/integrations/generics/universal-rest/examples).

> The Universal REST connector works for services that do not have complex data models. Examine the configuration guide to determine if the API will be compliant with the Universal REST connector.

## Where you enter the configuration

Go to [Integrations](https://app.lumosidentity.com/integrations?tab=can_integrate) and click the **Generic REST** card (or the **Generic REST (On Prem)** card for an API inside your network). Every setting and block on this page is a field on that card's connection form, named after the block: the accounts block goes in **Accounts Config (JSON)**, the error rules in **Error Detection (JSON)**, and so on.

Some fields are hidden until you tick the box that reveals them:

| Tick | To show |
| :- | :- |
| **Configure Write Capabilities** | The seven write configs (assign, unassign, create, update, activate, deactivate, delete) |
| **Show Advanced Authentication Settings** | **Auth Header Name**, **Auth Token Prefix**, **Auth Query Parameter**, **Second Auth Header Name**, **OAuth1 Realm**, and **JWT Claims (JSON)** |
| **Configure OAuth 2.0** | The OAuth settings |
| **Configure Client Certificate (mTLS)** | The mTLS settings |

Hiding a field only changes what the form displays. It doesn't clear the value or turn the capability off.

You also choose the credential type the connection uses:

| Credential | Use it when |
| :- | :- |
| API token or key | The API takes a token or key in a header, or in the web address via **Auth Query Parameter** |
| Basic | The API takes a username and password over HTTP Basic, or needs two secret headers (with **Second Auth Header Name**) |
| OAuth 2.0 client credentials | Server-to-server OAuth. Set **OAuth Token URL** |
| OAuth 2.0 authorization code | OAuth with a user approving access. Set **OAuth Authorization URL** and **OAuth Token URL** |
| OAuth 1.0a | APIs that sign each request (OAuth 1.0a or TBA). Set **OAuth1 Realm** if the API requires one, like NetSuite |
| JWT bearer assertion | The API accepts a signed JWT. You provide the JWT headers, claims, and signing secret, and Lumos sends a freshly signed JWT as the bearer token |
| Key-pair | The API accepts a JWT signed with your private key, like Snowflake. Set **JWT Claims (JSON)** |

## How configuration works

The connection has two kinds of settings.

**Simple fields.** Single values you type into a box. The API's web address, the name of the header your token goes in, and so on.

**Configuration blocks.** Small JSON documents you paste into a box. Each block describes one thing Lumos can do, like "here is how to list this app's users." JSON is just structured text made of pairs. A name in quotes, a colon, then a value:

```json theme={null}
{ "endpoint": "/scim/v2/Users" }
```

That one line says: the list of users lives at the path `/scim/v2/Users`. Every block on this page follows this same pattern. Names on the left, your values on the right.

You only fill in the blocks for what you want. For Lumos to see users without changing anything, fill in the accounts block and stop there. Each block you add unlocks one more capability.

| If you want Lumos to... | Fill in |
| :- | :- |
| See the app's users | Accounts Config |
| See what access exists (roles, groups, licenses) | Entitlements Config |
| See who holds which access | Associations Config |
| See workspaces, projects, or other containers | Resources Config |
| See when each user last logged in | Last Activity Config |
| Pull extra fields like department or title | Custom Attributes Schema |
| Grant and remove access, create and disable users | The write configs |

## The building blocks used in every configuration

Four ideas repeat in every block. After you learn them, every block below reads the same way.

### 1. The endpoint

The path where the data lives, added onto your Base URL. If the Base URL is `https://api.example.com` and the endpoint is `/v1/users`, Lumos calls `https://api.example.com/v1/users`. Your API's documentation lists these paths, usually under names like "List users."

### 2. The records path

APIs wrap their lists differently. Some return the list directly:

```json theme={null}
[ { "id": "1" }, { "id": "2" } ]
```

If yours does this, leave `records_path` out entirely.

Most APIs wrap the list inside a named field:

```json theme={null}
{ "totalResults": 2, "Resources": [ { "id": "1" }, { "id": "2" } ] }
```

Here the list lives under `Resources`, so you set `"records_path": "Resources"`. Common wrapper names in the wild: `Resources` (SCIM), `data` (Datadog), `results` (HubSpot), `records` (Salesforce), `workers` (ADP), `users` (Google Play).

### 3. Dotted paths

Many values sit inside nested fields. A dotted path walks down into them. Given this user record:

```json theme={null}
{
  "id": "42",
  "name": { "givenName": "Sarah", "familyName": "Thompson" },
  "emails": [ { "value": "sarah@example.com" } ]
}
```

The path `id` reads `42`. The path `name.givenName` walks into `name` and reads `Sarah`. The path `emails[0].value` reads the first entry in the emails list and takes its `value`, which is the email address. If a field name itself contains dots, wrap it in brackets and quotes: `["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"].employeeNumber`.

That's the whole trick. Every "path" field on this page is one of these.

### 4. Pagination

APIs return long lists in pages, and each API signals "here is how to get the next page" differently. The `pagination` object inside each block tells Lumos which signal your API uses. Look at a real response from your API and match it to one of the four styles.

**Style: none.** The API returns everything at once. Nothing else to configure.

```json theme={null}
{ "style": "none" }
```

**Style: offset.** You ask for records starting at a position. "Give me 20 records starting at record 21." SCIM works this way, and so does ADP.

| Field | What it is | Example |
| :- | :- | :- |
| `offset_param` | The name of the query parameter that carries the starting position | `startIndex` for SCIM, `$skip` for ADP |
| `size_param` | The parameter that carries how many records per page | `count` for SCIM, `$top` for ADP |
| `start_offset` | The number the counting starts at. SCIM counts from 1. ADP counts from 0. The default is 0 | `1` |
| `total_path` | Optional. A dotted path to the total record count in the response. When present, Lumos uses it to know when to stop | `totalResults` |
| `page_size` | How many records to ask for per page. The default is 100 | `20` |

```json theme={null}
{ "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 20, "total_path": "totalResults" }
```

**Style: page.** You ask for page numbers. "Give me page 3." Datadog works this way. Lumos stops at the first empty or short page.

| Field | What it is | Example |
| :- | :- | :- |
| `page_param` | The parameter carrying the page number | `page[number]` |
| `size_param` | The parameter carrying the page size | `page[size]` |
| `start_page` | The first page's number. Usually 1, but Datadog starts at 0. The default is 1 | `0` |
| `page_size` | Records per page. Default 100 | `100` |

```json theme={null}
{ "style": "page", "page_param": "page[number]", "size_param": "page[size]", "start_page": 0, "page_size": 100 }
```

**Style: cursor.** The API hands you a marker for the next page. The marker can arrive four different ways, and you configure exactly one of them:

| Where the marker appears | Field to set | Example |
| :- | :- | :- |
| A token inside the response body, which you send back as a query parameter | `next_cursor_path` for where to read it, plus `cursor_param` for the parameter to send it back in | HubSpot returns `paging.next.after`, echoed back as `?after=` |
| A complete next-page web address inside the body | `next_url_path` | Salesforce returns `nextRecordsUrl` |
| A token in a response header | `next_cursor_header` | `X-Next-Token` |
| A next-page address in the standard `Link` header | `use_link_header: true` | GitHub |

`size_param` and `page_size` work here too when the API accepts a page-size parameter.

```json theme={null}
{ "style": "cursor", "next_cursor_path": "paging.next.after", "cursor_param": "after", "size_param": "limit", "page_size": 100 }
```

## Connection settings

The simple fields, before any blocks.

| Setting | Default | Plain-English meaning |
| :- | :- | :- |
| **Base URL** | required | The API's root web address, like `https://api.example.com`. Every endpoint gets added onto this |
| **Auth Header Name** | `Authorization` | The name of the header your token travels in. Some vendors use their own, like `X-Api-Key` |
| **Auth Token Prefix** | `Bearer` | The word placed before the token. Leave it empty if the API wants the bare key with no prefix |
| **Auth Query Parameter** | empty | Only for APIs that want the token in the web address itself, like `?api_key=...`. Setting this switches the token from a header to that parameter |
| **Second Auth Header Name** | empty | For APIs needing two secret headers at once. Datadog needs `DD-API-KEY` and `DD-APPLICATION-KEY`. Choose the Basic credential, put the two secrets in its username and password, and name the two headers here and in Auth Header Name |
| **Credential Test Endpoint** | empty | A safe path Lumos calls to check the credentials work, like `/scim/v2/Users?count=1`. If empty, Lumos tests against the accounts endpoint |
| **Tenant ID** | empty | A stable name for this connection. If empty, Lumos derives one from the Base URL |

### OAuth settings

Only needed when your credential is one of the OAuth types.

| Setting | Default | Plain-English meaning |
| :- | :- | :- |
| **OAuth Authorization URL** | empty | Where users are sent to click "approve." Only for the authorization-code flow |
| **OAuth Token URL** | empty | Where Lumos trades credentials for an access token. Needed for both OAuth flows |
| **OAuth Extra Authorize Params (JSON)** | empty | Extra parameters some vendors require on the approval page. Google needs `{"access_type": "offline", "prompt": "consent"}` or it won't issue a refresh token |
| **OAuth Client Secret via HTTP Basic** | off | Some providers want the client id and secret sent as a Basic login on the token exchange instead of in the request body. Databricks requires this switched on |
| **OAuth Scopes** | empty | The permissions to request, space-separated. The connector requests nothing by default, so put whatever the API requires here. Databricks needs `all-apis` |
| **OAuth1 Realm** | empty | Only for old OAuth 1.0a APIs, like NetSuite's account id |
| **JWT Claims (JSON)** | empty | Only for the key-pair credential. The claims to sign: `{"iss": "...", "sub": "...", "aud": "...", "algorithm": "RS256", "expiry_minutes": 20}`. `iss` is required, the algorithm defaults to RS256, expiry defaults to 20 minutes, and any extra keys you add become claims too |

### mTLS settings

Some APIs, like ADP, require a client certificate on every call in addition to the token.

| Setting | Plain-English meaning |
| :- | :- |
| **Client Certificate (PEM)** | The certificate text, pasted in |
| **Client Private Key (PEM)** | The matching private key. Both must be set together, and they work with any credential type |

## Connecting to an API inside your network

If the API is reachable only from inside your network, use the **Generic REST (On Prem)** card instead of the cloud card. It runs through the Lumos On-Premise Agent, so set the agent up first (see [Installation](/integrations/on-premise-agent/installation)). The connection settings and configuration blocks on this page are the same for both cards.

## Accounts Config

Describes how to list the app's users. This is the first block to fill in, and the only one required for a basic connection.

| Field | Required | What it does |
| :- | :- | :- |
| `endpoint` | Yes | The path that lists users |
| `records_path` | No | Where the list sits in the response. Leave out if the response is the list itself |
| `pagination` | No | How to get the next page. Defaults to `none` |
| `mapping` | Yes | Which fields on each user record mean what, detailed below |

The `mapping` translates the API's field names into Lumos field names. Every field is a dotted path into one user record:

| Mapping field | Required | What it points at |
| :- | :- | :- |
| `id` | Yes | The user's unique, permanent identifier. Pick something that never changes |
| `email` | No | The user's email address |
| `given_name` | No | First name |
| `family_name` | No | Last name |
| `username` | No | Login name |
| `status_path` | No | The field that says whether the user is active. Leave out to treat everyone as active |
| `active_values` | No | The values of that field that count as active. Comparison ignores upper and lower case. The default list is `["active", "enabled", "true", "1", "yes"]`. Values can be written as JSON strings or as bare values like `true` or `1`; Lumos compares them as text |

How the status works: Lumos reads the field at `status_path` and checks it against your `active_values` list. A match means ACTIVE. Anything else means INACTIVE. So for a SCIM API where `"active": true`, use `"status_path": "active"` with `"active_values": ["true"]`. For ADP, where status hides at `workerStatus.statusCode.codeValue` with values like `Active`, point the path there and list `["Active"]`.

A complete block for a SCIM-shaped API:

```json theme={null}
{
  "endpoint": "/scim/v2/Users",
  "records_path": "Resources",
  "pagination": { "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 20, "total_path": "totalResults" },
  "mapping": {
    "id": "id",
    "email": "emails[0].value",
    "given_name": "name.givenName",
    "family_name": "name.familyName",
    "username": "userName",
    "status_path": "active",
    "active_values": ["true"]
  }
}
```

## Entitlements Config

Describes how to list the access that exists in the app. Roles, groups, licenses, teams. Whatever the app calls them.

| Field | Required | What it does |
| :- | :- | :- |
| `endpoint` | Yes | The path that lists them |
| `records_path` | No | Where the list sits in the response |
| `pagination` | No | Defaults to `none` |
| `type` | No | A one-word label for what kind of access this is. Defaults to `entitlement`. The standard vocabulary is `role`, `license`, `group`, `permission`, and `team` |
| `mapping` | Yes | Field translation, detailed below |

| Mapping field | Required | What it points at |
| :- | :- | :- |
| `id` | Yes | The entitlement's unique identifier |
| `label` | Yes | The human-readable name shown in Lumos |
| `description` | No | A longer description, if the API provides one |
| `is_assignable` | No | Set to `true` when you also configure the assign and unassign write configs, so Lumos knows this access can be granted |

**One kind of access:**

```json theme={null}
{
  "endpoint": "/scim/v2/Groups",
  "records_path": "Resources",
  "pagination": { "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 20, "total_path": "totalResults" },
  "type": "group",
  "mapping": { "id": "id", "label": "displayName" }
}
```

**Several kinds of access from one app.** Wrap multiple blocks in square brackets to make a list. Each block needs a different `type`, because other configuration refers to blocks by their type. This verified Salesforce configuration lists permission sets and groups together:

```json theme={null}
[
  {
    "endpoint": "/services/data/v59.0/query?q=SELECT+Id,Label+FROM+PermissionSet",
    "records_path": "records",
    "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
    "type": "permission_set",
    "mapping": { "id": "Id", "label": "Label" }
  },
  {
    "endpoint": "/services/data/v59.0/query?q=SELECT+Id,Name+FROM+Group",
    "records_path": "records",
    "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
    "type": "group",
    "mapping": { "id": "Id", "label": "Name" }
  }
]
```

## Associations Config

Describes how Lumos learns who holds which access. This is the block with the most options, because APIs expose memberships in four different shapes. Pick the mode matching what your API offers, then fill in that mode's fields.

**Which mode do I need?**

| Your API offers... | Mode |
| :- | :- |
| One endpoint listing every membership, one row each | `endpoint` |
| A groups listing where each group carries its member list | `endpoint`, plus `members_path` |
| User records that carry their own access list | `account_attribute` |
| Only a per-group members endpoint, asked group by group | `entitlement_fanout` |
| Only a per-user access endpoint, asked user by user | `account_fanout` |

### Mode: endpoint

Reads a membership listing.

| Field | Required | What it does |
| :- | :- | :- |
| `mode` | No | `"endpoint"`. This is the default mode |
| `endpoint` | Yes | The path to the membership listing |
| `records_path` | No | Where the list sits in the response |
| `pagination` | No | Defaults to `none` |
| `entitlement_id_path` | Yes | Dotted path to the access id on each record |
| `account_id_path` | Yes | Dotted path to the user id. On the record itself, or on each member entry when `members_path` is set |
| `members_path` | No | Set this when each record is a group carrying a list of members. Lumos then produces one association per member |

Without `members_path`, for a flat listing like Salesforce's assignment records:

```json theme={null}
{
  "mode": "endpoint",
  "endpoint": "/services/data/v59.0/query?q=SELECT+AssigneeId,PermissionSetId+FROM+PermissionSetAssignment",
  "records_path": "records",
  "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
  "account_id_path": "AssigneeId",
  "entitlement_id_path": "PermissionSetId"
}
```

With `members_path`, for SCIM Groups where each group carries its members:

```json theme={null}
{
  "mode": "endpoint",
  "endpoint": "/scim/v2/Groups",
  "records_path": "Resources",
  "pagination": { "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 20, "total_path": "totalResults" },
  "entitlement_id_path": "id",
  "members_path": "members",
  "account_id_path": "value"
}
```

Reading that second one aloud: list the groups, take each group's `id` as the access, look at its `members` list, and take each member's `value` as the user.

### Mode: account\_attribute

For APIs where each user record already carries its access. No extra endpoint needed, Lumos reads it during the user sync. Requires Accounts Config to be filled in.

| Field | Required | What it does |
| :- | :- | :- |
| `mode` | Yes | `"account_attribute"` |
| `account_entitlements_path` | Yes | Dotted path on each user record to its access entries |
| `attribute_entitlement_id_path` | Only sometimes | When each entry is an object, the path inside it to the access id. Leave out when entries are plain values |

Two verified examples showing the difference. SCIM users carry a `groups` list of objects, so both fields are needed:

```json theme={null}
{ "mode": "account_attribute", "account_entitlements_path": "groups", "attribute_entitlement_id_path": "value" }
```

Google Play users carry `developerAccountPermissions` as a list of plain text values, so the second field is left out:

```json theme={null}
{ "mode": "account_attribute", "account_entitlements_path": "developerAccountPermissions" }
```

### Mode: entitlement\_fanout

For APIs with no membership listing at all, where you must ask each group individually who its members are. Lumos walks the entitlements list and calls the endpoint once per entitlement.

| Field | Required | What it does |
| :- | :- | :- |
| `mode` | Yes | `"entitlement_fanout"` |
| `endpoint` | Yes | A path template containing `{entitlement_id}`, which Lumos fills in per group |
| `source_entitlement_type` | Yes | Which Entitlements Config block provides the groups to walk. Must equal that block's `type` |
| `records_path` | No | Where the member list sits in each response |
| `account_id_path` | Yes | Dotted path to the user id on each member |
| `pagination` | No | Applies to each per-group call |

The verified Databricks configuration, needed because Databricks's groups listing leaves out the members:

```json theme={null}
{
  "mode": "entitlement_fanout",
  "endpoint": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Groups/{entitlement_id}",
  "records_path": "members",
  "source_entitlement_type": "group",
  "account_id_path": "value",
  "pagination": { "style": "none" }
}
```

### Mode: account\_fanout

The mirror image. Ask each user individually what access they hold.

| Field | Required | What it does |
| :- | :- | :- |
| `mode` | Yes | `"account_fanout"` |
| `endpoint` | Yes | A path template containing `{account_id}`, filled in per user |
| `records_path` | No | Where the access list sits in each response |
| `entitlement_id_path` | Yes | Dotted path to the access id on each row |
| `pagination` | No | Applies to each per-user call |

A note on both fan-out modes. They make one API call per group or per user by nature, so a large tenant means many calls. Syncs resume midway rather than starting over if interrupted.

**Combining sources.** Like entitlements, this block accepts a list. Use a list when memberships come from more than one place, such as Salesforce's permission set assignments plus its group members. The blocks in the list don't need to use the same mode.

## Resources Config

Describes how to list the app's containers. Workspaces, projects, sites, or profiles. Only needed when access in your app is scoped to a container.

| Field | Required | What it does |
| :- | :- | :- |
| `endpoint` | Yes | The path that lists them |
| `records_path` | No | Where the list sits in the response |
| `pagination` | No | Defaults to `none` |
| `type` | No | A one-word label like `workspace` or `profile`. Defaults to `resource` |
| `mapping` | Yes | `id` and `label` are required paths, `description` is optional |

```json theme={null}
{
  "endpoint": "/services/data/v59.0/query?q=SELECT+Id,Name+FROM+Profile",
  "records_path": "records",
  "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
  "type": "profile",
  "mapping": { "id": "Id", "label": "Name" }
}
```

## Last Activity Config

Describes where to find when each user was last active, usually a last-login time. Powers inactive-account detection in Lumos.

| Field | Required | What it does |
| :- | :- | :- |
| `endpoint` | Yes | A path whose records carry a per-user timestamp |
| `records_path` | No | Where the list sits in the response |
| `pagination` | No | Defaults to `none` |
| `mapping.account_id` | Yes | Dotted path to the user id on each record |
| `mapping.happened_at` | Yes | Dotted path to the timestamp |
| `mapping.event_type` | No | A constant label for the event, defaulting to `last_activity`. Set it to something meaningful like `last_login` |
| `mapping.event_type_path` | No | Or read the event type from each record instead of using a constant |

Records missing an id or timestamp are skipped, so users who never logged in don't produce broken events.

```json theme={null}
{
  "endpoint": "/services/data/v59.0/query?q=SELECT+Id,LastLoginDate+FROM+User",
  "records_path": "records",
  "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
  "mapping": { "account_id": "Id", "happened_at": "LastLoginDate", "event_type": "last_login" }
}
```

## Custom Attributes Schema

Declares extra fields to pull in beyond the standard ones. Department, title, employee number, cost center. This block is a list, one entry per attribute.

| Field | Required | What it does |
| :- | :- | :- |
| `name` | Yes | The attribute's full name. This exact name is what shows up in Lumos, so make it stable and descriptive |
| `path` | No | Dotted path to the value on each record. With a path set, the value fills in automatically during sync. Leave it out to declare the attribute without populating it |
| `attribute_type` | No | `string` for text, which is the default, or `user` when the value refers to another user |
| `customized_type` | No | Which kind of record carries this attribute: `account` (the default), `entitlement`, or `resource` |
| `description` | No | A note about what the attribute means |

The verified Salesforce example, pulling two extra fields off each user:

```json theme={null}
[
  { "name": "Department", "path": "Department", "attribute_type": "string", "customized_type": "account", "description": "Salesforce User.Department" },
  { "name": "Title", "path": "Title", "attribute_type": "string", "customized_type": "account", "description": "Salesforce User.Title" }
]
```

## Write configs (provisioning)

Everything so far reads data. The write configs let Lumos change it. Grant access, remove access, create users, disable users. There are seven, one settings field each, and they all share one shape:

| Field | Required | What it does |
| :- | :- | :- |
| `method` | No | The kind of call: `POST` (the default), `PUT`, `PATCH`, or `DELETE` |
| `url` | Yes | The path to call, with placeholders in curly brackets that Lumos fills in |
| `body` | No | The JSON to send, also with placeholders |
| `response_id_path` | Create only | Where the new user's id appears in the response. It doesn't have to be called "id". Google Play identifies users by email, so its config sets this to `email` |

A placeholder is a marker like `{account_id}` that Lumos replaces with the real value when the action runs. Each write config gets its own set:

| Write config | Placeholders available |
| :- | :- |
| Assign Entitlement / Unassign Entitlement | `{account_id}`, `{entitlement_id}`, `{entitlement_type}`, `{resource_id}`, `{resource_type}` |
| Create Account | `{email}`, `{username}`, `{given_name}`, `{family_name}`, `{user_status}` |
| Activate / Deactivate / Delete Account | `{account_id}` |
| Update Account | `{account_id}` or `{id}`, plus the fields being changed |

Two useful details. Placeholders in the `url` are safely encoded for web addresses. And a body value that is exactly one placeholder keeps its real type, so a template can produce a true or false rather than the text "true".

### The four shapes real APIs take

The shipped examples establish four patterns. Find the one your API's documentation matches.

**SCIM-style PatchOp.** Changes are PATCH calls with a standard envelope. Granting access adds a member to the group:

```json theme={null}
{
  "method": "PATCH",
  "url": "/scim/v2/Groups/{entitlement_id}",
  "body": {
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "add", "path": "members", "value": [{ "value": "{account_id}" }] }]
  }
}
```

Removing access uses a filter to pick out the one member to remove, which is the standard SCIM way to do it:

```json theme={null}
{
  "method": "PATCH",
  "url": "/scim/v2/Groups/{entitlement_id}",
  "body": {
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "remove", "path": "members[value eq \"{account_id}\"]" }]
  }
}
```

Disabling a user replaces its `active` field with `false`, and enabling replaces it with `true`.

**Plain-field PATCH.** Some APIs skip the envelope and just take the changed fields. Salesforce disables a user with:

```json theme={null}
{ "method": "PATCH", "url": "/services/data/v59.0/sobjects/User/{account_id}", "body": { "IsActive": false } }
```

**Junction-record POST.** Some APIs model a grant as its own object you create. Salesforce grants a permission set by creating an assignment record:

```json theme={null}
{
  "method": "POST",
  "url": "/services/data/v59.0/sobjects/PermissionSetAssignment",
  "body": { "AssigneeId": "{account_id}", "PermissionSetId": "{entitlement_id}" }
}
```

Fair warning on this shape: undoing it usually means finding and deleting that record, which needs a lookup first, and that's beyond what one templated call can do.

**Bare DELETE.** Deletion is usually a call with no body at all:

```json theme={null}
{ "method": "DELETE", "url": "/scim/v2/Users/{account_id}" }
```

> **Write configs change the real app**
>
> Test them against a sandbox before any production system. The SCIM Playground at scim.dev is a free live sandbox built for exactly this, and its configuration on [Universal REST Configuration Examples](/integrations/generics/universal-rest/examples#scim-playground) covers the complete write lifecycle.

**Where the limit is.** Each write config is exactly one API call with values filled in. If an action needs the connector to first look something up, or to compute a value like a timestamp, it can't be expressed here. Real cases: Salesforce permission-set removal and Google Play's permission updates. Those need a dedicated connector or the [Connector SDK](/integrations/connector-sdk/introduction), and the Examples page flags each one.

## Error Detection

Most APIs report failures with an error status code, and Lumos handles those automatically. A few APIs always say "success" at the HTTP level and hide failures inside the response body. This optional block teaches Lumos to spot them, and it applies to every call the connection makes.

Configure exactly one of the three detection rules:

| Rule | Reads as | Fields |
| :- | :- | :- |
| Match on an error value | "It's an error when this field says so" | `error_when_path` points at the field, `error_equals` lists the values that mean error, ignoring case |
| Error field present | "It's an error when this field holds anything real" | `error_when_present` points at the field. Empty or false values don't count, so an API that returns `"error": false` on success works untouched |
| Match on a success value | "It's only a success when this field says so. Anything else, or nothing at all, is an error" | `success_when_path` points at the field, `success_equals` lists the acceptable values |

Two optional extras work with any rule. `message_path` points at the API's human-readable error message, and `code_path` at its error code, so both appear in Lumos when something fails.

```json theme={null}
{ "error_when_path": "status", "error_equals": ["error", "failed"], "message_path": "error.message", "code_path": "error.code" }
```

A badly formed block fails immediately when you connect, during the credential check, rather than quietly during a sync later.

## Putting it together: your first connection

1. **Get one real response.** Ask whoever owns the API for a sample response from its "list users" endpoint, or find one in its documentation. Everything below comes from looking at it.
2. **Fill in the connection settings.** The Base URL, and how the token travels (which header, which prefix). The API's authentication documentation states this on its first page.
3. **Write the Accounts Config.** Find the list in the sample response (that's your `records_path`), find how the API signals more pages (that's your `pagination`), and map the user fields (that's your `mapping`). Set the status path and active values last.
4. **Connect.** Lumos validates the credentials and the configuration immediately. A configuration mistake shows up here, not silently later.
5. **Add blocks one at a time.** Entitlements next, then associations, then the rest as needed. Each one can be added later by editing the connection, so there's no need to configure everything on day one.

If your API resembles one on [Universal REST Configuration Examples](/integrations/generics/universal-rest/examples), start from that configuration and change the paths. Matching on mechanics beats matching on vendor: an offset-paginated SCIM-ish API can copy the SCIM Playground configuration nearly unchanged, whatever the app is called.
