ZevAuth
Sign up

Delegated access to Zev products

Delegated access webhooks

Events we send when a user's connection to your app changes, what each payload carries, and how to apply them safely.

When something changes on a connection, ZevID tells us and we tell you. The events arrive at the normal webhook endpoints of the environment the connection belongs to: live for real users, test for your team’s test connections. data.tenantMode says which.

They are signed, retried and delivered exactly like every other ZevAuth webhook. Webhooks covers verifying the signature, retries and responding. Subscribe to the events below in the console under Webhooks, or leave the endpoint on “all events”.

Events

EventWhen
delegated_access.connection.createdA user connected your app to a Zev product, or reconnected it.
delegated_access.connection.updatedA connection changed: targets, permissions, a suspension, a pause, or a high-risk permission confirmed or ended. Read changes.
delegated_access.connection.revokedA connection was removed, by the user, by you, by the product or by ZevID. Stop using its tokens.
delegated_access.app.updatedYour app was suspended or restored on a product, or a permission was paused or unpaused for your whole app.
delegated_access.security.refresh_reuseA refresh token of one of your connections was used twice. Its refresh tokens are revoked and the user must reconnect.

Connection events

created, updated and revoked carry the connection as it is now:

{
  "type": "delegated_access.connection.updated",
  "createdAt": "2026-11-09T11:20:13.004Z",
  "environment": "env_live_...",
  "data": {
    "eventId": "8a0c2f5e-1b7d-4c3a-9f21-6d4e5f6a7b80",
    "product": "zevcloud",
    "tenantMode": "live",
    "changes": ["high_risk_expired"],
    "reason": "high_risk_expired",
    "by": "zevid",
    "occurredAt": "2026-11-09T11:20:12.022Z",
    "grantId": "6d1d2c3b-4a5f-4e6d-8c7b-9a0b1c2d3e4f",
    "accountId": "3b9f8e7d-6c5b-4a39-8271-605f4e3d2c1b",
    "gv": 5,
    "status": "active",
    "scopes": ["zevcloud.deploy", "zevcloud.services.delete"],
    "effectiveScopes": ["zevcloud.deploy"],
    "highRisk": [
      { "scope": "zevcloud.services.delete", "confirmedAt": "2026-10-10T11:20:12.000Z", "expiresAt": "2026-11-09T11:20:12.000Z" }
    ],
    "highRiskExpiresAt": null,
    "pausedScopes": [],
    "targets": [{ "id": "team_42", "name": "Acme Prod" }],
    "expiresAt": null
  }
}
FieldMeaning
eventIdStable across retries and across our deliveries of the same change. De-duplicate on it.
productThe Zev product, e.g. zevcloud.
tenantModelive, or test for a test connection. On every delegated-access event.
changesWhat changed. See the list below.
reasonThe main change, from the same list.
byWho caused it: user, partner (you), product or zevid.
occurredAtWhen it happened at ZevID.
grantIdThe connection. The same grant_id your token response carries.
accountIdThe user’s ZevID account id.
gvThe connection’s version. Goes up on every change.
statusactive, suspended or revoked.
scopesWhat the user granted.
effectiveScopesWhat you may use right now: granted, approved, not paused, high-risk only while confirmed. Empty unless status is active.
highRiskEvery high-risk permission on the connection. An expiresAt in the past means it ended.
highRiskExpiresAtEarliest end of an active high-risk permission, epoch seconds, or null.
pausedScopes[{ scope, by, level }]. by is zevid or product; level is grant (this connection) or approval (your whole app).
targetsWhat the user picked, [{ id, name }].
expiresAtTest connections only: when it ends on its own. null for live.

The payload never carries an email, a person’s name or a free-text reason.

changes

ValueMeaning
connectedFirst connection.
reconnectedThe user connected again after a revoke, or a fresh authorize replaced the old token chain.
scopes_added / scopes_removedPermissions changed.
targets_added / targets_removed / targets_renamedThe picked targets changed.
suspended / unsuspendedThe connection was suspended or restored.
revokedThe connection was removed. reason says why, for example revoked (the user or you), test_grant_expired (a test connection reached its end) or test_member_removed (that person left your team).
high_risk_confirmedA high-risk permission was granted.
high_risk_reconfirmedThe user confirmed high-risk permissions again.
high_risk_expiredA high-risk window ended. The connection is not revoked.
scope_suspended / scope_unsuspendedA permission was paused or unpaused. See Paused permissions.

App events

delegated_access.app.updated is about your app on one product, not one user:

{
  "eventId": "…",
  "product": "zevcloud",
  "changes": ["scope_suspended"],
  "reason": "scope_suspended",
  "by": "product",
  "occurredAt": "…",
  "status": "active",
  "approvedScopes": ["zevcloud.deploy", "zevcloud.services.read"],
  "pausedScopes": [{ "scope": "zevcloud.services.read", "by": "product" }],
  "scope": "zevcloud.services.read"
}

status is active or suspended. scope names the permission a pause is about, or is null. reason can also be app_suspended or app_unsuspended when ZevID suspended your app on every product at once.

Refresh reuse

delegated_access.security.refresh_reuse carries grantId, accountId, gv and status, with reason: "refresh_reuse". Someone presented a refresh token that was already used. That is either a stolen token or two refreshes running at the same time. All refresh tokens of the connection are revoked; the connection itself stays, and the user reconnects with one click. Check how you store refresh tokens and that refreshes are serialised; see Refresh tokens.

Applying events safely

  1. Verify the signature (how).
  2. Ignore the event if you already processed its data.eventId.
  3. For connection events, compare data.gv with what you stored for data.grantId. Skip it if it is not newer.
  4. Store the new state and act on it: on revoked, delete the stored refresh token; on updated, read effectiveScopes and targets.

If you missed events

If your endpoint was down beyond our retries, you do not need to replay anything. The next token refresh for a connection returns its current scope, targets and gv. A connection that no longer exists fails refresh with invalid_grant.

Updated at, Wednesday, October 21, 2026