User-Scoped API Keys
Issue, list, and revoke API keys on behalf of your application's end users — without building your own key infrastructure.
If your product (a calendar, an inbox, a profile platform, anything) wants to expose programmatic access to your users — "create a personal access token to use my account from a script" — ZewstID handles the boring parts: secure generation, hashing, scoping, rotation, revocation, and validation. You provide the UI and decide what the keys are for.
This is what GitHub does with PATs, what Stripe does with restricted keys, and what Clerk does with user API keys — except you don't have to build any of it.
Architecture
┌──────────────────┐ ┌────────────────┐ │ Your App │ ① OAuth login │ ZewstID │ │ (Cal / Mail / │ ──────────────────────► │ account. │ │ your product) │ │ zewstid.com │ │ │ └────────────────┘ │ ② Settings page │ ③ Issue key on ▲ │ shows the keys │ behalf of user │ │ the user owns │ ────────────────────────► │ │ │ (M2M, your service │ │ │ account creds) │ └──────────────────┘ ◄─────────────────────── ┌────┴───────────┐ ▲ key (shown once) │ ZewstID API │ │ │ api.zewstid. │ │ ④ End user calls your API: │ com │ │ Authorization: Bearer zw_live_... │ │ │ │ Stores keys, │ │ ⑤ Your API → introspect ─────────────►│ validates, │ │ { active, sub, scopes, ... } │ rate-limits │ └───────────────────────────────────── └────────────────┘
Three primary endpoints, all M2M-authenticated:
| Endpoint | Purpose |
|---|---|
POST /api/v1/applications/:appId/users/:sub/api-keys | Issue a key for an end user |
GET /api/v1/applications/:appId/users/:sub/api-keys | List a user's keys |
DELETE /api/v1/applications/:appId/users/:sub/api-keys/:keyId | Revoke |
POST /api/v1/api-keys/introspect | Validate a presented key |
Plus an admin lister:
GET /api/v1/applications/:appId/api-keys?kind=end_userOne-time setup
1. Provision a service account
Developer Portal → Service Accounts → New → grant:
api-keys:issueapi-keys:readapi-keys:revoke
The service account is automatically tied to your application — only your app can issue keys on its own users' behalf.
2. Configure your env
ZEWSTID_CLIENT_ID=sa_xxxxxxxxxxxx # service account ZEWSTID_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx ZEWSTID_APP_ID=app_xxxxxxxxxxxx # your application's public id
That's it. The SDK auto-discovers token + introspection endpoints.
SDK usage (Next.js)
Issuing a key
// app/api/me/api-keys/route.ts import { getServerSession } from 'next-auth/next'; import { getUserApiKeysClient } from '@zewstid/id-nextjs/server'; import { authOptions } from '@/lib/auth'; export async function POST(req: Request) { const session = await getServerSession(authOptions); if (!session?.user?.zewstIdSub) { return new Response('Unauthorized', { status: 401 }); } const { name, scopes } = await req.json(); const keys = getUserApiKeysClient(); const { apiKey } = await keys.create({ forUserSub: session.user.zewstIdSub, name, scopes, expiresInDays: 90, }); // apiKey.key is shown ONCE — return it to the client, then drop it. return Response.json({ apiKey }); }
Listing a user's keys
const { apiKeys } = await getUserApiKeysClient().list(session.user.zewstIdSub); // → [{ keyId, name, keyPrefix, scopes, lastUsedAt, ... }]
Revoking
await getUserApiKeysClient().revoke(session.user.zewstIdSub, keyId);
Validating a key your API received
// In your API middleware import { getUserApiKeysClient } from '@zewstid/id-nextjs/server'; export async function authenticate(req: Request) { const token = req.headers.get('authorization')?.replace(/^Bearer /i, ''); if (!token) throw new Response('No token', { status: 401 }); const result = await getUserApiKeysClient().introspect(token); if (!result.active) throw new Response('Invalid token', { status: 401 }); return { userSub: result.forUserSub, // who the request is for scopes: result.scopes ?? [], // what they can do appId: result.appId, // sanity check — is this YOUR app? }; }
SDK usage (Node)
import { ZewstIDUserApiKeys } from '@zewstid/id-node'; const keys = new ZewstIDUserApiKeys({ clientId: process.env.ZEWSTID_CLIENT_ID!, clientSecret: process.env.ZEWSTID_CLIENT_SECRET!, appId: process.env.ZEWSTID_APP_ID!, }); const { apiKey } = await keys.create({ forUserSub: 'user_xxx', name: 'CI deploy token', scopes: ['cal:read', 'cal:write'], expiresInDays: 90, }); await keys.revoke('user_xxx', apiKey.keyId);
Drop-in UI component
For the typical "settings page" experience, the
<ZewstIDApiKeysManager />// app/(dashboard)/settings/api-keys/page.tsx 'use client'; import { ZewstIDApiKeysManager } from '@zewstid/id-nextjs'; export default function ApiKeysSettings() { return ( <ZewstIDApiKeysManager // Wire to your own server-side endpoints that proxy to ZewstID endpoints={{ list: '/api/me/api-keys', create: '/api/me/api-keys', revoke: (keyId) => `/api/me/api-keys/${keyId}`, }} availableScopes={[ { value: 'cal:read', label: 'Read calendar events' }, { value: 'cal:write', label: 'Create / edit events' }, ]} /> ); }
The component is purely UI — your server-side route handlers (using
getUserApiKeysClient()Key format and validation
| Field | Value |
|---|---|
| Format | zw_live_<48 random chars>zw_test_<48 random chars>sk_live_*sk_sandbox_* |
| Visible prefix | First 8 chars + ... |
| Hashing | bcrypt (cost 10) — never stored in plaintext |
| Returned at creation | Once. After that, only the prefix is recoverable. |
Validating without round-tripping
For high-throughput APIs, you can verify keys locally if you cache the introspection result. Recommended pattern:
- First request with a new key: introspect, cache in Redis for 5 minutes
{ scopes, forUserSub } - Subsequent requests: hit the cache, skip the round-trip
- On revocation in your settings UI: invalidate the cache for that key
Don't try to verify keys offline by re-hashing — bcrypt is slow on purpose, and you'd duplicate ZewstID's storage. The introspection endpoint is fast enough for almost any workload.
Security notes
Show the key once. The introspection-based design means we never store the plaintext, so we can never re-display it. Make this very obvious in your UI — copy buttons, "save now" warnings, ideally a one-time download.
Scope your keys narrowly. The whole point is least-privilege. Don't issue
cal:*cal:readSet expiry by default. Suggest
90dRevoke on suspicious activity. Surface the
lastUsedAtusageCountBind to user identity. The
forUserSubforUserSubPricing
User-scoped API keys are included in your ZewstID plan with no per-key cost. Rate limits apply to issuance (60 keys / minute / app) and read operations (600 / minute), which is far above any real workload. Validation through introspection has its own much higher rate limit — designed to be on every request.
See also
- Scopes Catalog — full M2M scope list including
api-keys:* - M2M Authentication — provisioning service accounts
- API Keys (developer self-issued) — when you the developer want a key for your own automation
Was this page helpful?
Let us know how we can improve our documentation