Credential Synchronization
Credential Synchronization automatically turns users from Database Provider, LDAP Active Directory, API Authentication Provider, and OIDC Provider (Keycloak only) sources into Credential records, either on a schedule or on manual trigger. This keeps credentials centrally up to date from the external source instead of being created manually for each provider.
Sync Profile
Each identity provider (Database Provider, LDAP Active Directory, API Authentication Provider, OIDC Provider) has a separate Sync Profile (or, for OIDC, Synchronization) tab in its edit screen. The common fields in this tab are:

| Field | Description |
|---|---|
| Enable Synchronization | When enabled, credentials are synchronized from this source according to the configured schedule. When disabled, neither the scheduled nor the manual synchronization runs. |
| Cron Expression | A Quartz cron expression that determines how often synchronization runs (e.g. 0 0 2 * * ?). If left empty, synchronization can only be triggered manually from the Synchronization screen. |
| Deactivation Mode | The action applied to credentials — and, when group synchronization is on, to organizations — whose corresponding user or group is no longer present in the source on the last run: • Disable (recoverable) — the credential is disabled but its record is kept. This is the default. • Delete (recoverable) — access is cut the same way it always was, but the record itself is no longer physically deleted: a credential is disabled and retired in place, an organization is retired in place with the bond to its child credentials preserved. Permanent cleanup of retired records is planned as a separate maintenance capability. • None (keep as is) — no action is taken; drift is accepted. |
Delete is not the same thing as the Tombstone scope exit policy, even though both retire a record in place rather than removing it. Tombstone leaves the record's access untouched — it only marks it; Delete still cuts access, exactly as it did before this version, it just no longer removes the row while doing so.
The option is still labelled Delete (irreversible) on the dropdown itself — the label has not caught up with the change described above yet. The behavior above is the current one regardless of that label.
Provider-specific additional fields are described below.
LDAP Active Directory
| Field | Description |
|---|---|
| Base OU DN | The root DN of the OU (Organizational Unit) tree to synchronize. Example: OU=Users,DC=acme,DC=corp |
LDAP synchronization synchronizes not only users but also the organization tree the users belong to. The OIDC (Keycloak) source can also synchronize the group tree in addition to users when Synchronize Groups is enabled. The remaining sources (Database, API) synchronize users only.
Database Provider
| Field | Description |
|---|---|
| User List Query | A parameterless SELECT statement that returns the users to synchronize. The returned columns must use the following aliases: username (required), email, full_name. Example: select login as username, mail as email, display_name as full_name from users |
API Authentication Provider
| Field | Description |
|---|---|
| User List URL | The endpoint that returns the list of users to synchronize. |
| HTTP Method | The HTTP method used when calling the user list endpoint. |
| Timeout | The request is terminated after this many seconds. |
| Request Body | An optional request body sent to the user list endpoint (for POST/PUT). |
| Request Headers | Optional HTTP headers sent with the user list request (e.g. Authorization). |
| Users Array JSON Path | The JSON Path expression to the user array in the response. Default: $ |
| Username JSON Path | The JSON Path expression to the username within a user item. Required. |
| Email JSON Path | The JSON Path expression to the email within a user item. |
| Full Name JSON Path | The JSON Path expression to the full name within a user item. |
OIDC Provider (Keycloak)
Available only when the provider's Vendor is Keycloak — see OIDC Provider for the full field reference.
| Field | Description |
|---|---|
| Realm | The Keycloak realm to synchronize from. |
| Admin API Base URL | The base URL of the Keycloak Admin REST API. |
| Sync Client ID | The service-account client used to call the Admin API. Requires, at minimum, the view-users and query-groups realm-management roles — see Keycloak Service Account Roles. |
| Sync Page Size | The page size used when paging through Keycloak's user list. Default: 250. |
| Synchronize Groups | When enabled, the realm's group tree is also synchronized as Credential organizations. |
| Role Sync Source | Realm Roles, Client Roles, or Groups — which Keycloak role assignment becomes the synchronized user's roles. |
Unlike the other three sources, users synchronized from Keycloak are passwordless — the password stays with Keycloak, and authentication is verified through the OIDC Authentication policy's token validation, not through a stored password. See Password Model and Username Contract.
Synchronization Scope
By default a source synchronizes everything it can read. The Synchronization Scope card on the same tab narrows that set, with separate include/exclude pattern lists for Users, Groups / Organizations (LDAP and OIDC only), and Roles (OIDC only).
| Field | Description |
|---|---|
| Pattern Type | Glob — * matches any number of characters (the / separator included), ? matches exactly one, and every other character is literal, so a . or a ( in a pattern means that character. Regular Expression — the pattern is compiled exactly as written; see the warning below. |
| Matched Field | Which field of the record the patterns are applied to. Users can be matched on username, e-mail or full name on every source, on external identity on LDAP and OIDC, and on their DN (path) on LDAP. Groups are matched on path or name, roles on name. |
| Case Sensitivity | Off by default: patterns match regardless of case. |
| Include Patterns | While empty, every record is a candidate. Once at least one pattern is present, a record has to match one of them to stay in scope. |
| Exclude Patterns | A record matching one of these is dropped even when an include pattern also matches it — exclude always wins. |
A pattern matches the whole value, not a part of it: admin matches the user named admin and nothing else, while *admin* matches every username that contains it.
When the matched field is Path, both the record's value and the pattern are normalized before they are compared, so an LDAP DN and a Keycloak group path reduce to the same shape: CN=Dev,OU=Eng,DC=acme,DC=corp and /Eng/Dev both become /Eng/Dev. One pattern therefore means the same thing on every source, and you may write it in whichever dialect you find natural — ou=eng,dc=acme and /Eng/* are equally valid.
Filter patterns are compiled once, at the very start of the run. A pattern that does not compile, that is longer than 512 characters, or that is rejected as unsafe to evaluate (a shape that can make matching take exponentially long on non-matching input) ends the run as Failed — the run does not carry on as though the filter were absent. Continuing without it would bring back into scope everyone an exclude pattern was meant to drop, which is by far the more dangerous of the two outcomes. Globs are checked the same way, because a pattern such as *a*a*a*a*b is just as expensive once translated.
What Happens to a Record That Leaves the Scope
A user that still exists in the source but no longer matches the filter is not treated as removed from the source. Its credential is stamped as seen by the run — so the reconciliation step does not count it as stale — and the Scope Exit Policy decides what happens to it:
| Policy | Effect |
|---|---|
| Leave as is (default) | The credential is left untouched. |
| Disable | The credential is disabled; the record is kept. |
| Tombstone | The credential is marked as tombstoned. |
A manually created credential, and a credential owned by another identity source, are never touched by this policy — the same protections that apply to synchronization in general apply here.
How many records the filter dropped in a run is shown in the Filtered column of the run history. No per-user issue record is written for them: a scope narrowing that drops tens of thousands of users would otherwise fill the run's issue list and push the real skip reasons out of it.
Reconciliation Safety Net
The fields below sit under the scope filter and govern what reconciliation is allowed to do.
| Field | Description |
|---|---|
| Scope Exit Policy | What happens to a record that leaves the scope — see above. Default: Leave as is. |
| Deactivation Threshold (%) | When the records that would be deactivated exceed this share of the records the source manages, reconciliation does not run and waits for approval — see Deactivation Threshold and Reconcile Approval. Default: 20%. |
| Deactivation Threshold (records) | The same check by absolute count. Default 0, which leaves only the percentage check in effect. |
| Do not deactivate or delete when the source returns no stable external identity | Off by default. See the warning below. |
Reconciliation decides that a user is gone by not seeing it in the run. Without a stable external identity — an id that stays the same when the user is renamed — the only key is the username, so a user renamed in the source looks removed, and a destructive reconciliation acts on that appearance.
Which sources return such an identity today:
| Source | Stable external identity |
|---|---|
| OIDC (Keycloak) | Yes — the user's Keycloak UUID. |
| LDAP / Active Directory | Only when a stable attribute (for example entryUUID or objectGUID) is among the attributes fetched for the user object class. Synchronization does not request one on its own — changing the existing directory queries would be a change of behavior in itself. |
| Database | No — the user list query contract has no identity column. |
| API | No — the response mapping has no identity field. |
Current behavior is unchanged in this version. A source with no stable identity still disables or deletes exactly as it did before; the run only records a warning in its issue list so that the risk is visible where it happens. Turning Do not deactivate or delete when the source returns no stable external identity on makes reconciliation stop for that source instead of acting on a username.
That opt-in is planned to become the default in a future major version. Turning it on early for Database and API sources — where a stable identity cannot be obtained at all — is the safe direction.
Synchronization Status and History
The screen opened from Identity > Providers > Synchronization lists LDAP, Database, API, and OIDC sources in separate tabs. Each row shows:

| Field | Description |
|---|---|
| Name | The name of the identity provider. |
| Schedule | The configured cron expression. |
| Status | The job status: Scheduled, Running, Paused, Blocked, Complete, Error, or Undefined (never run). |
| Last Synchronized | The completion time of the last run. |
| Upserted, Deactivated, Skipped, Errors | The record counts processed on the last run. |
| Result | The last run's outcome: Succeeded, Warning, or Failed. |
The Synchronize Now button on this screen triggers synchronization for the corresponding source immediately, without waiting for its schedule.
Synchronize Now starts the run in the background and returns at once — the counts shown the moment the button is pressed still belong to the previous run.
The run record does not wait, though: a row appears in the Run History table the moment the button is pressed, in Queued state; it becomes Running once a node picks it up, and takes its result when it finishes. A run that never manages to start is therefore still visible on screen.
Seeing the result no longer requires a manual refresh on this screen: the history table follows the running synchronization live, and the moment the run finishes, the summary counts on the source rows above are refreshed too. The message shown after the trigger says as much — it no longer asks you to refresh the table.
Synchronize Now cannot be run for a source whose synchronization is disabled (the Enable Synchronization field is off).
Synchronize Now is also reachable from an individual synchronized record's own detail screen — the Synchronization tab/section on a credential or organization — for users with identity management permission. Triggering it there runs the same synchronization for the record's entire source provider, not only that one record.
Choosing What a Manual Run Covers
On LDAP and OIDC sources the Synchronize Now button carries a scope menu; pressing the button itself still runs a full synchronization.
| Scope | What it does |
|---|---|
| Full | Users, the group/organization tree and roles, then reconciliation. This is what a scheduled run always does, and what Synchronize Now did before this version. |
| Users only | Reads users only. The group/organization tree is not read — so it is not reconciled either, and users keep the organization and roles they already have instead of being cleared by a pass that fetched nothing. A user disabled in the source is still reflected as disabled. |
| Groups only | Reads the group/organization tree only. No user is read or written; only the organization reconciliation runs. |
Database and API sources synchronize users only, so their button has no menu. A run's scope is shown in the Scope column of the run history; rows recorded before this version show Full, which is what those runs were.
The distinction the run scope carries is between "it was fetched and came back empty" and "it was never fetched". A users-only run does not read the group tree, so it makes no statement about organizations and roles, and the engine leaves both alone. A full run that reads the tree and finds a user in no group does make that statement, and the user's organization is cleared — as it always has been.
Run History
Beyond the last-run snapshot in the table above, Apinizer keeps a permanent record of every synchronization run. A Synchronization History card appears at the bottom of two screens:
- The Identity > Providers > Synchronization screen described above — the card here lists runs from every source in the current project, with a Source column identifying which provider each row belongs to.
- Each provider's own view screen (LDAP Active Directory, Database Provider, API Authentication Provider, OIDC Provider) — the same card, scoped to that source only, without the Source column.
Both surfaces show the same columns, paged from the server (10 / 20 / 50 rows per page):
| Column | Description |
|---|---|
| Source | The provider's name and type. Shown only on the project-wide Synchronization screen. |
| Started | The time the run was queued — the row is born at this moment. |
| Duration | How long the run actually executed; time spent waiting in the queue is not counted. |
| Trigger | Scheduled or Manual; for a manual run, the triggering administrator's username is shown underneath. |
| Scope | What the run covered: Full, Users only, Groups only, or Reconcile apply — see Choosing What a Manual Run Covers. Rows recorded before this version show Full. |
| Result | The run's current state: Queued, Running, Success, Warning, Failed, Interrupted, Cancelled, or Awaiting Reconcile Approval — see Run Lifecycle. |
| Upserted, Deactivated, Skipped, Unchanged, Errors | The record counts processed by this run — see Skipping Unchanged Users for what Unchanged means. |
| Filtered | How many records the synchronization scope filter dropped. Runs recorded before the filter existed show - rather than 0: not knowing and dropping nothing are different statements. |
| Issues | How many per-user issues this run recorded. Click the count to expand the row and read the reasons — see Run Issue List. Runs that executed before the issue log existed show - in this column. |
| Reason | A translated explanation of the result. A Failed, Interrupted, or Cancelled run also shows a reason badge classifying why — see Failure Reason. |
| Actions | Carries Approve Reconciliation while a staged reconciliation is still approvable — see Deactivation Threshold and Reconcile Approval. Carries Cancel while the run is still Queued or Running — see Cancelling a Run. |
Failure Reason
When a run's result is Failed, Interrupted, or Cancelled, the Reason column also carries a small reason badge classifying why, together with a tooltip that suggests what to do next — the full, untranslated error message stays available for deeper troubleshooting.
| Reason badge | When it applies | What to do |
|---|---|---|
| Unreachable | The source could not be reached at all — a network, DNS, or TLS failure. | Check network connectivity to the source and its TLS configuration. |
| Timeout | Connecting to the source, or reading from it, did not finish within the configured timeout. | Check whether the source is responsive, or raise the timeout if it is simply slow. |
| Auth rejected | The source rejected the credentials used to connect (an HTTP 401, or the equivalent LDAP bind failure). | Check the credentials configured for this source. |
| Permission missing | The connection succeeded, but the account used does not have permission to read what this run needed (an HTTP 403, or the equivalent LDAP permission failure). The tooltip names which read failed — Users, Groups, or Roles. | Grant the missing read permission for the named area. For an OIDC (Keycloak) source, see Keycloak Service Account Roles. |
| HTTP error | The source returned an HTTP error other than 401 or 403. The status code is shown alongside the badge. | Check the source's own logs for that status code. |
| Configuration | The run could not even attempt to reach the source — a required connection field was missing or invalid. | Check the source's connection settings. |
| Interrupted | The run went silent before finishing — see Run Lifecycle. | Trigger the synchronization again. |
| Cancelled | The run was cancelled before it finished — see Cancelling a Run. | No action needed; trigger a new run if the work is still wanted. |
| Unknown | The failure does not match any of the classifications above. | Check the untranslated error message for detail. |
Every run is recorded — including a run that failed outright and a run that was rejected during pre-checks before synchronization could even start (for example, a missing connection or a required field left empty). The run an administrator is looking for is often the one that didn't work. Because the row is created before the scheduler is triggered, even a run whose trigger failed leaves a row behind, carrying the Failed result and its reason; previously such an attempt left no trace at all.
A Warning result means the run finished without error, but its deactivation/reconciliation step was skipped as a safety measure. There are two possible causes:
- The source returned zero users. Nothing is upserted, deactivated, or skipped-and-counted for that run. Check the source's user filter, base DN, or query settings.
- The user list could only be read in part. The directory server did not support paged results (or did not allow them), so only the first page was read. The records that were read are upserted normally; the only step skipped is deactivation — because a partial list would make every record on an unread page look as if it no longer existed in the source. The same applies when even a single record read from the directory cannot be converted: the run counts as incomplete and deactivation/deletion is skipped. See LDAP Active Directory.
The goal is the same in both cases: keeping a transient source outage — or an incomplete read — from causing a mass deactivation.
The source name in history is a snapshot captured when the run was recorded. Renaming the provider does not change that snapshot. Deleting the provider removes its run history and issue lists together.
Run Lifecycle
Every run is born Queued, becomes Running once a Manager node takes it over, and ends in a result state. Result states are final — once a run has concluded, its result no longer changes.
| State | Meaning |
|---|---|
| Queued | The run is recorded and the scheduler has been triggered; no node has picked up the work yet. |
| Running | A Manager node is executing the run and reports a heartbeat while it works — throughout every phase, including the initial fetch from the source. A large source (tens of thousands of users) that takes longer than ten minutes to read is therefore not mistaken for an abandoned run. |
| Success | The run completed; upsert and (where configured) deactivation both ran. |
| Warning | The run finished without error, but its reconciliation step was skipped as a safety measure — see the Warning note above. |
| Failed | The run errored, was rejected in pre-checks, or could not be triggered at all. |
| Interrupted | The run went silent before finishing: the node executing it stopped sending heartbeats (it was restarted, shut down, or lost the network). Its counters may be incomplete. |
| Cancelled | An administrator cancelled the run before it finished — see Cancelling a Run. Nothing fetched during the run is written: records read from the source are held in memory until the fetch completes, and a cancellation during the fetch discards them along with skipping the deactivation/reconciliation step. |
| Awaiting Reconcile Approval | The run finished without error and its upsert step ran, but the number of records that would have been deactivated exceeded the deactivation threshold, so the reconciliation step did not run at all and is waiting for an administrator. Nothing was deactivated or deleted. See Deactivation Threshold and Reconcile Approval. |
A maintenance task runs every five minutes and scans for unfinished runs. A Running run whose heartbeat has been silent for roughly ten minutes, and a Queued run that no node has taken over for roughly fifteen minutes, are both marked Interrupted, which releases the source so it can be synchronized again.
When you need to tell the two apart in a customer environment, the Manager application log is enough at INFO level: the sweeper writes a WARN line for every run it marks — Identity sync run <id> ... marked INTERRUPTED: no heartbeat for <n>s (lastHeartbeatAt=..., phase=..., processed=...) — and the run itself writes fetch starting, one ... phase complete line per phase (groups, roles, users) with the number of source requests and the elapsed time, and fetch complete. A source request slower than five seconds is logged as SLOW Keycloak admin read with its path and duration. If fetch starting is present but no phase complete follows and the node kept running, the source is answering too slowly; if the log simply stops, the node went down. With several Manager replicas, check every replica's log.
Marking a run interrupted never touches the source's last-synchronization counters: the abandoned run produced no counts of its own, so the numbers from the previous run stay as they were rather than being overwritten with a misleading zero.
Runs in the Awaiting Reconcile Approval state are not part of that scan. Such a run has not died, it has finished — it simply carries a decision nobody has applied yet. Sweeping it would turn the window an administrator has to review the approval into ten minutes.
Cancelling a Run
A Cancel action appears in the Actions column of the run history table while a run is still Queued or Running, for administrators with identity management permission on the source.
Cancelling does not stop the run instantly. The engine checks for the cancellation request at the next page boundary while it pages through the source, and stops fetching there. Records read from the source during the fetch are held in memory, not written as they arrive, so a cancellation during the fetch discards them — nothing from that run reaches a credential. The deactivation/deletion step (reconciliation) is skipped as well, for the same reason a partial read skips it elsewhere: a partial list would make every record on a page that was never fetched look as if it no longer existed in the source.
The run's own result becomes Cancelled, and because reconciliation did not run, the source's last-synchronization status reads Warning, the same as any other run whose reconciliation step was skipped as a safety measure.
Cancelling a run that has already finished is refused with 409 and a translated "already finished" message.
Deactivation Threshold and Reconcile Approval
The reconciliation step disables or deletes the credentials that no longer appear in the source, according to the Deactivation Mode setting. A single misconfiguration on the source side — a wrong base DN, a narrowed search filter, a group that emptied out — can turn that step into an operation that cuts off thousands of consumers in one run.
So reconciliation takes two counts before it starts: how many records this run would touch, and how many sync-managed records the source owns in total. If the ratio exceeds the deactivation threshold, reconciliation does not run at all — not a single record is written — and the run closes in the Awaiting Reconcile Approval state.
The threshold values are configured in each identity provider's synchronization settings; the percentage threshold defaults to 20%, and the count threshold is undefined by default (only the ratio is checked). Setting a threshold to zero or a negative number turns that check off; with both turned off no measurement is taken at all.
The check belongs to the destructive path only: it is made when the Deactivation Mode is Disable or Delete, and skipped when it is None, where nothing would be deactivated in the first place. It is also a safety net rather than a policy — a filter change small enough to stay under the threshold does not trigger it, and what happens to those records is decided by the scope exit policy.
While a run is awaiting approval:
- The source is not locked. The run has closed, so the source can be synchronized again and scheduled runs keep working. A new threshold breach that happens while you wait refreshes the pending approval with the current measurement — each source has at most one approval waiting, and it always carries the newest run's numbers. The approve action then belongs to that newer row: approving from the older one is refused, because the numbers on that screen are no longer the numbers that would be applied.
- The source's last synchronization result reads as Warning. That is accurate: the run finished without error and nothing was deactivated. The wait for approval lives on the run row, not in the source's summary.
- An approval is valid for 24 hours. A lapsed approval cannot be applied; the source's next run produces a new one from a current measurement.
When an administrator approves reconciliation from the row in the Synchronization History table, that same run does not resume: a new run starts that only applies the reconciliation. It fetches nothing from the source and applies exactly the measurement that was approved.
Immediately before touching the first record, the apply run re-verifies the approval: the candidate list, the synchronization filter and the deactivation mode must be identical to what they were when the approval was granted. If the filter was edited in the meantime, if the mode was switched from disable to delete, or if a normal run re-stamped some of the candidates, the apply is refused, the run ends as Failed, and no record is changed.
The way forward in that case is to run the synchronization again and approve the new result. The system never silently recomputes and applies — an approved number differing from the work performed is exactly what this mechanism exists to prevent.
The approving user is recorded, and an approval is single-use: once applied it cannot be used again. No separate permission is required — an administrator who can manage the identity provider can approve.
Following a Running Synchronization
While a Queued or Running row is visible in the Synchronization History table, the table updates itself silently about every five seconds: the paginator does not jump back to the first page, an expanded issue list stays open, and no loading spinner flickers. The updates stop by themselves once the run reaches a result — with no active run on screen, the table issues no requests at all.
This behavior belongs to the table itself, so it works the same way on all five screens the table appears on: the Identity > Providers > Synchronization screen and the view screen of each of the four providers (LDAP, Database, API, OIDC).
A run's state (Queued → Running → result) is reflected immediately, but the number of records processed so far and the current phase are written by the engine only about every fifteen seconds. So even though the table refreshes every five seconds, those counters advance in steps; the same number staying put across a few refreshes is expected behavior, not a stall. Refreshing more often would not make the counters move faster — it would only add read load.
The live refresh has two limits:
- It engages only when a running row is on the currently displayed page. Runs are sorted newest first, so an active run is on page one by definition; while paging back through history the table is not refreshed.
- It does not pick up a scheduled run that starts in the background — the live refresh only follows a run row that is already on screen. This is why each of the four provider view screens also carries a Refresh button next to the Synchronization History heading: refreshing the table once brings the new run in.
On the Identity > Providers > Synchronization screen, the moment the table sees a run finish, the summary counts on the source rows above it are refreshed as well. The last-run summary box on the provider view screens is not part of that refresh; use the Refresh button on those screens to update it.
Single Run per Source
Only one run can be active for a given identity source at a time. While a run is queued or running, a second trigger for the same source is rejected with an "already running" error; synchronization of other sources is unaffected.
The rule holds when Manager runs as a cluster of several nodes: the database decides the winner, so even simultaneous triggers on two nodes synchronize the source only once. In earlier versions this check consulted only the memory of the node that received the request, and could not see a run in progress on another node.