Inviting Users & Auto-Creating ZewstIDs
Invite someone to your app by email from your backend. If they already have a ZewstID, the invite is tied to it; if they don't, ZewstID creates one automatically — then they sign in and land in your app.
This is an M2M flow. You'll need a service account with the
scope. New here? Read M2M Authentication first, and the Scopes Catalog for how scopes are granted.users:invite
Overview
Your Backend ZewstID Invitee │ POST /m2m/users/invite │ │ │ ────────────────────────────────►│ creates ZewstID if new │ │ │ ─── branded invite email ────► │ │ │ │ clicks link │ │ ◄──────────────────────────────│ │ │ 302 → your invite_accept_uri │ │ (your OAuth sign-in runs) │ │ │ POST /m2m/invites/:token/accept │ │ │ ────────────────────────────────►│ consumes token, returns user │
Prerequisites
- Create a service account in the Developer Portal; note its and
client_id(the secret is shown once, at creation).client_secret - Grant it . In the portal the scope must show as "in token" — if it appears under a drift warning, click Re-sync scopes.
users:invite - Register your redirect URLs on the service account:
- — the exact URL ZewstID may return the user to.
redirect_uri - — the page in your app that handles acceptance (its origin must match a registered URL).
invite_accept_uri
1. Get an M2M access token
Use the standard token endpoint, form-encoded (not
/api/v1/oauth/tokencurl -X POST https://api.zewstid.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=client_credentials \ -d client_id=$ZEWSTID_SA_CLIENT_ID \ -d client_secret=$ZEWSTID_SA_CLIENT_SECRET \ --data-urlencode 'scope=users:invite'
Cache and reuse the
access_token400 invalid_scope2. Send the invite
This is the step that creates a ZewstID when the email has none.
curl -X POST https://api.zewstid.com/api/v1/m2m/users/invite \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "redirect_uri": "https://app.example.com/auth/callback", "invite_accept_uri": "https://app.example.com/invitations/accept", "first_name": "New", "last_name": "Person", "app_name": "Example App", "state": "optional-opaque-value", "metadata": { "role": "tester" } }'
| Field | Required | Notes |
|---|---|---|
email | ✅ | Dedup key. Existing ZewstID → reused; otherwise a new one is created. |
redirect_uri | ✅ | Must be a registered redirect URL on the SA, or 400 |
invite_accept_uri | — | Where the click lands; its origin must be registered. Falls back to redirect_uri |
first_namelast_name | — | Seeded onto a newly-created user. |
app_name | — | Branding in the email; defaults to the SA/client name. |
state | — | Opaque; echoed back on the redirect. |
metadata | — | Stored on the invite (and merged onto a new user's attributes). |
What ZewstID does: looks up the email; if no user exists it creates a ZewstID (passwordless, email pre-verified, tagged
invitedBy=<your SA>201
{ "invite_id": "…", "email": "[email protected]", "status": "sent", "user_exists": false, "expires_at": "…Z" }
user_exists: false3. The user clicks the email → ZewstID redirects to your app
ZewstID handles the click at its own URL, then 302-redirects to your
invite_accept_uri
https://app.example.com/invitations/accept ?invite_token=<token> &email=<email> &app_name=<app_name> &user_exists=<true|false> &state=<your state, if sent>
The click does not consume the token (that happens in step 5). Optionally, your page may read public invite info first:
GET https://api.zewstid.com/api/v1/invites/<invite_token> → { "email": "…", "app_name": "…", "user_exists": false, "expires_at": "…Z" }
(Public, rate-limited 30/min — no invite id, owner, or metadata exposed.)
4. Authenticate the user with your normal sign-in
From the accept page, run your standard ZewstID OAuth sign-in (see OAuth Integration). A brand-new user establishes how they sign in; an existing user just logs in.
Why not consume the invite directly here? The accept step hands off to your OAuth flow instead of minting a session itself. Bypassing your app's sign-in causes a "state cookie was missing" error. Let your OAuth flow own the session; the invite is reconciled in step 5.
Optionally,
POST /api/v1/invites/<token>/activate{ "password": "…" }5. Consume the invite from your backend
After the user is authenticated, consume the invite with the same service account that created it:
curl -X POST https://api.zewstid.com/api/v1/m2m/invites/<invite_token>/accept \ -H "Authorization: Bearer $ACCESS_TOKEN"
It atomically consumes the token (single-use) and returns the reconciled identity:
{ "invite_id": "…", "email": "…", "user_id": "<ZewstID user id>", "user_exists": false, "app_name": "…", "accepted_at": "…Z", "metadata": { "role": "tester" } }
Persist
user_idsubExisting vs. new users — and the one gotcha
- Email already a ZewstID → reused (); no new identity.
user_exists: true - Email with no ZewstID → a new identity is created ().
user_exists: false - ⚠️ Dedup is by email only. Inviting the same person under a different email than their existing ZewstID creates a second, duplicate identity — ZewstID can't tell they're the same human. If your users may arrive under multiple emails, normalize/confirm the invited email, or reconcile on your side.
Errors & troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Token 400 invalid_scope | users:invite | Portal → the SA → Re-sync scopes |
Token 401 invalid_client | bad credentials, or you used /api/v1/oauth/token | Use https://api.zewstid.com/oauth/token |
Invite 400 | redirect_uriinvite_accept_uri | Register the exact URL(s) on the SA |
Invite 403 insufficient_scope | token lacks users:invite | Re-mint after attaching the scope |
| Consume fails on 2nd call | token already consumed (single-use) | Expected — don't retry a consumed token |
| Consume wrong-client error | a different SA tried to consume | Consume with the same SA that invited |
Security notes
- The invite token travels only in the email and the redirect — keep it out
of logs, analytics, and OAuth .
state - Invites expire in 72 hours and are single-use.
- /
redirect_uriare validated against the SA's registered URLs — register exactly what you use; never accept client-supplied redirects.invite_accept_uri - Keep the SA server-side only; rotate it in the portal if leaked.
client_secret
Endpoint reference
| Step | Method & path | Auth |
|---|---|---|
| Token | POST /oauth/token | client_idclient_secret |
| Send invite | POST /api/v1/m2m/users/invite | Bearer (users:invite |
| Public invite info | GET /api/v1/invites/:token | none (rate-limited) |
| Click → your app | GET /api/v1/invites/:token/accept | none |
| (Optional) set password | POST /api/v1/invites/:token/activate | none (rate-limited) |
| Consume invite | POST /api/v1/m2m/invites/:token/accept | Bearer (users:invite |
See also
Was this page helpful?
Let us know how we can improve our documentation