How it works
- Your backend authenticates the user in your own system
- Your backend calls
POST /v2/portal.createSessionwith a root key that holds the required permissions (see Required permissions) - You redirect the user to the returned portal URL, which carries the code
- The portal exchanges the code for a 24-hour access token
- The browser calls the Unkey API with that token in an httpOnly cookie
1. Configure a portal
Enable the Customer Portal for your workspace in the Unkey dashboard.Dashboard configuration UI is coming soon. During early access, reach out to the Unkey team to get your portal configured.
my-portal or billing-dashboard. You can pass either that slug or the portal’s ID when creating sessions.
Slugs must be 3–64 characters, lowercase alphanumeric and hyphens only, cannot start or end with a hyphen, and cannot contain consecutive hyphens. A portal value that matches neither a slug nor an ID in your workspace returns a 404, so a typo and a portal that was never provisioned look the same.
Optionally, you can customize branding with your logo and brand colors.
2. Create a session
When your user wants to access the portal, create a session from your backend:Required parameters
Optional parameters
3. Redirect your user
Send the user to the portal URL. The code it carries is valid for 15 minutes and can only be redeemed once.- Exchange the code for a 24-hour access token
- Set it as an httpOnly cookie
- Redirect to the first visible tab based on the session’s scopes
Scopes and tabs
Scopes come from a fixed vocabulary. Every scope is bound to the end user in the session:keys:* applies only to keys that user owns within the portal’s keyspace, and analytics:read returns only that user’s own verification events. An end user can never see another identity’s keys or analytics.
Tab visibility is derived from the scopes:
The API requires at least one scope. An empty
scopes array is rejected with HTTP 400, as is any value outside the vocabulary above.
Session lifecycle
Both credentials are stored only as hashes, so they cannot be recovered from Unkey. The code exists only in the URL you were given, and the access token only in the user’s cookie.
Re-authenticating creates a new session with a fresh code rather than extending an existing one.
When the access token expires:
- If
returnUrlwas set on the session → redirects to{returnUrl}?reason=session_expired - Otherwise → shows a “Session expired” error page
returnUrl is set per session on portal.createSession, not once on the portal,
so one portal can return each user to whichever page they came from.
Branding
The portal supports basic white-labeling:
Logo URLs must be HTTPS.
Required permissions
Creating a portal session needs two things from your root key, and both are checked. First, permission to mint sessions for the portal:portal.<portal_id>.create_portal_session to restrict a key to one portal.
Second, a session can never carry a capability your root key does not itself hold. Each scope you request also requires the equivalent permission on the keyspace behind the portal:
Requesting a scope you do not hold returns 403 for the whole request rather than a session with fewer capabilities, so a missing grant shows up immediately instead of as a portal that silently misses a tab.
A root key without the portal session permission gets a 404, not a 403. That is deliberate: a caller who cannot mint for a portal is not told whether it exists, so a guessed slug reveals nothing.
You can grant these on the root key in the Unkey dashboard under Settings, Root Keys.
Error responses
The exchange deliberately does not distinguish between an unknown code, an expired one, and one that was already redeemed.