M2M (Machine-to-Machine) Authentication
Machine-to-machine authentication allows your backend services, cron jobs, and automation scripts to securely call the ZewstID API without user interaction.
Overview
M2M authentication uses the OAuth 2.0 Client Credentials grant. Your service authenticates with a client ID and secret, and receives an access token scoped to specific permissions.
Your Service ZewstID API │ │ │ POST /oauth/token │ │ grant_type=client_credentials│ │ client_id=xxx │ │ client_secret=yyy │ │ ─────────────────────────► │ │ │ │ { access_token: "eyJ..." } │ │ ◄───────────────────────── │ │ │ │ GET /api/v1/m2m/users │ │ Authorization: Bearer eyJ... │ │ ─────────────────────────► │ │ │ │ { users: [...] } │ │ ◄───────────────────────── │
Creating a Service Account
Via the Developer Portal
- Navigate to Service Accounts in the left sidebar
- Click Create Service Account
- Enter a name and description
- Select the scopes your service needs
- Click Create — you'll receive a and
clientIdclientSecret
Important: Save the client secret immediately. It is only shown once.
Via the API
curl -X POST https://api.zewstid.com/api/v1/service-accounts \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "My Backend Service", "description": "Handles nightly user sync", "scopes": ["users:read", "users:write"] }'
Response:
{ "id": "sa_abc123", "clientId": "sa-my-backend-service", "clientSecret": "zs_secret_xxxxx", "name": "My Backend Service", "scopes": ["users:read", "users:write"], "createdAt": "2026-02-20T10:00:00Z" }
Getting an Access Token
Use the
client_credentialscurl -X POST https://api.zewstid.com/api/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "sa-my-backend-service", "client_secret": "zs_secret_xxxxx", "scope": "users:read" }'
Response:
{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 3600, "scope": "users:read" }
Tokens expire after 1 hour by default. Your service should request a new token before the current one expires.
Scope System
Scopes control what your service account can access:
| Scope | Description |
|---|---|
users:read | List and get user profiles |
users:write | Create and update users |
users:invite | Invite users via email |
users:admin | Delete users, manage credentials |
orgs:read | List organizations and members |
orgs:write | Create and manage organizations |
analytics:read | Read authentication analytics |
agents:read | List and view agents |
agents:write | Register and manage agents |
delegations:read | View delegation requests |
delegations:write | Create and manage delegations |
Request only the scopes your service needs. You can update scopes later via the portal or API.
M2M API Endpoints
Once you have an access token, use it to call the M2M API:
| Method | Endpoint | Scope Required | Description |
|---|---|---|---|
GET | /api/v1/m2m/whoami | Any | Verify token and get service identity |
GET | /api/v1/m2m/users | users:read | List users with pagination |
GET | /api/v1/m2m/users/:id | users:read | Get user by ID |
POST | /api/v1/m2m/users | users:write | Create a new user |
PATCH | /api/v1/m2m/users/:id | users:write | Update user profile |
DELETE | /api/v1/m2m/users/:id | users:admin | Delete a user |
GET | /api/v1/m2m/users/:id/sessions | users:admin | List user sessions |
DELETE | /api/v1/m2m/users/:id/sessions | users:admin | Revoke all sessions |
GET | /api/v1/m2m/orgs | orgs:read | List organizations |
GET | /api/v1/m2m/orgs/:id | orgs:read | Get organization |
GET | /api/v1/m2m/orgs/:id/members | orgs:read | List organization members |
GET | /api/v1/m2m/analytics/overview | analytics:read | Authentication analytics |
GET | /api/v1/m2m/analytics/events | analytics:read | Auth event log |
GET | /api/v1/m2m/analytics/users/active | analytics:read | Active user metrics |
POST | /api/v1/m2m/users/invite | users:invite | Send user invite email |
GET | /api/v1/invites/:token | Public | Get invite info (read-only) |
POST | /api/v1/m2m/invites/:token/accept | users:invite | Consume invite token |
User Invites
Invite users to your application via email. ZewstID creates the user account (if needed), sends a branded email, and provides a flow for the user to authenticate and join your app.
Sending an Invite
curl -X POST https://api.zewstid.com/api/v1/m2m/users/invite \ -H "Authorization: Bearer $M2M_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "redirect_uri": "https://yourapp.com/api/auth/callback/zewstid", "invite_accept_uri": "https://yourapp.com/invites/accept", "app_name": "Your App", "first_name": "Jane", "last_name": "Doe", "metadata": { "role": "member" } }'
Response (201):
{ "invite_id": "uuid", "email": "[email protected]", "status": "sent", "user_exists": false, "expires_at": "2026-04-03T12:00:00Z" }
Fields:
| Field | Required | Description |
|---|---|---|
email | Yes | Email address to invite |
redirect_uri | Yes | Must match a registered redirect URI on your service account |
invite_accept_uri | No | Page in your app where invite UI is shown. Origin must match a registered redirect URI. If omitted, falls back to redirect_uri |
state | No | Opaque state string, forwarded to redirect |
first_name | No | Pre-populate user's first name |
last_name | No | Pre-populate user's last name |
app_name | No | Display name for your app in the invite email |
metadata | No | Arbitrary JSON stored with the invite, returned on acceptance |
How the Flow Works
- You call — ZewstID creates the user (if new) and sends the invite email
POST /m2m/users/invite - User clicks the email link → ZewstID redirects to your with query params:
invite_accept_uri?invite_token=xxx&[email protected]&app_name=YourApp&user_exists=true - Your page shows an invite UI and triggers a standard OAuth sign-in (e.g. NextAuth with
signIn())login_hint - After authentication, your backend calls to consume the invite
POST /m2m/invites/:token/accept
Consuming the Invite
After the user authenticates on your app, call this from your backend:
curl -X POST https://api.zewstid.com/api/v1/m2m/invites/$INVITE_TOKEN/accept \ -H "Authorization: Bearer $M2M_TOKEN"
Response (200):
{ "invite_id": "uuid", "email": "[email protected]", "user_id": "keycloak-uuid", "user_exists": true, "app_name": "Your App", "accepted_at": "2026-03-31T12:00:00Z", "metadata": { "role": "member" } }
The token is single-use — a second call returns 404. Only the service account that created the invite can consume it (returns 403 otherwise).
Checking Invite Status
Read invite details without consuming:
curl https://api.zewstid.com/api/v1/invites/$INVITE_TOKEN
Response (200):
{ "email": "[email protected]", "app_name": "Your App", "user_exists": false, "status": "pending", "expires_in": 259200 }
Building Your Invite Accept Page (Next.js Example)
Your app needs an invite accept page that the user lands on after clicking the email link. This page handles authentication via your normal OAuth flow, then consumes the invite token.
1. Create the invite accept page (e.g.
app/invites/accept/page.tsx'use client'; import { useSearchParams } from 'next/navigation'; import { signIn, useSession } from 'next-auth/react'; import { useEffect, useState } from 'react'; export default function AcceptInvitePage() { const params = useSearchParams(); const { data: session } = useSession(); const [status, setStatus] = useState<'loading' | 'accepting' | 'done' | 'error'>('loading'); const inviteToken = params.get('invite_token'); const email = params.get('email'); const appName = params.get('app_name'); const userExists = params.get('user_exists') === 'true'; // If user is authenticated, consume the invite via your backend useEffect(() => { if (session && inviteToken) { setStatus('accepting'); fetch(`/api/invites/accept?invite_token=${inviteToken}`) .then(res => res.json()) .then(() => setStatus('done')) .catch(() => setStatus('error')); } }, [session, inviteToken]); if (!inviteToken) return <p>Invalid invite link.</p>; if (session) { if (status === 'done') return <p>Welcome! You've joined {appName}.</p>; if (status === 'error') return <p>Something went wrong. Please try again.</p>; return <p>Setting up your account...</p>; } // Not authenticated — show sign-in button return ( <div> <h1>{appName} has invited {email} to join</h1> <button onClick={() => signIn('zewstid', { callbackUrl: `/invites/accept?invite_token=${inviteToken}`, }, { login_hint: email ?? '', prompt: userExists ? 'login' : 'create', }) }> {userExists ? 'Sign in with ZewstID' : 'Create account with ZewstID'} </button> </div> ); }
2. Create a backend API route to consume the invite (e.g.
app/api/invites/accept/route.tsimport { NextRequest, NextResponse } from 'next/server'; const M2M_TOKEN_URL = 'https://api.zewstid.com/api/v1/oauth/token'; const INVITE_ACCEPT_URL = 'https://api.zewstid.com/api/v1/m2m/invites'; async function getM2MToken() { const res = await fetch(M2M_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: process.env.ZEWSTID_SA_CLIENT_ID, client_secret: process.env.ZEWSTID_SA_CLIENT_SECRET, scope: 'users:invite', }), }); const data = await res.json(); return data.access_token; } export async function GET(req: NextRequest) { const token = req.nextUrl.searchParams.get('invite_token'); if (!token) return NextResponse.json({ error: 'Missing token' }, { status: 400 }); const m2mToken = await getM2MToken(); const res = await fetch(`${INVITE_ACCEPT_URL}/${token}/accept`, { method: 'POST', headers: { Authorization: `Bearer ${m2mToken}` }, }); const data = await res.json(); return NextResponse.json(data, { status: res.status }); }
3. Set environment variables for your service account:
ZEWSTID_SA_CLIENT_ID=sa-your-app ZEWSTID_SA_CLIENT_SECRET=zs_secret_xxxxx
4. When sending invites, include
invite_accept_uri{ "email": "[email protected]", "redirect_uri": "https://yourapp.com/api/auth/callback/zewstid", "invite_accept_uri": "https://yourapp.com/invites/accept", "app_name": "Your App" }
Tip: The
origin must match one of your service account's registered redirect URIs. Theinvite_accept_uriis your NextAuth callback URL.redirect_uri
Node SDK Usage
The
@zewstid/nodeZewstIDServiceAccountimport { ZewstIDServiceAccount } from '@zewstid/node'; const serviceAccount = new ZewstIDServiceAccount({ domain: 'auth.zewstid.com', clientId: process.env.ZEWSTID_SA_CLIENT_ID!, clientSecret: process.env.ZEWSTID_SA_CLIENT_SECRET!, scopes: ['users:read', 'users:write'], }); // Get a token (automatically cached and refreshed) const token = await serviceAccount.getToken(); // Use the token const res = await fetch('https://api.zewstid.com/api/v1/m2m/users', { headers: { Authorization: `Bearer ${token}` }, }); const users = await res.json();
The SDK handles token caching, automatic refresh, and retry logic.
Credential Rotation
Rotate your service account credentials periodically for security:
curl -X POST https://api.zewstid.com/api/v1/service-accounts/sa_abc123/rotate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response:
{ "clientId": "sa-my-backend-service", "clientSecret": "zs_secret_new_yyyyy", "rotatedAt": "2026-02-20T12:00:00Z" }
The old secret is invalidated immediately. Update your service configuration with the new secret.
Rate Limits
M2M endpoints have the following rate limits:
| Endpoint | Limit |
|---|---|
POST /oauth/token | 30 requests/minute per client |
GET /m2m/* | 100 requests/minute per client |
POST/PATCH /m2m/* | 30 requests/minute per client |
DELETE /m2m/* | 10 requests/minute per client |
Rate limit headers are included in every response:
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1708420800
Best Practices
- Least privilege — Request only the scopes your service needs
- Rotate credentials — Rotate secrets every 90 days
- Cache tokens — Don't request a new token for every API call
- Handle expiration — Check and refresh before expiry
expires_in - Secure storage — Store credentials in environment variables or a secrets manager, never in source code
- Monitor usage — Check the audit log for unexpected access patterns
Next Steps
- Agent Authentication — Register AI agents with trust levels
- Delegation & A2A — Agent-to-agent token exchange
- Node.js SDK Reference — class docs
ZewstIDServiceAccount
Was this page helpful?
Let us know how we can improve our documentation