Identity & Sign-InIdentity Hooks (Directory & App Connections)
On this page0

Reference

Included in: All plans. Which Plan Includes What

Identity Hooks (Directory & App Connections)

Identity Hooks allow Zonitix to synchronize with your existing identity providers like Microsoft Entra ID, Okta, or Google Workspace.

What A Hook Is

A “hook” is one saved connection to one outside system. You add and manage hooks under Access Governance → Identity Hooks: connect a directory to bring your people in, connect the apps they use so onboarding and offboarding can create and close accounts there, and connect Microsoft Intune or Jamf Pro to pull device evidence.

Your local directory is the baseline; Okta, Microsoft Entra ID, and Google Workspace connect as identity hooks and are compared for drift.IDENTITY HOOKSLocal directorybaseline · source of truthOktaverifiedEntra IDverifiedGoogle WorkspaceverifiedDrift is measured against the baseline and reconciled with a source of truth (dry-run by default).

Unified User Directory

Instead of manually entering employee accounts in multiple dashboards, Identity Hooks link Zonitix directly to your corporate directory (like Microsoft Entra ID or Okta). When an employee is added or removed in your corporate system, the next scheduled sync picks up the change and updates their status and profile attributes in Zonitix.

Microsoft Entra ID

Brings in users and groups, and each run fetches only what changed since the last one. Group names are imported; role assignment stays under your control in Zonitix.

Okta Directory

Retrieves active user profiles and reflects account status back into Zonitix. Offboarding is started by an administrator, not triggered automatically from Okta.

Google Workspace

Integrates Google Directory Sync to fetch active department profiles and organization structures.

Connecting A Hook — The Five Steps

Every provider goes through the same wizard, so once you have connected one you know them all. You need a permission to manage hooks or integrations; the two permission names are under Technical detail, in Driving Providers Over The API.

01

Connect

Name the hook and enter the connection fields. You can’t continue until every required field is filled.

02

Capabilities

Choose what the hook may do: Read Users, Write Users, Read Groups and Write Groups. Each is offered only for hook types that support it. Then set the Synchronization Interval: every 1, 4, 8, 12 or 24 hours.

03

Mapping

Review the attribute mapping. Custom mappings unlock after a successful validation.

04

Validate

Saves the hook, then runs one live check per capability. A failing optional check just turns that capability off.

05

Done

Run a dry run or a manual sync. Scheduled background sync turns on once the hook is verified.

Credentials are encrypted at rest and never returned to the browser — editing a hook shows you only which credential fields are set, not their values. Saving merges your changes into the stored config, so changing the sync interval can never silently wipe a secret.

Deleting a hook does not delete the people or groups it imported. They stay in your directory and are simply no longer tied to that hook. What is removed is the hook itself and the app-usage data it supplied to SaaS Discovery. The deletion is recorded in the audit log.

Each hook has its own Sync dashboard, opened from the icon on the hook’s row or from its menu. It shows how many records the latest completed run synced, created and modified, a viewer for checking how one person’s directory data maps to Zonitix fields, and the last 10 sync runs with what each one changed.

What Sync Actually Does

ProviderIncremental?Reads groupsAttribute write-back (AlignSync)Group membership write
Entra ID (Azure AD)Yes — Graph delta queries; an expired delta token falls back to a full syncYesYesYes
OktaNo — full paginated fetch each runYesYesYes
Google WorkspaceNo — full paginated fetch each runYesNoYes
AWS IAMNo — full fetch each runYesNoYes
LDAP / OpenLDAPNo — paged search of the whole Base DNYesYesNo

A background scheduler wakes every 5 minutes and syncs any hook that is active, verified, and past its interval — so a 24-hour interval means “about once a day,” not on the hour. Manager relationships are resolved in a second pass, which is what builds your org chart. Sync never deletes a user; accounts disabled upstream are marked inactive.

Creating and disabling accounts from an onboarding or offboarding run is a separate capability with a longer list of providers; see Signed Offer To Onboarding (Zero-Touch).

Per-capability validation runs for Microsoft Entra ID, AWS and LDAP/OpenLDAP. Every other type — Okta, Google Workspace and the SaaS connectors — runs one required Authenticate check. Either way, a hook cannot sync until it has passed and is verified. The first validation a hook ever passes also switches its scheduled sync on, provided it has never synced. A later validation never switches a hook you paused back on.

How A Directory Write Is Protected

Disabling or creating an account is the one action an attacker wants most, so it does not run wherever a workflow happens to execute. Every directory write funnels through a single service that decides, while the workflow engine only asks — the engine can no longer even read the encrypted hook credentials.

  • A dedicated write credential. The endpoint accepts its own directory token, not the shared internal one — a compromised report renderer or any other peer cannot reach it.
  • A fixed action list, never a passthrough. Only create, disable, enable, add to group and remove from group. The two group actions need the hook’s Write Groups capability, and the group is one Zonitix already knows, never a name the caller supplies. There is deliberately no delete and no “run this LDAP command for me.”
  • You pass an email, never a directory path. Zonitix resolves the account itself, inside that hook’s own configured scope, so a caller cannot point the write at Domain Admins or a service account.
  • Disable, never delete. Offboarding suspends the account and releases the seat while keeping it recoverable — a mistake costs an inconvenience, not a user’s group history and mailbox linkage.
  • A burst limit with two-person release. An automated caller that starts disabling accounts faster than a real process ever would — ten in an hour — is parked for a second administrator to approve, not obeyed. That is the control that stops a compromised workflow disabling everyone at 3am.

Releasing A Held Disable

The offboarding step reports “Held for a second administrator” and the account stays enabled. A notice then appears at the top of Access Governance → Identity Hooks, on every plan, saying how many offboarding account disables are held. It is shown only while something is held, and only to people who can open Identity Hooks and are also either an administrator or allowed to approve identity changes. One of them clicks Review, then Approve or Reject. It must be someone other than the person who ran the offboarding.

Re-enabling an account is only automated where it can be undone without guessing (LDAP/OpenLDAP today); for other providers Zonitix tells you to re-enable it in that provider’s own console rather than pretend it did.

If something here is not working as you expect, start with When something goes wrong.

The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.

What To Create On The Provider Side

Microsoft Entra ID / Azure AD

  1. Azure Portal → Microsoft Entra ID → App registrations → New registration.
  2. From Overview, copy the Application (client) ID and Directory (tenant) ID.
  3. Certificates & secrets → New client secret → copy the Value immediately (it is shown once, and it is not the Secret ID).
  4. API permissions → Microsoft Graph → Application permissions (not Delegated — there is no signed-in user in this flow).
  5. Add Directory.Read.All. User.Read.All alone will pass the user check but fail the group check.
  6. Click Grant admin consent. This is the step people miss — without it the credentials authenticate fine and then every directory call returns 403.

Okta

  1. Okta Admin Console → Security → API → Tokens.
  2. Create Token, name it, and copy the value immediately — it is shown only once.
  3. The token inherits the permissions of the admin who created it, so create it as a Read-Only Admin (or higher) to read users and groups.

Google Workspace

  1. Google Cloud Console → APIs & Services → Library → enable the Admin SDK API.
  2. IAM & Admin → Service Accounts → create one, then Keys → Add key → JSON and download it.
  3. Copy the service account’s numeric Client ID (not its email address).
  4. admin.google.com → Security → Access and data control → API controls → Manage Domain Wide Delegation → Add new, paste the Client ID.
  5. Authorize these scopes — the second only if you want group sync:
    .../auth/admin.directory.user.readonly
    .../auth/admin.directory.group.readonly

Active Directory / LDAP

  1. Open outbound access from your instance to the domain controller: 389 (LDAP), 636 (LDAPS, recommended), or 3268/3269 for the Global Catalog.
  2. Get the DC’s FQDN with Get-ADDomainController. Use the FQDN, not a bare IP, so LDAPS certificate validation works.
  3. Get the Base DN with Get-ADDomain (the DistinguishedName field), or derive it: corp.example.com → dc=corp,dc=example,dc=com.
  4. Create a dedicated read-only service account to bind with — not a person’s login.

What To Paste Into Zonitix

ProviderFieldsExample
Entra ID (Azure AD)tenantId · clientId · clientSecret00000000-0000-0000-0000-000000000000
Oktadomain · apiTokenhttps://acme.okta.com (no /admin)
Google Workspacedomain · adminEmail · serviceAccountJsonexample.com · admin@example.com · { "type": "service_account", … }
LDAP / OpenLDAPurl · baseDN · bindDN · bindPasswordldaps://dc01.corp.example.com:636 · dc=corp,dc=example,dc=com

adminEmail on Google Workspace is the super-admin the service account impersonates through domain-wide delegation — it is not a login for Zonitix.

Held Disables: Permissions, Reply And Routes

The notice about held disables is shown to people who can open Identity Hooks (manage_hooks or manage_integrations) and are also either an administrator or hold approve_identity_changes.

Once the same actor has 10 disables in the last 60 minutes, the next disable is not performed: the endpoint answers 202 with { parked: true, requestId, message }, stores a pending change request with source WORKFLOW, and audits DIRECTORY_DISABLE_PARKED. The request is listed and decided through GET /user-management/align/change-requests, POST /user-management/align/change-requests/:id/approve and POST /user-management/align/change-requests/:id/reject, all of which need approve_identity_changes. Without the AlignSync entitlement those three routes still answer, for held disables only. An approval by the requester is refused: “You cannot approve your own change request — a different person must approve it.”

SaaS App Connectors

Beyond your directory, Zonitix connects to the SaaS apps your people actually use, so a joiner or leaver can be actioned in the same run. Every connector below calls the vendor’s real API. There is no placeholder data anywhere in this path — a connector without a working implementation fails loudly and records a failed sync rather than importing invented users into your directory.

ConnectorWhat it readsCredentialCreates / removes accounts in runs
SlackWorkspace membersBot OAuth tokenYes
GitHubOrganisation membersPAT (read:org)Yes
JiraUsers in the siteAtlassian API tokenYes
ZoomUsers + licence typeServer-to-Server OAuthYes
SalesforceActive users (SOQL)Connected app (OAuth2)Yes
ZendeskAll users, paginated in full — note this includes end-users (ticket requesters), not only agentsAPI tokenYes
ServiceNowsys_user recordsInstance accountYes
NotionMembers (bots filtered out)Internal integration tokenNo — read only
DropboxTeam membersTeam access tokenYes
BambooHREmployee directoryAPI keyYes
1PasswordSCIM usersSCIM bridge tokenYes
Generic SCIM 2.0Users from any app that speaks SCIM 2.0 (Adobe, Datadog, Asana, Monday…), paginated — and provisions/deactivates accounts on the same endpoint. One hook per app.SCIM base URL + bearer tokenYes

The last column says whether an onboarding or offboarding run can create and remove the person’s account in that app through the hook. Together with the directories, that makes the full list of systems a run can drive: Microsoft Entra ID, Okta, Google Workspace, Active Directory / OpenLDAP, AWS IAM, Slack, GitHub, Zoom, Jira, Salesforce, BambooHR, 1Password, Zendesk, ServiceNow, Dropbox and Generic SCIM 2.0. Confluence, Figma and GCP hooks cannot create or remove accounts. What “remove” means differs by provider — some disable the account, some delete it — see Signed Offer To Onboarding (Zero-Touch).

Two you should know the shape of before relying on them. Confluence authenticates and confirms the token works, but the endpoint it reads returns only the token owner’s own profile — it is a credential check, not a directory import. Figma cannot list organisation members through the public REST API at all, so its sync refuses with an explicit message telling you it needs the Enterprise SCIM API rather than quietly returning nothing.

None of the SaaS connectors read groups today — they import people, not group structure. Group sync is available for Microsoft Entra ID, Okta, Google Workspace, AWS IAM and LDAP / OpenLDAP.

When It Doesn’t Connect

SymptomWhat it means & the fix
AADSTS7000215 / invalid_clientAzure client secret is wrong or expired. Check the Expires column under Certificates & secrets, create a new secret, and paste the Value (not the Secret ID).
Auth passes, reading users 403sCredentials are fine; the app registration was never granted the Graph permission, or admin consent was never clicked. Grant Directory.Read.All and click Grant admin consent.
Users sync, groups failYou granted User.Read.All instead of Directory.Read.All.
LDAP result code 49The server was reachable — this is the bind account. Check the Bind DN is a full DN (or a UPN for AD) and that the account isn’t locked or expired.
LDAP result code 32 / 5032: the Base DN doesn’t exist in this directory. 50: the bind worked but the account can’t read that subtree — an ACL problem, not a password problem.
ECONNREFUSED / ETIMEDOUT / ENOTFOUNDNetwork, not credentials. Check host and port (389 vs 636), firewall and security-group rules, and certificate trust if you used ldaps://.
Google token exchange failsDomain-wide delegation isn’t authorized for the scopes you requested. Re-check the numeric Client ID and scope list under Admin Console → Security → API controls → Domain-wide delegation.

Client Credentials Flow

Zonitix authenticates to Microsoft Entra ID (formerly Azure AD) using the OAuth2 server-to-server client credentials flow. Other providers use their own mechanisms — Okta uses a static API token, and Google Workspace uses a signed JWT with domain-wide delegation.

// Fetching OAuth token from Azure Active Directory Endpoint:

POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token

Body: client_id=CLIENT_ID & client_secret=SECRET & scope=https://graph.microsoft.com/.default & grant_type=client_credentials

// A fresh token is requested for each directory call and held in memory only for that request. Nothing is persisted to disk.

Driving Providers Over The API

The wizard above is a thin front-end over one REST resource. An integrator or CI job can create, verify, sync, and pull compliance evidence from a provider entirely over HTTP, and a compliance inspector can pull per-provider evidence the same way. Everything on this resource is proxied by the API gateway to the integration service — the gateway strips the /user-management segment and forwards to /providers on the service — so every path below is rooted at:

https://<your-instance>-api.zonitix.com/api/v1/user-management/providers

Every call is authenticated at the gateway and then gated by RBAC in the service. There are two tiers: a write/CONNECT tier that requires manage_hooks or manage_integrations, and a read/VIEW tier satisfied by either of those or by manage_alignment_policy, execute_identity_writeback, or approve_identity_changes. manage_hooks is kept as a valid superset in every list for backward compatibility.

MethodPathPurposeAuth
GET/providersList all providers. Credential ciphertext is stripped; each record instead carries configuredCredentials (the names of the secret fields that are set).VIEW
POST/providersCreate a provider. Only allowlisted fields persist; isVerified cannot be set by the caller. Returns 201.CONNECT
PUT/providers/:idUpdate a provider. config is MERGED over what is stored, never replaced. 404 if the id is unknown.CONNECT
DELETE/providers/:idHard-delete the provider row. The people and groups it imported are kept and released from the hook (sourceProviderId cleared), its SaaS Discovery usage rows are deleted, and the deletion is audited as PROVIDER_DELETED with the three counts. One transaction. Returns 204; 404 if unknown.CONNECT
POST/providers/:id/testMake a REAL auth call to the provider. On success sets isVerified=true and returns { message, availableAttributes }.CONNECT
POST/providers/:id/validateRun one live check per capability; sets isVerified from the required checks and switches OFF any capability whose check failed.CONNECT
POST/providers/:id/syncImport users/groups. Refused with 400 unless isVerified is true. Returns 200 { message } plus the flags skipped, unsupported, inFlight or partial when they apply.CONNECT
GET/providers/:id/logsThe 10 most recent ProviderSyncLog rows for this provider, newest first.VIEW
GET/providers/:id/evidence/:typePull compliance evidence (no-MFA users, admin-role holders, stale access keys, device compliance and encryption) with a live provider call.VIEW or manage_compliance

The Writable-Field Allowlist

Create and update both mass-assign only from a fixed allowlist. A key that is not on this list is silently dropped from the request body — it is not an error, it just does not persist. The list is exactly:

name · description · type · config · isActive · syncInterval

canReadUsers · canReadGroups · canWriteUsers · canWriteGroups · canReadAllResources

isVerified Is Not Writable

The trust flag isVerified is deliberately absent from the allowlist. A CONNECT operator cannot PUT it true. It is set server-side only: it starts false on create, is forced back to false whenever a connection field changes, and becomes true only after a passing /test or a validation whose required checks all pass. To verify this control: create a provider with "isVerified": true in the body and confirm the returned record still reads false.

Field defaults on the model, for anything you omit at create time: isActive = true, syncInterval = 24 (hours), every canRead*/canWrite* flag = false, config = {}. The capability flags being off by default is why a freshly created hook syncs nothing until you either check the boxes in the wizard or send the flags true — and until a validation confirms them.

One exception, for older LDAP hooks. A hook also carries capabilitiesSetAt, the time its capabilities were first decided. It is set when a create request carries any capability flag, or when an update changes a flag to a different value; re-sending the values already stored does not count. An LDAP or OPENLDAP hook where it is still empty — for example one created over the API with no capability flags at all — is treated as allowed to read users and groups, so an import that was already running is not stopped. Every other type enforces the stored flags strictly. For writes: AlignSync write-back always requires canWriteUsers to be true, while account create and disable from a workflow run is allowed on a hook whose capabilities were never decided.

Per-Type config Schemas

config is a free-form JSON object; which keys are required is decided by type at test/sync time (the connection test throws a named error if one is missing). These are the types the connection test actually implements — any other type is rejected at test time with Test for {type} not fully implemented.

typeRequired config keysNotes
AZURE_ADtenantId · clientId · clientSecretClient-credentials OAuth; a sample user read discovers attributes.
OKTAdomain · apiTokendomain is the org URL, no /admin. SSWS token.
AWSaccessKeyId + secretAccessKey or roleArnOptional region, and an optional externalId that is sent as the External ID when the role is assumed. Proven with sts:GetCallerIdentity; roleArn takes the AssumeRole path.
GOOGLE_WORKSPACEdomain · adminEmail · serviceAccountJsonDomain-wide delegation; adminEmail is the impersonated super-admin.
LDAP / OPENLDAPurl · bindDN · bindPasswordbaseDN optional; if present the test also confirms it is readable.
ZOOMaccountId · clientId · clientSecretServer-to-Server OAuth.
CONFLUENCEdomain · adminEmail · apiTokenTest authenticates only; the read returns just the token owner (credential check, not a directory import).
GCPserviceAccountJson · gcpProjectIdJWT-bearer, no delegation; proven with projects.get.
SLACK · NOTION · DROPBOX · FIGMAapiTokenSingle-token SaaS connectors. Figma cannot list org members over the public API and refuses sync explicitly.
JIRA · ZENDESK · SERVICENOWdomain · adminEmail · apiTokenBasic-auth SaaS. Zendesk domain is the bare subdomain.
GITHUB · BAMBOOHRdomain · apiTokenGitHub domain is the org login; BambooHR the company subdomain.
SALESFORCEclientId · clientSecretConnected-app client-credentials OAuth.
ONE_PASSWORDdomain · apiTokendomain is the SCIM bridge base URL; test hits the same /Users the sync uses.
SCIMdomain · apiTokenGeneric SCIM 2.0. domain is the app’s SCIM base URL; apiToken is its bearer token.
INTUNEtenantId · clientId · clientSecretAn app registration in Microsoft Entra ID. A device management hook: see below.
JAMFbaseUrl · clientId · clientSecretA Jamf Pro API client. baseUrl is your Jamf Pro address. A device management hook: see below.

Device Management Hooks (Microsoft Intune And Jamf Pro)

In plain words: a Microsoft Intune or Jamf Pro hook does not bring people or groups into your directory. It reads how your managed devices are doing — whether they are compliant and whether their disks are encrypted — and feeds that to Compliance as evidence. You connect it the same way as any other hook, from the Device Management category of the hook catalogue.

Technically: POST /:id/sync on these two types runs a device-evidence refresh instead of a directory import (Intune: device-compliance; Jamf Pro: disk-encryption) and the summary reads “device evidence refreshed” with the number of devices scanned and flagged. The background scheduler runs the same refresh on the hook’s sync interval. A dry run contacts nothing and returns a preview line. The same data can be pulled on demand from GET /:id/evidence/:type.

The hook catalogue also shows tiles for a number of individual apps that are provisioned over SCIM. Each of those tiles creates an ordinary SCIM hook, so the SCIM row above applies to them.

Secrets, And Why Update Merges

Any config key whose name looks like a credential is encrypted at rest and stripped from every response. Concretely that is the seven named fields — clientSecret, apiToken, bindPassword, secretAccessKey, serviceAccountJson, accessKeyId, externalId (the AWS role External ID) — plus anything matching secret / token / password / passwd / api_key / credential / private_key / access_key / service_account. A response never returns the value, its length, or a prefix; it returns configuredCredentials, an array of the key names that are set. An inspector reads that array to confirm which secrets exist without ever seeing one.

The Merge Rule On Update

Because the client is never handed a secret, it physically cannot send one back. So PUT /:id merges the incoming config over the stored one instead of replacing it. A sensitive key you omit is preserved — renaming a hook or changing its sync interval can never wipe its clientSecret. A key you do send wins, so rotating a secret works. And sending an empty string is the deliberate way to clear one. Changing any connection field (the credentials, or region / clientId / tenantId / domain / url / baseUrl / bindDN / baseDN / adminEmail / googleAdminEmail / gcpProjectId / roleArn / loginUrl / accountId / azureSubscriptionId) resets isVerified to false, so a repointed hook must re-prove itself before it can sync again.

Worked Example — Create, Test, Then Rotate

// 1) Create an Azure AD hook. isVerified in the body is ignored (allowlist).

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/user-management/providers \

-H 'Authorization: Bearer <session-jwt>' -H 'Content-Type: application/json' \

-d '{

"name": "Corp Entra", "type": "AZURE_AD",

"canReadUsers": true, "canReadGroups": true, "syncInterval": 12,

"config": { "tenantId": "...", "clientId": "...", "clientSecret": "..." }

}'

// → 201; response.config has NO clientSecret, and configuredCredentials: ["clientSecret"]; isVerified: false


// 2) Prove the credentials with a real Graph call. This is what sets isVerified.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/user-management/providers/42/test \

-H 'Authorization: Bearer <session-jwt>'

// → 200 { "message": "Successfully authenticated with Azure AD!",

// "availableAttributes": ["id","displayName","mail", ...] } ← the live user schema


// 3) Rotate ONLY the secret. Omitting tenantId/clientId keeps them (merge).

curl -X PUT https://<your-instance>-api.zonitix.com/api/v1/user-management/providers/42 \

-H 'Authorization: Bearer <session-jwt>' -H 'Content-Type: application/json' \

-d '{ "config": { "clientSecret": "<new-secret>" } }'

// clientSecret is a connection field → isVerified flips back to false; re-run /test.

// To CLEAR it instead, send "clientSecret": "" (empty string).

Test vs. Validate — Two Different Verifications

POST /:id/test — Binary Auth

Makes one real auth call to the provider (OAuth token, LDAP bind, GetCallerIdentity, a JWT exchange — per type). On success it sets isVerified=true, stashes the discovered availableAttributes into config, and returns { message, availableAttributes }. A failure throws, is audited as PROVIDER_TEST_FAILED, and leaves the flag untouched.

POST /:id/validate — Per-Capability

Runs one check per capability and enforces the outcome: it sets isVerified = canFinish (only required checks gate finishing) and switches off any capability whose optional check failed — never on. Returns the full checks[], passed/total, canFinish, failedRequired, and disabledCapabilities.

Bespoke capability suites exist only for AZURE_AD, AWS, and LDAP/OPENLDAP. For every other type — Okta, Google Workspace, and all the SaaS connectors — /validate falls back to a single required Authenticate check that just runs the connection test, so it still flows through the same verify-then-sync path. If a type has no connection test at all, /validate returns supported: false and persists nothing.

POST /:id/sync — Importing Users And Groups

Body fields, all optional: syncType ("delta" default over the API, or "full"), isManual (defaults to true when called via the API), and dryRun (defaults to false). The guard an inspector checks: sync throws 400 Provider must be verified before syncing when isVerified is false — there is no path to import directory data on an unverified hook. A scheduled (non-manual) run on an inactive hook is skipped rather than erroring.

// Simulate a full sync without writing anything — the safe way to preview:

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/user-management/providers/42/sync \

-H 'Authorization: Bearer <session-jwt>' -H 'Content-Type: application/json' \

-d '{ "syncType": "full", "dryRun": true }'

// → 200 { "message": "[DRY RUN] ..." }; lastSync is NOT advanced, audited as SYNC_..._DRY_RUN

Sync is really implemented for AZURE_AD, GOOGLE_WORKSPACE, AWS, OKTA, LDAP/OPENLDAP, ZOOM, CONFLUENCE, the extended SaaS set (Slack, Jira, GitHub, Salesforce, Zendesk, ServiceNow, Notion, Dropbox, BambooHR, 1Password) and SCIM. GCP and FIGMA have no directory sync: a manual sync answers 200 with a message that starts “Sync not available:” and unsupported: true, writes a SYNC_<TYPE>_UNSUPPORTED audit row, and does not move lastSync; the scheduler never dispatches them. INTUNE and JAMF refresh device evidence instead of importing people (see Device management hooks). Check the returned message, and GET /:id/logs, to confirm a run did real work.

The response is always { message }, with up to four extra flags that are present only when true: skipped (nothing ran), unsupported (the type has no directory sync), inFlight (a sync of this hook is already running) and partial (the connector stopped before it had read the whole directory, so only part was imported). A client that reads only message will mistake these for a normal run.

GET /:id/evidence/:type — Compliance Evidence

An inspector can pull point-in-time evidence straight from the provider. Each call makes a live API request and returns a JSON envelope: { title, timestamp, count, data } (the MFA scans also carry scanned). Evidence is implemented for five provider types — OKTA, AZURE_AD, AWS, INTUNE and JAMF; any other type returns 400 Evidence fetching not supported for provider type.

:typeProvider(s)What it returns
mfa-statusOkta · Entra ID · AWSActive users with no MFA method (Okta: no active factor; Azure: isMfaRegistered=false, needs Reports/AuditLog read; AWS: no MFA device).
admin-rolesOkta · Entra ID · AWSPrivileged-role holders (Okta Super Administrators; Azure Global Administrators; AWS users with the AdministratorAccess policy).
stale-access-keysAWS onlyIAM access keys older than 90 days, with key id, created date, and status.
device-compliance · compliance · encryptionMicrosoft IntuneAll three names return the same result: managed devices that are non-compliant or unencrypted.
disk-encryption · encryption · compliance · patchingJamf ProAll four names return the same result: computers without FileVault encryption.

// Pull the no-MFA evidence for an Okta hook (provider id 17):

curl https://<your-instance>-api.zonitix.com/api/v1/user-management/providers/17/evidence/mfa-status \

-H 'Authorization: Bearer <session-jwt>'

// → 200 { "title": "Okta Active Users without MFA Enrolled",

// "timestamp": "...", "scanned": 812, "count": 3, "data": [ ... ] }

Honest Limit — Where The isVerified Gate Actually Sits

The Okta evidence path enforces the verification gate itself: it refuses with “This provider connection has not been verified — evidence collection is blocked until it is re-verified” when isVerified is false, and it SSRF-guards the provider-supplied host before calling it — important because evidence is reachable by several read-only roles. The Entra ID, Intune and Jamf Pro evidence paths refuse an unverified hook in the same way. AWS is the one evidence function that does not re-check isVerified inside the evidence call. In practice a provider only holds usable credentials after a /test or /validate has set the flag, so treat the AWS gate as implied-by-workflow, not enforced-in-code. Reported so it is not mistaken for a uniform control.

Reading Is Not Writing

The VIEW tier can list providers, read sync logs, and pull evidence (evidence is also open to manage_compliance), but cannot create, edit, test, validate, sync, or delete — those need the CONNECT tier. And no read ever exposes a credential value: responses carry configuredCredentials (names only), and the stored ciphertext stays server-side. An inspector can therefore verify the whole control surface without holding, or being able to exfiltrate, a single secret.

Related reference