Ana içeriğe geç

Identity Sync Runs Management API

Overview

Every identity source keeps its own history of synchronization runs — the passes that pull users (and, on LDAP and OIDC, groups/organizations) from the source into Apinizer credentials. This page documents the Management API surface for that history, addressed under the source each run belongs to: /identity-sources/{sourceType}/{sourceName}/sync-runs. sourceType is one of ldap, database, api, oidc; the name is resolved within the project and within that type only, so a source named the same in two different types is two different resources.

This is the portable counterpart of the Manager's own per-type synchronization endpoints. For the screen and concepts this surface backs — the Sync Profile, run scopes, the run history table, and the deactivation-threshold approval workflow — see Credential Synchronization.

Every address on this page is under /apiops/projects/{projectName} and follows the shared Common Contract: the stable error envelope, correlation IDs, bounded pagination, and Idempotency-Key on the run-creating call. A caller outside the project's scope always gets 404 resourceNotFound, never 403.

Endpoints

MethodPathDescription
POST/identity-sources/{sourceType}/{sourceName}/sync-runsQueue a synchronization run for the source (202 + Location)
GET/identity-sources/{sourceType}/{sourceName}/sync-runsList the source's runs, newest first
GET/identity-sources/{sourceType}/{sourceName}/sync-runs/{runId}One run's progress and outcome
GET/identity-sources/{sourceType}/{sourceName}/sync-runs/{runId}/issuesThe per-subject issues the run recorded
POST/identity-sources/{sourceType}/{sourceName}/sync-runs/{runId}/actions/approve-reconcileApprove a run's staged reconciliation and queue the applying run (202 + Location)
POST/identity-sources/{sourceType}/{sourceName}/sync-runs/{runId}/actions/cancelCancel a QUEUED or RUNNING run (200 + the run)

Both list endpoints accept only page and size — neither exposes a sort parameter. Runs are always listed newest-queued-first; issues are always listed in the order they were recorded within the run.

Source types and run scopes

sourceType identifies which of the four identity source kinds the address resolves against:

sourceTypeIdentity source
ldapLDAP / Active Directory
databaseDatabase identity provider
apiAPI identity provider
oidcOIDC provider

A path segment other than one of these four, or a sourceName that resolves to no source of that type in the project, is 404 resourceNotFound.

The optional mode on the queue request selects the run's scope. Which scopes a source type accepts depends on whether it can fetch users and groups as separate passes:

sourceTypeAccepted mode values
ldap, oidcFULL (default), USERS_ONLY, GROUPS_ONLY
database, apiFULL only

RECONCILE_APPLY cannot be requested on the queue endpoint under any source type — that scope is produced only by approve-reconcile, never chosen directly. A mode the source type does not accept, or a value that is not one of the four scopes, is 400 malformedRequest (params.reason = runModeNotSupported); the request is never silently downgraded to FULL.

Queue a run

POST .../sync-runs claims the source and writes a QUEUED run row before triggering the synchronization job, so a trigger that fails still leaves a visible FAILED row instead of nothing at all. Three guards run before the job is triggered:

  • The source's synchronization must be enabled, else 409 lifecycleConflict (params.reason = syncDisabled).
  • An OIDC source whose vendor has no Keycloak Admin REST API is refused with 409 lifecycleConflict (params.reason = syncRequiresKeycloakVendor) rather than landing a FAILED run.
  • Only one run may be active for a source at a time; queuing a second run while one is queued or running is 409 lifecycleConflict (params.reason = syncAlreadyRunning).

Approve a run's pending reconcile

When a run's reconciliation step would deactivate or delete more records than the source's deactivation threshold allows, the run finishes in the AWAITING_RECONCILE_APPROVAL state without touching a single record, and stages an approval. POST .../sync-runs/{runId}/actions/approve-reconcile re-verifies that approval against the same candidate list, filter, and deactivation mode it was staged with, then queues a new RECONCILE_APPLY run that applies exactly what was approved — it fetches nothing from the source again. See Deactivation Threshold and Reconcile Approval for the full workflow.

Approval is refused with:

  • 409 lifecycleConflict (params.reason = reconcileApprovalConflict) — the run is no longer awaiting approval, or a newer run on the same source has replaced the pending approval.
  • 400 malformedRequest (params.reason = reconcileApprovalNotFound) — the source has no pending approval.
  • 400 malformedRequest (params.reason = reconcileApprovalExpired) — the pending approval lapsed (approvals are valid for 24 hours).

Cancel a queued or running run

POST .../sync-runs/{runId}/actions/cancel asks a QUEUED or RUNNING run to stop at the next page boundary: fetching from the source is cut off, and nothing fetched so far is written — pages read from the source are held in memory for the duration of the fetch and only applied once the fetch completes, so a cancellation during the fetch discards them. (This differs from a bounded-but-incomplete fetch that finishes on its own, such as a directory server that does not support paged results — that run still applies the rows it did manage to read; only its reconciliation step is skipped. See the WARNING result below.) The reconciliation step (deactivating or deleting records) is skipped entirely for a cancelled run too — a partial fetch must never be allowed to deactivate anyone. The run's own status becomes CANCELLED; the source's last-synchronization status becomes WARNING.

The call returns 200 immediately, before the run has necessarily stopped — it only records that a stop was requested. The run body comes back with cancelRequested set to true:

{
"runId": "68b1f2a4c9e1a2b3c4d5e6f7",
"sourceName": "corp-ad",
"sourceType": "LDAP",
"status": "RUNNING",
"runMode": "FULL",
"cancelRequested": true,
"startedAt": "2026-09-03T10:15:00Z"
}

A run that is not QUEUED or RUNNING — one that has already reached a final state, in either direction — cannot be cancelled: 409 lifecycleConflict (params.reason = runNotActive).

Idempotency

POST .../sync-runs requires an Idempotency-Key header (see Common Contract → Idempotency); a missing or blank header is 400 idempotencyKeyRequired. Reads and approve-reconcile need no key — approval is already single-use by construction. The queue key is claimed per project and per source's queue operation, together with a canonical hash of the request body:

SituationResult
New keyThe run is queued
Same key, same body, first call already finished409 idempotencyReplayExpiredparams.resource names the run id the first call created; read it with GET .../{runId} instead
Same key, same body, first call still in flight409 idempotencyInProgress
Same key, a different body409 idempotencyKeyReused

A claim whose underlying call failed is released, so the same key may be retried once the cause is fixed.

Request Body

POST .../sync-runs takes one optional field:

FieldTypeMeaning
modestringThe run's scope — see Source types and run scopes above. Absent means the source's full run.

Approve-reconcile takes no body — everything it needs is already staged in the pending approval.

Response

Accepted — 202

The receipt returned by POST .../sync-runs and .../actions/approve-reconcile, with Location pointing at the queued run:

{
"runId": "68b1f2a4c9e1a2b3c4d5e6f7",
"sourceName": "corp-ad",
"sourceType": "LDAP",
"status": "QUEUED",
"runMode": "FULL",
"queuedAt": "2026-09-03T10:15:00Z"
}

Run — 200

The shape returned by GET .../{runId} and each row of GET .../sync-runs:

FieldTypeMeaning
runIdstringThe run's id
sourceNamestringThe source's name, as it was at the time of the run (a snapshot — later renaming or deleting the source does not change it)
sourceTypestringLDAP | DATABASE | API | OIDC
statusenumQUEUED | RUNNING | SUCCESS | WARNING | FAILED | AWAITING_RECONCILE_APPROVAL | INTERRUPTED | CANCELLED
runModeenumThe run's scope: FULL | USERS_ONLY | GROUPS_ONLY | RECONCILE_APPLY. Runs recorded before this field existed read as FULL.
approvalPendingbooleantrue exactly when status is AWAITING_RECONCILE_APPROVAL
triggerstringMANUAL for a run queued through this API or the Manager screen, SCHEDULED for one the source's own cron started
triggeredBystringThe user who queued the run; absent on a scheduled run
startedAttimestampWhen the run was queued — the row's birth time
runningAttimestampWhen a node picked the run up; absent while still QUEUED
heartbeatAttimestampThe executing node's last liveness signal
finishedAttimestampWhen the run reached its result
durationMsintegerActual execution time once finished; time spent queued is not counted
phaseenumFETCH | RECONCILE; absent while QUEUED
processedCountintegerSubjects processed so far
issueCountintegerTotal issue rows this run recorded (see Issue below) — the true total, even past the persisted cap
issuesTruncatedbooleantrue once the persisted-issue cap was hit; issueCount still shows the true total
upserted / deactivated / skipped / filtered / errorsintegerPer-outcome counters. filtered counts subjects the source's scope filter excluded, kept apart from skipped (a conflict a caller has to resolve)
unchangedintegerSubjects whose synchronized fields matched the source exactly on this run — the record was not rewritten and not redeployed to gateways, only its "last synchronized" stamp was refreshed. A healthy repeat run normally shows upserted: 0 and a large unchanged; the first run after an upgrade rewrites every record once before this settles in
failureReasonenumSet once the run has a failure to report: UNREACHABLE | TIMEOUT | AUTH_REJECTED | PERMISSION_MISSING | HTTP_ERROR | CONFIG | INTERRUPTED | CANCELLED | UNKNOWN. Absent on a clean SUCCESS
failureHttpStatusintegerThe HTTP status code the source returned, when failureReason is HTTP_ERROR, AUTH_REJECTED, or PERMISSION_MISSING; absent otherwise
failureScopeenumWhich read the failure happened on, when known: USERS | GROUPS | ROLES; absent when the failure was not tied to one read
cancelRequestedbooleantrue once a cancel action has been accepted for this run
messagestringLegacy raw-English fallback reason, kept for runs recorded before translatable reasons existed
messagesarrayUp to 10 translatable reasons for this run's outcome, each { "key": string, "params": object }

WARNING means the run finished without error but its reconciliation step was deliberately skipped as a safety measure (the source returned no subjects, or its subject list could only be read in part) — nothing was deactivated or deleted; a partial read still applies the rows it did manage to read. CANCELLED means a cancel action was accepted and the run stopped before completing its fetch — unlike a partial read, nothing fetched during a cancelled run is written, and reconciliation never ran.

Issue — 200, paged

One row of GET .../{runId}/issues:

FieldTypeMeaning
seqintegerSequence within the run, starting at 1
severityenumERROR (the subject was left out of synchronization) | WARNING (the subject was processed but the row still deserves attention)
messageobjectThe translatable reason: { "key": string, "params": object }
subjectstringThe username or external identity the row is about; absent when the reason cannot be tied to one subject
occurredAttimestampWhen this row was recorded

At most 1,000 issue rows are kept per run; issuesTruncated on the run itself tells you when the true count went further.

List envelope — 200

GET .../sync-runs and GET .../{runId}/issues both return the shared paged envelope:

{
"status": "SUCCESS",
"items": [],
"page": 0,
"size": 20,
"totalElements": 42,
"totalPages": 3,
"sort": ["startedAt,desc"]
}

Errors

Every error follows the Common Contract's error envelope — branch on errorKey, never on message.

Error keyWhen
malformedRequestThe requested mode is not one of the four scopes, or is not one the source type accepts (params.reason = runModeNotSupported); the source has no pending reconcile approval (params.reason = reconcileApprovalNotFound); or the pending approval has expired (params.reason = reconcileApprovalExpired)
resourceNotFoundThe sourceType path segment is not ldap, database, api, or oidc; the sourceName resolves to no source of that type in the project; or an unknown run id, or a run id that belongs to a different source, was addressed
lifecycleConflictparams.reason names the cause: syncDisabled (the source's synchronization is not enabled), syncRequiresKeycloakVendor (an OIDC source without a Keycloak Admin REST API), syncAlreadyRunning (a second run was queued while one was already active), reconcileApprovalConflict (the run is not awaiting approval, or a newer run replaced the pending approval), runNotActive (a cancel was requested on a run that is not QUEUED or RUNNING), or unknownSourceType (defensive — the run's own recorded source type could not be resolved to a synchronization job)
idempotencyKeyRequiredPOST .../sync-runs was called without its Idempotency-Key header
idempotencyKeyReusedThe same key was sent with a different request body
idempotencyReplayExpiredThe same key and body already completed — params.resource names the run it created
idempotencyInProgressThe same key and body's first call is still running
permissionDeniedAuthenticated, but the caller lacks the Identity permission at the level this operation requires

See Common Contract → Error keys for the complete closed set and the status-code table.

Permissions & Licensing

  • Reads (GET) require the Identity permission at View; queuing a run and approving a reconcile (POST) require it at Manage.
  • The admin (platform) project addresses admin-owned identity sources. A caller whose account is not a system administrator gets 404 resourceNotFound when addressing the admin project — the same answer a caller outside its project scope gets, never 403.
  • A project outside the caller's authorized scope is always 404 resourceNotFound, never 403 — see Common Contract → Telling 402, 403 and 404 apart.

Example

Queue a full run on an LDAP source with an idempotency key, poll it, then read its issues:

# 1. Queue — requires Idempotency-Key
curl -X POST "https://apinizer.example.com/apiops/projects/finance/identity-sources/ldap/corp-ad/sync-runs" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9d0c8f4a-2f1e-4c7b-9c2f-8b3a1d5e7c40" \
-d '{ "mode": "USERS_ONLY" }'
# 202 Accepted, Location: /apiops/projects/finance/identity-sources/ldap/corp-ad/sync-runs/68b1f2a4c9e1a2b3c4d5e6f7

# 2. Poll the run
curl "https://apinizer.example.com/apiops/projects/finance/identity-sources/ldap/corp-ad/sync-runs/68b1f2a4c9e1a2b3c4d5e6f7" \
-H "Authorization: Bearer $TOKEN"

# 3. Once finished, read what was skipped or erred
curl "https://apinizer.example.com/apiops/projects/finance/identity-sources/ldap/corp-ad/sync-runs/68b1f2a4c9e1a2b3c4d5e6f7/issues?page=0&size=50" \
-H "Authorization: Bearer $TOKEN"

# 4. If a run comes back AWAITING_RECONCILE_APPROVAL, approve it
curl -X POST "https://apinizer.example.com/apiops/projects/finance/identity-sources/ldap/corp-ad/sync-runs/68b1f2a4c9e1a2b3c4d5e6f7/actions/approve-reconcile" \
-H "Authorization: Bearer $TOKEN"
# 202 Accepted, Location points at the new RECONCILE_APPLY run
  • Credential Synchronization - The manager UI screen and concepts this surface backs: Sync Profile, run scopes, run history, and the deactivation-threshold approval workflow
  • Common Contract - The shared error envelope, idempotency, pagination, and status-code rules every endpoint on this page follows
  • Error Handling - The envelope used by the manager's older endpoints