RapidValueDocs RapidValue.eu
Docs/Connector reference

🌐 REST API (generic)

Every catalogue of named connectors ends somewhere. The generic REST connector is what sits past that edge: an in-house application, a regional payroll system, a niche tool with a JSON API and no IGA integration anyone has heard of. So it is worth being precise about the claim, because it is not that any REST API connects automatically. It is that the shape of an integration becomes configuration rather than code: which endpoints to read, how to turn a record into an identity or an entitlement, and which calls to make when the platform grants, revokes, creates or disables something. Somebody still has to know the target API and describe it. What they do not have to do is write, review, ship and maintain a connector — and that is the difference that matters, because a connector written for one customer has to be kept alive for as long as that customer has it.

The same engine backs the vendor entries that are "REST underneath" — Nmbrs, AFAS, TOPdesk, GitHub — which simply arrive with the configuration pre-filled.

The read model

A REST connector is described as a list of objects — users, groups, memberships, cost centres — each carrying four pieces of information.

What it is, in IGA terms. Each object declares an iga_target: identity, account, entitlement, context, and so on. This tells the platform whether the records coming back are people, accounts, permissions, or organisational structure. It is a modelling decision, not a technical one, and the one most often made wrongly on a first attempt.

Where to fetch it. A path relative to the connector's base URL, a method, any per-object headers, and an items_path — a dotted path naming where in the JSON response the array of records actually lives, because {"data": {"users": [...]}} is as common as a bare array. An object can also be derived from another object's records instead of being fetched at all.

How to shape it before mapping. Records can be filtered, deduplicated on named keys, enriched with aliases (named expressions written once and reused by a short name), and expanded through nested requests — a per-record follow-up call whose results attach to the parent or flatten into one record per child, which is how group-with-members is expressed.

How to turn a record into IGA attributes. Field mappings, one per target attribute, each with an operator: passthrough copies a source field by dotted path, constant writes a fixed value, map looks up through a table with a default, and expression renders a template over the record. These are the seam where the target's vocabulary becomes the platform's; they are covered in depth under Provisioning Mappings.

Unlike the SCIM connector, which ships with /Users and /Groups already described because the standard says what they are, the generic REST entry ships with no objects at all — there is nothing to guess at.

Discover, inspect, then adopt

The onboarding wizard can run a live sample for each configured object, expose the discovered fields and let an administrator adopt mappings into RapidValue's canonical model. Multiple discovered object types can be mapped in one connector; derived and nested objects cover relationships that are not returned as one flat array.

The sample is evidence for a mapping, not a schema guarantee. It does not make an arbitrary multi-object merge safe automatically, and it does not prove that a write operation exists. Read and write capabilities remain explicit per engine, object and operation.

Write operations as configuration

The write side is a dictionary of operations keyed by what the platform is trying to do. Six keys are dispatched by the engine:

Key Fired when
create_account an account must be provisioned on the target
grant_membership an entitlement is granted
revoke_membership an entitlement is revoked
update_identity an attribute change is propagated
disable_account an account is disabled
enable_account an account is re-enabled

Each carries a method, a path, an expected set of success codes, an optional body template, and optionally a dotted path naming where in the response the target's identifier for the new object can be found — which matters more than it looks, because a create that succeeds without yielding an identifier has produced something the platform cannot address again.

The body template is a JSON object whose string values may be expressions. What those expressions can see depends on the operation: a grant or revoke sees the two identifiers and nothing else, because that is all the platform passes; a create sees the identity's mapped attributes. For attribute propagation, leaving the template empty sends the already-resolved changes directly, since the field mappings have already translated them into the target's own field names.

One write model, several protocols

The same configuration shape is read by the SQL engine, which interprets it natively rather than as HTTP: the path is a validated table name, the body template maps column to value, and the verb is fixed by the operation rather than taken from the config — a grant is an INSERT, a revoke a DELETE — because a dropdown offering only HTTP methods cannot express a SQL statement. Values are always bound as parameters. Where a body template is absent, creates and attribute updates fall back to the already-resolved field mappings; grants and revokes have no such fallback and say so, because the platform passes them two identifiers and nothing that would let it guess a column name.

That reuse is deliberate: adding a protocol did not fork the configuration model. One shape, one editor, one thing to learn, one place to change. The alternative — a second schema per protocol — is how integration layers become impossible to document.

One operation, several variants

grant_membership above is one dispatch key, not necessarily one call. A target frequently needs a genuinely different call for the same logical operation depending on who it is called for — the motivating case: on Microsoft Entra, creating a Personal account is POST /users, but creating a B2B Guest account is a different endpoint entirely, POST /invitations. One dispatch key, two calls, chosen by account type.

So each operation key holds a list of variants, and every variant may be scoped along up to three independent axes:

Axis Scopes by
Account type which account category the call applies to (Personal, B2B Guest, Service…)
Entitlement type which kind of entitlement is being granted or revoked
Assignment kind member vs. owner, where the target distinguishes them

An unscoped variant is the fallback; a scoped one wins over it whenever it matches, and among several matching variants the most specific one wins — the same most-specific-wins rule field mappings already use. Critically, the three axes are independent of each other and of the field mappings and permissions configured for the same operation:

  • Adding a Guest-scoped variant of create_account does not require touching the field mappings for Personal accounts, or vice versa — each variant maps its own fields.
  • Which account types are allowed to perform an operation at all (Systems → Account Types → Writes) is a separate decision from which call fires for them. Revoking the permission does not change the call a permitted type would make; it only stops that type from reaching it.

A variant match is all-or-nothing across its scoped axes — a variant scoped to "owner edge, admin account" fires only for an owner edge on an admin account, never partially for either alone. Getting one axis wrong sends the wrong call to the target rather than a close-enough one, so the platform never degrades a partial match into a fired call.

A write can be a sequence

Some targets cannot express one logical operation as one call: a resource must be created before a user can be linked to it; an upsert has to be attempted as a create and fall back to an update. Rather than answer each case with a bespoke connector, an operation may instead be an ordered list of steps over a growing context — values computed once and shared across steps, each step's captured response available to the ones after it, a condition that skips a step, and non-success codes tolerated rather than treated as failures, such as a 409 where a conflict means "already exists" rather than "broken".

Which raises the question a sequence always raises: what happens when step three fails after step one has already changed something in the target. Each step may therefore carry a compensating call — the same shape as a step, describing how to undo that step's own effect. When a later step fails hard, every earlier step that succeeded and carries a compensation is undone in reverse order, most-recently-succeeded first, before the failure is reported.

"grant_membership": {
  "vars": { "op_id": "{{ account_external_id }}-{{ entitlement_external_id }}" },
  "steps": [
    {
      "id": "create_position",
      "method": "POST",
      "path": "/operationalPositions",
      "success_status": [200, 201],
      "response_id_path": "id",
      "body_template": {
        "operationalPositionId": "{{ vars.op_id }}",
        "typeId": "{{ entitlement_external_id }}"
      },
      "compensate": {
        "id": "delete_position",
        "method": "DELETE",
        "path": "/operationalPositions/{{ steps.create_position.id }}",
        "success_status": [200, 204, 404]
      }
    },
    {
      "id": "link_user",
      "method": "PUT",
      "path": "/operationalPositions/{{ vars.op_id }}",
      "success_status": [200, 204],
      "body_template": {
        "userList": [
          { "userId": "{{ account_external_id }}", "isMain": true,
            "startDate": "{{ today }}" }
        ]
      }
    }
  ]
}

If link_user fails, delete_position runs — addressed by the identifier the create actually returned, not one the platform guessed — and the operation reports failure with an unchanged target rather than an orphaned position nobody knows about.

Three properties are worth stating exactly, each a limit as much as a feature.

A step never compensates its own failure. Compensation undoes work that succeeded; a call that failed has, as far as the platform can tell, done nothing to undo, and a target that half-applies a failed request is outside what this can repair. The same holds for a step skipped by its condition and for one whose non-success code was tolerated: neither succeeded, so neither is undone.

Compensation is best-effort. A compensating call that fails does not stop the remaining ones being attempted — undoing four of five changes beats abandoning the rollback at the first difficulty. The goal is to undo as much as possible, not to guarantee a clean target.

Every attempt is recorded on the result, successes and failures alike, so the failed job in the queue says both what broke and what was rolled back — see Provisioning Jobs. Silently skipping a compensation would produce exactly the invisible orphan the mechanism exists to prevent.

Authentication, and why the token is minted first

The generic REST entry offers no authentication, an API key in a named header, a bearer token, HTTP basic, and OAuth2 client credentials. The engine additionally supports the authorization-code grant run unattended from a stored refresh token, which is how certain vendor entries authenticate.

For both OAuth2 methods, a token is obtained by a real exchange before the health check makes its request. That ordering is the entire point. Where the token is minted only for client credentials, a connector on the authorization-code grant attaches no credential at all — and if the configured health path happens to be publicly readable, the connection check returns green for a connector that cannot read a single protected record. The wizard advances, activation raises nothing, and the integration is found to be broken by the first sync that matters.

Doing the exchange first converts that into an honest failure: wrong credentials fail the exchange, and that failure is the reported result. A connector on the authorization-code grant with no stored refresh token is likewise refused with an explanation rather than quietly making unauthenticated calls.

This is one instance of a general posture: a step called "test the connection" must be capable of failing. The onboarding walkthrough covers the gate it feeds.

TLS trust

Verification is on by default, and there are two ways to deal with a target whose certificate does not chain to a public root.

The intended one is the tenant's trusted certificate store: an internal CA certificate — or a specific self-signed target certificate — is added once, and every outbound connector connection for that tenant then verifies against the system trust store plus those certificates. Because it is not a per-connector paste, an internal CA is described once rather than copied into each system using it, and the store is read fresh when an engine is built, so removing a certificate takes effect without editing any connector.

The other is a per-connector skip verification switch, which disables certificate checking for that connector entirely. It is an escape hatch, labelled as one, and it exists because the alternative — administrators concluding that the only way past a certificate error is to disable TLS somewhere broader — is worse.

Both apply in all three execution modes, resolved by the same logic in each, so the answer cannot differ by where the call is made from.

Known edges, published rather than discovered

  • The generic engine fetches one page. Objects carry pagination fields, and vendor templates populate them, but the generic REST read path issues a single request per object and takes the records it returns. Following a nextLink or walking an offset exists only in the connectors with their own read path — Entra follows Graph's @odata.nextLink, SCIM walks startIndex. For a target whose collections exceed one page, the answers today are a server-side page size or filter in the configured path, or a vendor connector. This is the sharpest limit here, and the one most likely to be met first.
  • SOAP is declared, not built. It appears in the catalogue as a coming-soon protocol and shares the write model by intent, but there is no runtime engine behind it and the wizard will not let you select it. Legacy targets are reached over REST, SQL, or a flat-file export over SFTP — whose write-back, though real, has a fixed shape rather than using this model.
  • Nested bodies render only inside a sequence. A single-call operation renders only the top-level string values of its body, so a template buried in a nested structure goes out unrendered. If a body has structure, express the operation as a one-step sequence.
  • A template that fails to render nulls its field rather than abandoning the write. The failure is logged, but the call still goes out with a null where a value was configured — a deliberate trade, and a reason to test a body template rather than assume it.
  • A create that returns no usable identifier still counts as a success, because it was one. But the platform then holds an account it cannot address, which is how an orphan begins: configure the response identifier path. Read/Write Parity covers the wider version of this.
  • The vendor-SDK connectors do not read the TLS options. Entra, Google Workspace, ServiceNow, Salesforce and Box authenticate through their own client libraries; the certificate store and the skip-verify switch do not reach them, and the control is hidden there rather than shown and ignored. A self-hosted instance of one of those products is not a supported target.
  • Multi-step sequences are authored as JSON. Objects, field mappings and single-call write operations have editors; a sequence and its compensating calls are written by hand. A step-level editor is a known gap, not a shipped feature.
  • Nothing here validates the target's semantics. The platform can tell you a call returned 200; whether the resulting state is the one you intended is a question only reconciliation answers.

Further reading:

Did this answer your question?One click records the page; add detail by email if something is missing.

Try “tenant isolation”, “role mining” or “Entra”.