Delegated access to Zev products
Common scenarios
The situations partners hit most often with delegated access, what happens in each, and what to build.
Most questions about delegated access are one of these. Each says what you are trying to do, what happens, and what to build.
You want to test before going live
What happens. Your live app needs ZevID’s approval and a verified domain. Your test environment needs neither, but only your own team can sign in to it or connect it, and test and live never mix.
Build this. Test with your test environment and your team’s own ZevID accounts. Add teammates under Team in the console. Test connections end after a set number of days, shown in the console; reconnect to keep testing. See Test environments.
Your app already uses ZevAuth for sign-in, then asks for ZevCloud access
What happens. The user already has a ZevID account and a ZevID session from signing in to you, so they see the consent screen only, with no second sign-in. High-risk permissions may still ask them to “Confirm it’s you”.
Build this. Tell ZevID who you expect: build the URL on your server with
your ZevAuth user_id, or send their ZevID account id as login_hint. Only that account can
approve. Check that account_id in the token response is the same user. We
check it too, and revoke a connection that came back for anyone else. See
The flow.
Your app has its own sign-in, not ZevAuth
What happens. The user signs in to ZevID once, on the consent screen, the way they would for “Connect GitHub”. That proves they own the product account. It does not replace your sign-in.
Build this. Store the connection (grant_id) against your own user,
in the session that started the flow. Never match users by email.
The browser is signed in to a different ZevID account than your user
What happens. With a hint (login_hint, or user_id on the server call), ZevID asks the user to
switch to the right account. If they cannot or will not, you get
error=access_denied with error_reason=account_mismatch.
Build this. Tell the user to continue with the ZevID account they use with you. Never link a connection to a different account.
The user has never used the product
What happens. ZevCloud creates their account the first time they arrive with their ZevID. The consent screen shows ZevCloud’s own “Create a team” button, which brings them straight back to finish connecting.
Build this. Nothing extra. Never create product accounts or teams for users; only the person can, in the product.
None of the user’s teams can be picked
What happens. Teams the user cannot connect, for example where they are only a member, are shown greyed out with the product’s reason, such as “Only owners and admins can connect apps”.
Build this. Explain the requirement in your UI before you send them to connect.
The user allows the connection but leaves a high-risk permission unticked
What happens. The connection works without it.
Build this. Always read the granted scope. Offer the feature again later
through authorize. See High-risk permissions.
A high-risk permission ended after its confirmation window
What happens. Tokens stop carrying it; the rest of the connection keeps
working. Product calls that need it answer 403 high_risk_confirmation_required.
A refresh answers invalid_grant with reason: high_risk_expired only when
nothing else usable is left on the connection.
Build this. Send the user through authorize with prompt=reconfirm. Do not
ask them to reconnect from scratch.
The user wants to add or remove a team later
What happens. They can remove one from Apps and websites in ZevID. Adding one always starts from your app.
Build this. Offer a Manage teams button that starts authorize with
prompt=configure.
You need two Zev products
What happens. Each product is a separate approval, consent and connection.
Build this. Run one authorize per product and store each grant_id
separately.
Access stops working
What happens. The user disconnected, removed a team or lost their role;
the product or ZevID paused a permission or suspended your app; a test
connection ended; or a refresh token was reused. Each arrives as a
webhook with a reason, and as an error with
the same reason.
Build this. Handle events and errors by reason:
- reconnect for
grant_revoked,refresh_reusedandtest_grant_expired; - reconfirm for
high_risk_expired; - wait for ZevID on pauses and suspensions (
scope_suspended,grant_suspended,app_suspended).
The full list is on Errors.
Two servers refresh the same connection at once
What happens. It looks like theft: every refresh token of that connection is revoked and the user must reconnect.
Build this. Serialise refreshes per connection, for example with a lock on
grant_id, and always store the newest refresh token. See
Refresh tokens.
Updated at, Wednesday, October 21, 2026