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 |
|---|---|---|
| The name of the query parameter that carries the starting position |
|
| The parameter that carries how many records per page |
|
| The number the counting starts at. SCIM counts from 1. ADP counts from 0. The default is 0 |
|
| Optional. A dotted path to the total record count in the response. When present, this is what decides where the walk ends |
|
| How many records to ask for per page. The default is 100 |
|
{ "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 |
|---|---|---|
| The parameter carrying the page number |
|
| The parameter carrying the page size |
|
| The first page's number. Usually 1, but Datadog starts at 0. The default is 1 |
|
| Records per page. Default 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 |
| HubSpot returns |
A complete next-page web address inside the body |
| Salesforce returns |
A token in a response header |
|
|
A next-page address in the standard |
| 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 |
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 |
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 |
| The name of the header your token travels in. Some vendors use their own, like |
Auth Token Prefix |
| 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 |
Second Auth Header Name | empty | For APIs needing two secret headers at once. Datadog needs |
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: |
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 |
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 |
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 |
|---|---|---|
| Yes | The path that lists users |
| No | Where the list sits in the response. Leave out if the response is the list itself |
| No | How to get the next page. Defaults to |
| 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 |
|---|---|---|
| Yes | The user's unique, permanent identifier. Pick something that never changes |
| No | The user's email address |
| No | First name |
| No | Last name |
| No | Login name |
| No | The field that says whether the user is active. Leave out to leave the status unset |
| No | The values of that field that count as active. Comparison ignores upper and lower case. The default list is |
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 |
|---|---|---|
| Yes | The path that lists them |
| No | Where the list sits in the response |
| No | Defaults to |
| No | A one-word label for what kind of access this is. Defaults to |
| Yes | Field translation, detailed below |
type is yours to choose. entitlement, role, license, group, permission, 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 |
|---|---|---|
| Yes | The entitlement's unique identifier |
| Yes | The human-readable name shown in Lumos. If the path finds nothing, Lumos shows the id instead |
| No | A longer description, if the API provides one |
| No | Set to |
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 |
|
A groups listing where each group carries its member list |
|
User records that carry their own access list |
|
Only a per-group members endpoint, asked group by group |
|
Only a per-user access endpoint, asked user by user |
|
Mode: endpoint
Reads a membership listing.
Field | Required | What it does |
|---|---|---|
| No |
|
| Yes | The path to the membership listing |
| No | Where the list sits in the response |
| No | Defaults to |
| Yes | Dotted path to the access id on each record |
| Yes | Dotted path to the user id. On the record itself, or on each member entry when |
| 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 |
|---|---|---|
| Yes |
|
| Yes | Dotted path on each user record to its access entries |
| 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 |
|---|---|---|
| Yes |
|
| Yes | A path template containing |
| Yes | Which Entitlements Config block provides the groups to walk. Must equal that block's |
| No | Where the member list sits in each response |
| Yes | Dotted path to the user id on each member |
| 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 |
|---|---|---|
| Yes |
|
| Yes | A path template containing |
| No | Where the access list sits in each response |
| Yes | Dotted path to the access id on each row |
| 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 |
|---|---|---|
| Yes | The path that lists them |
| No | Where the list sits in the response |
| No | Defaults to |
| No | A one-word label like |
| Yes |
|
{
"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 |
|---|---|---|
| Yes | A path whose records carry a per-user timestamp |
| No | Where the list sits in the response |
| No | Defaults to |
| Yes | Dotted path to the user id on each record |
| Yes | Dotted path to the timestamp. Reported exactly as the API wrote it, so an ISO-8601 value is what you want |
| No | A constant label for the event, defaulting to |
| 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 |
|---|---|---|
| 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 |
| 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 |
| No |
|
| No | Which kind of record carries this attribute: |
| 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 |
|---|---|---|
| No | The kind of call: |
| Yes | The path to call, with placeholders in curly brackets that Lumos fills in |
| No | The JSON to send, also with placeholders |
| 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 |
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 |
|
Create Account |
|
Activate / Deactivate / Delete Account |
|
Update Account |
|
Four useful details.
Placeholders in the
urlare 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 anullwould 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 field present | "It's an error when this field holds anything real" |
|
Match on a success value | "It's only a success when this field says so. Anything else, or nothing at all, is an error" |
|
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_pathfinds a list, thatmapping.idresolves to a real value, and that pagination actually advances. Ifmapping.idfinds 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_pathor asource_entitlement_typethat 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
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.
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.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 yourpagination), and map the user fields (that's yourmapping). Set the status path and active values last.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.
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.
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/tokenwithclient_secret_basic, and theall-apisscope is required via theoauth_scopessetting.Gotcha one: Databricks' SCIM Groups list omits
members, so associations useentitlement_fanoutmode (fetch each group individually and read its members), not flat endpoint mode.Gotcha two: membership edits on the built-in
account userssystem group return403 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 endpointhttps://accounts.adp.com/auth/oauth/v2/token.Accounts come from
GET /hr/v2/workerswith$skip/$toppaging, mappingassociateOID, the worker's name, andworkerStatus.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 (
PermissionSetAssignment,GroupMember).Last activity maps
LastLoginDatefrom the User object.DepartmentandTitlesurface 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 fromGET /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
basiccredential's username and password become theDD-API-KEYandDD-APPLICATION-KEYheader values. Theauth_headerandsecond_auth_headersettings hold the header names, never the secrets.Site matters: the base URL must match your Datadog region (
api.datadoghq.com,api.datadoghq.eu,api.us3.datadoghq.com, and so on). A wrong-region host returns403 Forbiddeneven with valid keys.The users and roles endpoints need the
user_access_readpermission, 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.readscope.The example maps CRM contacts as accounts to guarantee pagination volume. For an identity-object mapping, point the same config shape at
/settings/v3/usersinstead.
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=offlineandprompt=consentviaoauth_extra_authorize_params, or Google never issues a refresh token.Accounts come from the developer users endpoint, and associations use
account_attributemode, 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.