Connecting the Generic REST Integration (Early Access)

Last updated: August 6, 2026

Universal REST is an Early Access connector that connects any JSON 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, Google Play, and the SCIM Playground) are on the Examples at the bottom of the page.

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:

{ "endpoint": "/scim/v2/Users" }

That line tells Lumos: 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. Want Lumos to see users but never change 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

Leaving a block out is not an error. Lumos simply reports nothing for that capability and the sync carries on. There are two exceptions. Asking Lumos to list users with no Accounts Config fails, and running a write action whose own config block is empty fails. Both report a message naming the missing setting.

The show/hide switches

The default fields are limited. Toggle the additional settings to configure fields based on the end system:

Checkbox

Reveals

Show Advanced Authentication Settings

Auth Header Name, Auth Token Prefix, Auth Query Parameter, Second Auth Header Name, OAuth1 Realm, JWT Claims

Configure OAuth 2.0

The OAuth 2.0 URLs, scopes, and options

Configure Client Certificate (mTLS)

The client certificate and private key boxes

Configure Write Capabilities

The seven write configuration blocks

These checkboxes only change what the form draws. Unticking one hides its fields; it does not clear the values behind them or turn the behavior off.

The always-visible fields are the Base URL, the Tenant ID, the Credential Test Endpoint, the six read configuration blocks, and Error Detection.

The building blocks used in every configuration

Four ideas turn up in nearly every configuration block below. Read these once and the rest of the page falls into place.

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."

An endpoint may carry its own query string, which is how the Salesforce configurations pass SOQL queries: /services/data/v59.0/query?q=SELECT+Id+FROM+User. One caution comes with that. When a pagination style adds query parameters of its own (an offset, a page number, a page size), those parameters replace the endpoint's built-in query string. So an endpoint with a ? in it pairs safely with {"style": "none"}, or with a cursor style that follows a full next-page link and sets no page-size parameter — exactly what the Salesforce examples do. If you need both a built-in query string and offset-style paging, ask the API for a path without the query string.

2. The records path

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

[ { "id": "1" }, { "id": "2" } ]

If yours does this, leave records_path out entirely.

Most APIs wrap the list inside a named field:

{ "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).

If the path points at something that isn't a list, Lumos stops with a message telling you so. If the path simply isn't there, Lumos treats it as an empty page, which is what you want for an app with no data yet.

3. Dotted paths

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

{
  "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. Negative positions count from the end, so emails[-1].value reads the last one. If a field name itself contains dots, wrap it in brackets and quotes: ["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"].employeeNumber, or ["@odata.nextLink"] for OData.

Numbers and true/false values are read as text, so a numeric id comes through as "42" and a true comes through as "True". Value comparisons on this page ignore upper and lower case, so you never have to match that capitalization by hand.

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.

{ "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, this is what decides where the walk ends

totalResults

page_size

How many records to ask for per page. The default is 100

20

{ "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 20, "total_path": "totalResults" }

Set total_path whenever the API reports a total. Without it, Lumos has to treat a short page as the end of the list. Some servers quietly cap page sizes below what you asked for, which would end the sync early. With it, Lumos keeps going until the total is reached. Leaving out offset_param means Lumos can't move the position at all, so only the first page is ever read.

Style: page. You ask for page numbers. "Give me page 3." 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

{ "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 at least 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; OData returns ["@odata.nextLink"]

A token in a response header

next_cursor_header

X-Next-Token

A next-page address in the standard Linkheader

use_link_header: true

GitHub

size_param and page_size work here too when the API accepts a page-size parameter. A cursor block with none of the four sources set is rejected as soon as you connect.

{ "style": "cursor", "next_cursor_path": "paging.next.after", "cursor_param": "after", "size_param": "limit", "page_size": 100 }

Two things Lumos does for you. A page_size you set is a default: when Lumos asks for a specific page size during a sync, that request wins. And Lumos refuses to loop forever. Three things end the walk rather than paging in circles: a cursor pointing at the page you just read, a cursor alternating between two values, and a position that fails to advance. Each is noted in the logs.

One safety rule on next-page links. When the API hands back a complete web address (next_url_path or the Link header), Lumos resolves it against your Base URL and then checks the host. If it points at a different host than your Base URL, Lumos refuses to follow it rather than send your credentials somewhere else. APIs that legitimately paginate on another host need that host as the Base URL, or a token-style cursor instead.

Connection settings

The simple fields, in the order the form shows them.

Setting

Default

Plain-English meaning

Base URL

required

The API's root web address, like https://api.example.com. It must be a complete http/https address with a host. Every endpoint gets added onto this

Tenant ID

empty

A stable name for this connection. If empty, Lumos derives one from the Base URL's host

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 instead

Then the six read configuration blocks (Accounts, Entitlements, Associations, Resources, Last Activity, Custom Attributes Schema), the write blocks behind their checkbox, Error Detection, and the authentication settings below.

Authentication

Pick one credential when you set up the connection. Each works on its own.

Credential

What you supply

How it's sent

API token / key

One secret

In the header named by Auth Header Name, after Auth Token Prefix. Or as a query parameter, if you set one

Basic authentication

Username and password

Standard HTTP Basic. Or as two secret headers, if you set Second Auth Header Name

OAuth 2.0 client credentials

Client id and secret

Lumos trades them for an access token and sends that as a bearer token

OAuth 2.0 authorization code

Client id and secret, plus a user clicking "approve"

Lumos runs the consent flow, then sends the access token, refreshing it as needed

OAuth 1.0a / TBA

Consumer key and secret, token id and secret

Each request is signed, the way NetSuite requires

JWT bearer assertion

The JWT headers, claims, and signing secret

Lumos mints a fresh signed JWT per call and sends it as a bearer token

Key pair (self-signed JWT)

A private key and a key identifier

Lumos mints a signed assertion from your JWT Claims and sends it as a bearer token. Snowflake, Adobe, and Google service accounts work this way

Leaving the API token empty is allowed and means "call this API with no credentials at all," which is only useful for genuinely open APIs.

These fields shape how the token travels. They live behind Show Advanced Authentication Settings, because the defaults already suit most bearer-token APIs.

Setting

Default

Plain-English meaning

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 like ?api_key=…. Setting this switches the API token from a header to that parameter. It has no effect on the other credential types

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 and put the two secrets in its username and password. The username is sent in Auth Header Name, the password in this one

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. Lumos adds the issued-at and expiry times itself

OAuth settings

Behind Configure OAuth 2.0. Only needed when your credential is one of the two OAuth 2.0 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 Scopes

empty

The permissions to request, space-separated. Used both on the approval page and in the client-credentials exchange. The connector requests nothing by default, so put whatever the API requires here. Databricks needs all-apis

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 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. Values must be text, and anything you name here overrides Lumos's own value for that parameter

mTLS settings

Behind Configure Client Certificate (mTLS). 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. The certificate is presented on the data calls and on the OAuth token exchange. That is what lets ADP's client-credentials-over-mTLS setup work from this one form, with no separate token step. Note that these two boxes are large text areas, so a pasted PEM is visible on screen rather than masked.

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. It is a single block, not a list.

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's. 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 leave the status unset

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"]

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"].

Two details worth knowing. The status is two-state: there is no way to express "pending" or "suspended" here, so an app with four states collapses into active and inactive. And a record whose id path finds nothing is skipped rather than imported half-formed. A mistyped id path therefore yields no users at all, which is why Lumos checks that path for you when you connect.

Every account is reported as a user account. The connector does not distinguish service accounts from user accounts.

A complete block for a SCIM-shaped API:

{
  "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

mapping

Yes

Field translation, detailed below

type is yours to choose. entitlementrolelicensegrouppermission, and team are the labels Lumos suggests, but any short identifier works — the Salesforce configuration uses permission_set. Whatever you pick, it must not be empty, and when you list several blocks each must use a different one.

Mapping field

Required

What it points at

id

Yes

The entitlement's unique identifier

label

Yes

The human-readable name shown in Lumos. If the path finds nothing, Lumos shows the id instead

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:

{
  "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. Lumos walks the blocks one after another, finishing each before starting the next. This verified Salesforce configuration lists permission sets and groups together:

[
  {
    "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" }
  }
]

Changing the number of blocks while a sync is running invalidates that sync's place in the list. Lumos says so, and the next sync starts cleanly from the beginning.

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 you 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:

{
  "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:

{
  "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. A group with no members contributes nothing, which is correct rather than an error.

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: the endpoint, the paging, and the user id all come from there. This block therefore has no endpoint or pagination of its own.

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:

{ "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:

{ "mode": "account_attribute", "account_entitlements_path": "developerAccountPermissions" }

If the entries turn out to be objects and you left attribute_entitlement_id_path out, Lumos stops and says so rather than silently importing nothing. A single value where a list was expected is handled as a list of one.

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 groups themselves are paged using the Entitlements Config block's own pagination

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

{
  "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" }
}

If  source_entitlement_type matches no block, or Entitlements Config is missing, Lumos reports the error along with the list of types you do have.

Mode: account_fanout

The mirror image. Ask each user individually what access they hold. The users come from Accounts Config, so there is no source_ field here.

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. The users themselves are paged using Accounts Config's pagination

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: Lumos remembers both which group or user it was on and how far it had got through that group's or user's own list.

Combining sources. Like entitlements, this block accepts a list, and Lumos finishes one block before starting the next. 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. An empty list, [], is a valid way to say "this app has no memberships to sync."

Resources Config

Describes how to list the app's containers. Workspaces, projects, sites, or profiles. This publishes them into Lumos as their own objects — Salesforce profiles, for instance. It is a single block, not a list.

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. As with entitlements, a label that doesn't resolve falls back to the id

{
  "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" }
}

One thing this block does not do: it does not scope access to containers. Every entitlement and every membership this connector reports belongs to the app as a whole, not to a particular workspace or project. If your app's roles differ per workspace, the connector can list both the workspaces and the roles, but it cannot express "this role, in that workspace." That needs a dedicated connector.

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. Reported exactly as the API wrote it, so an ISO-8601 value is what you want

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. If the path finds nothing on a record, the constant above is used for it

Records missing an id or timestamp are skipped, so users who never logged in don't produce broken events. Lumos reads whatever the endpoint returns for every user. It does not ask the API to narrow the results to particular people, because REST listings rarely support that in a uniform way.

{
  "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 and what the value is filed under, 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

Each entry does double duty: it declares the attribute to Lumos and tells the sync where to read it. account entries are read off user records while listing users, entitlement entries off entitlement records while listing entitlements. resource entries are declared but not filled in automatically — resource records don't carry custom attribute values today. A path that finds nothing on a given record simply leaves that attribute off that record.

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

[
  { "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" }
]

Use bracket-quoted paths for SCIM's long extension names, both in name and in path:

[
  {
    "name": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.employeeNumber",
    "path": "[\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\"].employeeNumber",
    "customized_type": "account"
  }
]

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, each with its own settings field, behind the Configure Write Capabilities checkbox. They all share one shape:

Field

Required

What it does

method

No

The kind of call: POST (the default), PUTPATCH, 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}, plus any extra field the request carries

Activate / Deactivate / Delete Account

{account_id}

Update Account

{account_id} or {id}, plus the fields being changed

Four useful details.

  • Placeholders in the url are safely encoded for web addresses, so an id containing a slash can't reshape the path.

  • 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".

  • If a field wasn't supplied for this particular action, any body key whose whole value is that placeholder is left out of the request rather than sent as null. That matters on a PATCH or PUT, where a null would blank the field in the target app.

  • A placeholder Lumos doesn't recognize is a configuration error, and the message lists the names that are available for that action — the quickest way to fix a typo.

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:

{
  "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:

{
  "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:

{ "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:

{
  "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:

{ "method": "DELETE", "url": "/scim/v2/Users/{account_id}" }

Important: 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 in the Examples at the bottom of the page covers the complete write lifecycle.

Lumos judges these actions by the HTTP result. If the call succeeds, the action is reported as done — a created account comes back as active, a deactivated one as inactive, a deleted one as deleted. An API that returns a success code while ignoring the request will therefore look like it worked, which is what Error Detection below is for.

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 section, and the Examples at the bottom of the page flag 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, reads and writes alike.

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.

{ "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. The two value-matching rules need their list of values filled in; a rule with no values is rejected the same way.

Two things these rules don't apply to. A response that isn't JSON at all — an HTML login page, an empty body — is already reported as its own clear error. That usually means the path is wrong, or the request was redirected to a sign-in page. And a body that is a bare number or string carries no field to check, so it passes through.

What Lumos checks when you connect

Connecting does more than store the form. Lumos deliberately tries to fail early, while you're still looking at the settings, instead of halfway through tomorrow's sync.

  • When you save the credential, Lumos checks its shape: that it's a credential this connector supports, that nothing required is blank, and that no secret has stray spaces from a bad paste. A deliberately empty API token is the one accepted blank, for open APIs.

  • When you connect, Lumos parses your Error Detection block, then makes real calls. It needs at least one of Credential Test Endpoint, Accounts Config, or Associations Config to have something to try; with none of them it tells you so.

  • The Credential Test Endpoint, if you set one, is called as-is.

  • The accounts endpoint, if configured, is fetched with a page size of one — and if there's more than one page, the second page is fetched too. That single extra call is the highest-value check in the whole form: it proves the credential works, that records_path finds a list, that mapping.id resolves to a real value, and that pagination actually advances. If mapping.id finds nothing, the message lists the field names the API did return, so you can correct the path in one pass.

  • Each association block gets one bounded page: one listing page, or for a fan-out mode, one group plus one page of its members. A wrong members_path or a source_entitlement_type that matches no block surfaces here.

If all of that passes, the connection is valid and Lumos records the tenant name — your Tenant ID if you set one, otherwise the Base URL's host.

What this connector cannot do

Worth knowing before you start, so you don't spend the afternoon looking for a setting that isn't there.

  • One call per action. No looking something up first, no merging into an existing list, no computing a value like "now plus a minute." Read-modify-write flows and dynamic timestamps need a dedicated connector.

  • Two account states. Active and inactive only. Pending, invited, and suspended all collapse into one of the two.

  • No per-container access. Entitlements and memberships apply to the app as a whole. Containers can be listed as resources, but access cannot be scoped to them.

  • JSON only. The API must return JSON. XML or CSV endpoints are out of reach.

  • Static field maps. A value either exists at a path or it doesn't. There is no place to write a rule that combines two fields, reformats a date, or filters records. There is also no way to ask the activity endpoint for a specific list of users.

  • Same-host pagination. Next-page links pointing at another host are refused, to keep credentials from traveling somewhere unintended.

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 the credential. If the token doesn't travel as Authorization: Bearer ..., tick Show Advanced Authentication Settings and set the header name and prefix. The API's authentication documentation usually 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, including a real paged call against your accounts endpoint. 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.

  6. Turn on writes last, and only after the reads look right. Tick Configure Write Capabilities, start with one action against a sandbox, and confirm it in the app before adding the rest.

If your API resembles one on the Examples at the bottom of this page, start from that configuration and change the paths. Matching on mechanics beats matching on vendor: an offset-paginated SCIM-style API can copy the SCIM Playground configuration nearly unchanged, whatever the app is called.

Generic REST Examples

Verified configurations for real APIs, shipped with the connector. Each demonstrates a different combination of authentication and pagination, so even if your target API isn't listed, the example matching its shape is your starting point. Every example was validated against a live tenant unless noted.

The configuration blocks below are shown pretty-printed for readability. In the connection settings, each block is pasted as JSON into its named settings field (Accounts Config, Associations Config, and so on). Replace the YOUR_...placeholders with your own values.

Example

Auth

Pagination

Example Capabilities

SCIM Playground

Token, or OAuth client credentials

Offset

Full read + write lifecycle

Databricks

OAuth client credentials

Offset

Full read + write lifecycle

ADP Workforce Now

OAuth client credentials over mTLS

Offset

Read

Salesforce

OAuth, both flows

Cursor (next URL)

Read + most writes

GitHub

Token (PAT)

Cursor (Link header)

Read

Datadog

Two-header keys

Page number

Read

HubSpot

Token (Private App)

Cursor (body token)

Read

Google Play Store

OAuth authorization code

None

Read + create/delete

SCIM Playground

The reference example and the safest place to learn the connector. The SCIM Playground at scim.dev (base URL https://api.scim.dev) is a free live SCIM 2.0 API. The example covers the full lifecycle: accounts, entitlements, associations, custom attributes via the SCIM enterprise extension, and every write operation, in both token and OAuth client-credentials variants. Use it to rehearse write configurations before pointing them at a real tenant.

Verified configuration. Settings: base_url = https://api.scim.dev, Token credential (your scim.dev API key), Auth Header Name Authorization, Auth Token Prefix Bearer.

Accounts Config

{
  "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"]
  }
}

Custom Attributes Schema:

[
  {
    "name": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User.employeeNumber",
    "path": "[\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\"].employeeNumber",
    "attribute_type": "string",
    "customized_type": "account",
    "description": "Enterprise employee number"
  }
]

Entitlements Config (groups):

{
  "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" }
}

Associations Config

The example ships both modes, so you can compare them against the same API. Endpoint mode reads the Groups listing and expands each group's members list:

{
  "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"
}

Account-attribute mode reads the group ids embedded on each user record instead (requires the Accounts Config above):

{
  "mode": "account_attribute",
  "account_entitlements_path": "groups",
  "attribute_entitlement_id_path": "value"
}

Assign and Unassign Entitlement Configs

(PatchOps against the group; unassign selects the member with a value filter):

{
  "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}" }] }]
  }
}
{
  "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}\"]" }]
  }
}

Create Account Config

{
  "method": "POST",
  "url": "/scim/v2/Users",
  "body": {
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "{username}",
    "active": true,
    "name": { "givenName": "{given_name}", "familyName": "{family_name}" },
    "emails": [{ "value": "{email}", "primary": true }]
  },
  "response_id_path": "id"
}

Deactivate Account Config (Activate is identical with "value": true):

{
  "method": "PATCH",
  "url": "/scim/v2/Users/{account_id}",
  "body": {
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "replace", "path": "active", "value": false }]
  }
}

Update Account Config (placeholders for the fields being changed):

{
  "method": "PATCH",
  "url": "/scim/v2/Users/{account_id}",
  "body": {
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "replace", "path": "name.givenName", "value": "{given_name}" }]
  }
}

Delete Account Config (no body needed)

{ "method": "DELETE", "url": "/scim/v2/Users/{account_id}" }

Databricks

Account-level SCIM over OAuth client credentials, with the full write lifecycle verified end to end (create, assign, unassign, deactivate, activate, delete).

  • Base host is the account console per cloud: accounts.cloud.databricks.com (AWS), accounts.azuredatabricks.net (Azure), accounts.gcp.databricks.com (GCP). You provide an account ID and an account-level service principal, not a workspace URL.

  • Token endpoint is <base>/oidc/accounts/{account_id}/v1/token with client_secret_basic, and the all-apis scope is required via the oauth_scopes setting.

  • Gotcha one: Databricks' SCIM Groups list omits members, so associations use entitlement_fanout mode (fetch each group individually and read its members), not flat endpoint mode.

  • Gotcha two: membership edits on the built-in account users system group return 403 PERMISSION_DENIED. Assign to custom groups only.

Verified configuration. Settings: base_url = https://accounts.cloud.databricks.com (or your cloud's account console host), OAuth client-credentials credential (service principal id and secret), OAuth Client Secret via HTTP Basic enabled, OAuth Scopes all-apis, OAuth Token URL <base>/oidc/accounts/YOUR_ACCOUNT_ID/v1/token.

Accounts Config:

{
  "endpoint": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Users",
  "records_path": "Resources",
  "pagination": { "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 50, "total_path": "totalResults" },
  "mapping": {
    "id": "id",
    "email": "userName",
    "username": "userName",
    "given_name": "name.givenName",
    "family_name": "name.familyName",
    "status_path": "active",
    "active_values": ["true"]
  }
}

Entitlements Config:

{
  "endpoint": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Groups",
  "records_path": "Resources",
  "pagination": { "style": "offset", "offset_param": "startIndex", "size_param": "count", "start_offset": 1, "page_size": 50, "total_path": "totalResults" },
  "type": "group",
  "mapping": { "id": "id", "label": "displayName" }
}

Associations Config (the fan-out):

{
  "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" }
}

Assign Entitlement Config (a SCIM PatchOp against the group):

{
  "method": "PATCH",
  "url": "/api/2.0/accounts/YOUR_ACCOUNT_ID/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}" }] }]
  }
}

Unassign Entitlement Config (the remove PatchOp, with a value filter selecting the member):

{
  "method": "PATCH",
  "url": "/api/2.0/accounts/YOUR_ACCOUNT_ID/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}\"]" }]
  }
}

Create Account Config:

{
  "method": "POST",
  "url": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Users",
  "body": {
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "{username}",
    "active": true,
    "name": { "givenName": "{given_name}", "familyName": "{family_name}" },
    "emails": [{ "value": "{email}", "primary": true }]
  },
  "response_id_path": "id"
}

Deactivate Account Config (Activate is identical with "value": true):

{
  "method": "PATCH",
  "url": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Users/{account_id}",
  "body": {
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [{ "op": "replace", "path": "active", "value": false }]
  }
}

Delete Account Config:

{ "method": "DELETE", "url": "/api/2.0/accounts/YOUR_ACCOUNT_ID/scim/v2/Users/{account_id}" }

ADP Workforce Now

OAuth client credentials over mTLS. ADP requires a client certificate on the token endpoint and on every API call, which the connector handles through the client_cert_pem and client_key_pem settings, presented on both the token exchange and data requests.

  • Base URL https://api.adp.com, token endpoint https://accounts.adp.com/auth/oauth/v2/token.

  • Accounts come from GET /hr/v2/workers with $skip/$top paging, mapping associateOID, the worker's name, and workerStatus.

  • You supply the service principal client ID and secret, and paste your ADP client certificate and private key as PEM.

Verified configuration. Settings: base_url = https://api.adp.com, OAuth client-credentials credential, Client Certificate (PEM) and Client Private Key (PEM) set to your ADP certificate pair.

Accounts Config (note the $skip/$top OData-style paging starting at 0, and the nested status path):

{
  "endpoint": "/hr/v2/workers",
  "records_path": "workers",
  "pagination": { "style": "offset", "offset_param": "$skip", "size_param": "$top", "start_offset": 0, "page_size": 50 },
  "mapping": {
    "id": "associateOID",
    "given_name": "person.legalName.givenName",
    "family_name": "person.legalName.familyName1",
    "status_path": "workerStatus.statusCode.codeValue",
    "active_values": ["Active"]
  }
}

Salesforce

The deepest example: OAuth 2.0 in both flows Salesforce supports (authorization code and client credentials), reads as SOQL queries with the nextRecordsUrl cursor, and writes as SObject REST calls.

  • Entitlements come from two typed blocks, permission sets and groups, and associations from two endpoint blocks reading the junction objects (PermissionSetAssignmentGroupMember).

  • Last activity maps LastLoginDate from the User object. Department and Title surface as custom attributes.

  • Writes cover assign, create, activate, deactivate, and update.

  • Where the boundary sits: unassigning a permission set requires deleting a junction record whose id needs a lookup query first, and Salesforce deactivates rather than deletes users. Both are out of a single templated call's reach, which is why the dedicated Salesforce connector in the Integration Library remains the recommended path for full provisioning.

Verified configuration. Settings: base_url = https://your-instance.my.salesforce.com, OAuth credential.

Accounts Config (a SOQL query as the endpoint, with the nextRecordsUrl cursor):

{
  "endpoint": "/services/data/v59.0/query?q=SELECT+Id,Username,Email,FirstName,LastName,IsActive,Department,Title,LastLoginDate+FROM+User",
  "records_path": "records",
  "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
  "mapping": {
    "id": "Id",
    "email": "Email",
    "given_name": "FirstName",
    "family_name": "LastName",
    "username": "Username",
    "status_path": "IsActive",
    "active_values": ["true"]
  }
}

Entitlements Config (an array of two typed blocks, permission sets plus groups; each type must be unique):

[
  {
    "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 (an array of two endpoint blocks, permission set assignments plus group members):

[
  {
    "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"
  },
  {
    "mode": "endpoint",
    "endpoint": "/services/data/v59.0/query?q=SELECT+UserOrGroupId,GroupId+FROM+GroupMember",
    "records_path": "records",
    "pagination": { "style": "cursor", "next_url_path": "nextRecordsUrl" },
    "account_id_path": "UserOrGroupId",
    "entitlement_id_path": "GroupId"
  }
]

Custom Attributes Schema:

[
  { "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" }
]

Provisioning. Writes are SObject REST calls. Assign creates a PermissionSetAssignment junction record:

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

Create Account Config:

{
  "method": "POST",
  "url": "/services/data/v59.0/sobjects/User",
  "body": { "Username": "{username}", "Email": "{email}", "LastName": "{family_name}", "FirstName": "{given_name}" },
  "response_id_path": "id"
}

Deactivate Account Config (a plain field PATCH, no PatchOp envelope; Activate flips IsActive to true):

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

Update Account Config:

{
  "method": "PATCH",
  "url": "/services/data/v59.0/sobjects/User/{account_id}",
  "body": { "FirstName": "{given_name}", "LastName": "{family_name}" }
}

GitHub

Read-only, demonstrating RFC-5988 Link-header pagination (use_link_header: true).

  • Token credential: a classic PAT with read:org, or a fine-grained token with organization Members read access, sent as Bearer.

  • Accounts from GET /orgs/{org}/members, entitlements from GET /orgs/{org}/teams.

  • Note: listing teams requires membership in the org. Against an org you don't belong to it returns 403 permission_denied.

Verified configuration. Settings: base_url = https://api.github.com, Token credential (the PAT).

Accounts Config (no records_path, since the response body is the array itself):

{
  "endpoint": "/orgs/YOUR_ORG/members",
  "pagination": { "style": "cursor", "use_link_header": true, "size_param": "per_page", "page_size": 100 },
  "mapping": { "id": "id", "username": "login" }
}

Entitlements Config (teams):

{
  "endpoint": "/orgs/YOUR_ORG/teams",
  "pagination": { "style": "cursor", "use_link_header": true, "size_param": "per_page", "page_size": 100 },
  "type": "team",
  "mapping": { "id": "id", "label": "name" }
}

Datadog

Read-only, demonstrating two-header authentication and zero-based page-number pagination.

  • The basic credential's username and password become the DD-API-KEY and DD-APPLICATION-KEY header values. The auth_header and second_auth_header settings hold the header names, never the secrets.

  • Site matters: the base URL must match your Datadog region (api.datadoghq.comapi.datadoghq.euapi.us3.datadoghq.com, and so on). A wrong-region host returns 403 Forbidden even with valid keys.

  • The users and roles endpoints need the user_access_read permission, so use an Application key from a Datadog admin.

Verified configuration. Settings: base_url = https://api.datadoghq.com, Basic credential (username = API key value, password = Application key value), Auth Header Name DD-API-KEY, Second Auth Header Name DD-APPLICATION-KEY. The header-name settings hold names only, the secrets live on the credential.

Accounts Config (zero-based page-number pagination with bracketed params):

{
  "endpoint": "/api/v2/users",
  "records_path": "data",
  "pagination": { "style": "page", "page_param": "page[number]", "size_param": "page[size]", "start_page": 0, "page_size": 100 },
  "mapping": {
    "id": "id",
    "email": "attributes.email",
    "username": "attributes.handle",
    "status_path": "attributes.status",
    "active_values": ["Active"]
  }
}

Entitlements Config (roles, same page-number pagination):

{
  "endpoint": "/api/v2/roles",
  "records_path": "data",
  "pagination": { "style": "page", "page_param": "page[number]", "size_param": "page[size]", "start_page": 0, "page_size": 100 },
  "type": "role",
  "mapping": { "id": "id", "label": "attributes.name" }
}

HubSpot

Read-only, demonstrating a nested body-token cursor: the next-page token is read from paging.next.after in the response body and echoed back as the after query parameter.

  • Token credential: a Private App token with the crm.objects.contacts.read scope.

  • The example maps CRM contacts as accounts to guarantee pagination volume. For an identity-object mapping, point the same config shape at /settings/v3/users instead.

Verified configuration. Settings: base_url = https://api.hubapi.com, Token credential (the Private App token).

Accounts Config (body-token cursor: read paging.next.after, echo back as ?after=):

{
  "endpoint": "/crm/v3/objects/contacts",
  "records_path": "results",
  "pagination": { "style": "cursor", "next_cursor_path": "paging.next.after", "cursor_param": "after", "size_param": "limit", "page_size": 100 },
  "mapping": {
    "id": "id",
    "email": "properties.email",
    "given_name": "properties.firstname",
    "family_name": "properties.lastname"
  }
}

Google Play Store

OAuth authorization code with Google's consent quirks handled, and a clear illustration of the connector's boundary.

  • The consent URL must carry access_type=offline and prompt=consent via oauth_extra_authorize_params, or Google never issues a refresh token.

  • Accounts come from the developer users endpoint, and associations use account_attribute mode, reading each user's embedded permission list.

Verified configuration. Settings: base_url = https://androidpublisher.googleapis.com, OAuth credential.

Accounts Config (the required pageSize=-1 is baked into the path, so pagination is none; note two active values):

{
  "endpoint": "/androidpublisher/v3/developers/DEVELOPER_ID/users?pageSize=-1",
  "records_path": "users",
  "pagination": { "style": "none" },
  "mapping": {
    "id": "email",
    "username": "name",
    "email": "email",
    "status_path": "accessState",
    "active_values": ["ACCESS_GRANTED", "ACCESS_STATE_UNSPECIFIED"]
  }
}

Associations Config (the permission entries are plain strings, so no attribute_entitlement_id_path is needed):

{
  "mode": "account_attribute",
  "account_entitlements_path": "developerAccountPermissions"
}

Provisioning. Create and delete are the two lifecycle operations the API allows as single calls. Note response_id_path is email, since Google Play identifies developer users by email rather than an opaque id:

{
  "method": "POST",
  "url": "/androidpublisher/v3/developers/DEVELOPER_ID/users",
  "body": { "email": "{email}" },
  "response_id_path": "email"
}
{ "method": "DELETE", "url": "/androidpublisher/v3/developers/DEVELOPER_ID/users/{account_id}" }

Choosing by your API's shape

Match on mechanics, not vendor. An offset-paginated SCIM-ish API: start from SCIM Playground or Databricks. A token-in-body cursor: HubSpot. A next-URL cursor: Salesforce. Link headers: GitHub. Multi-header keys: Datadog. Certificate-bound OAuth: ADP. If a capability needs logic beyond one templated HTTP call per operation, that capability belongs in the Connector SDK section.