Loading self-IAM…
Content is loading — this may take a moment.
Loading self-IAM…
Content is loading — this may take a moment.
API reference
Every route with its authentication, request shape, status codes, limits, and use cases. Base URL: https://selfiam.site
How quota is enforced — and why failures still count.
Every API key has a daily op budget that resets at UTC midnight: **Free 100/key**, **Pro 2,000/key (10,000/org)**. The counter is incremented atomically so concurrent requests cannot overshoot.
**Email OTP** has a separate per-organization daily budget: **Free 10**, **Pro 50** sends per UTC day. A slot is reserved before delivery and refunded when the mail was not triggered, so only successfully delivered OTP emails count. While a code is still valid for a recipient it is reused — never re-emailed.
**Usage is counted even when the request fails.** All keyed routes run `verifyApiKeyAndQuota` before body/header validation, so malformed, rejected, and unauthorized requests still consume quota. Success and failure responses both echo `remainingOps` / `resetDate`.
When the budget is exhausted every further call returns `429 rate_limit_exceeded` until the reset. On the Pro plan, individual keys share an org-level cap (10,000/day) — when it is hit, all keys for that org are blocked until UTC midnight. On Free, each key has 100/day.
The hybrid JWT + cookie session model.
Sign-in/sign-up returns a short-lived **HS256 JWT (15 min)**. The token is mirrored in an **HttpOnly cookie `ck_session`** (SameSite=Lax, Secure in production) so server pages and routes can authenticate without JavaScript, while the self-IAM widget keeps the token in localStorage for its own calls.
Sessions are stored centrally in the `sessions` collection and are revocable: logout deletes the record so the JWT can never be refreshed. The widget revalidates via `/me` on load and slides the session forward before expiry.
The JWT carries a `kind` field (`accounts` | `clients`) that selects the identity collection the token resolves against — the dashboard always requires `accounts` sessions.
**Verification flag (`emailVerified`)** — every identity carries an `emailVerified` boolean. It starts as `false` at signup and becomes `true` after a successful OTP verification or a verified Google OAuth login. When the org has `emailOtpEnabled: true`, the `/auth/me` endpoint checks this flag and returns `pendingOtp: true` if the email is not yet verified — the client must redirect to `/verify-otp`. The flag is infallible and permanent: once set, it cannot be revoked or bypassed.
Separate collections and one-account-everywhere membership.
Platform logins (self-IAM dashboard) live in the **`accounts`** collection, isolated so client API keys can never touch them. End-users created by client websites live in one shared **`clients`** collection across all organizations.
One account works across organizations: signing in through another org's key adds the org to `organizationIds` automatically. Usernames/emails are globally unique per collection — duplicate sign-ups get `409 identity_conflict` and must sign in instead.
A Google account is **linked once**: the same Google identity can join multiple organizations, but no organization's API can re-bind an already-linked Google account.
How credentials and keys are protected.
API keys are stored only as **SHA-256 hashes** and shown once at creation; keys require a name/note. Passwords are hashed with bcrypt (cost 10). QuickHash tokens are compared in constant time.
All request input is coerced to strings before entering queries (injection-safe), and a dummy bcrypt comparison runs when a user is not found so response timing does not leak account existence.
The platform super-admin lives in **environment variables only** (`SELFIAM_SUPER_ADMIN_EMAIL` / `_PASSWORD` / `_USERNAME`) — no database account exists to be leaked or brute-forced. Its session carries a `sup` claim, password checks run in constant time, login is accepted only through the platform organization, and the reserved email is rejected at signup.
Google OAuth state is stored per-flow; WhatsApp OTPs are SHA-256-hashed, single-use, and expire after 5 minutes. **Email OTPs are held in MongoDB** — hashed with SHA-256, single-use, with a 5-attempt lockout and a 5-minute expiry — the `otpRef` returned at send time must be paired with the exact email used to trigger it.
**Deployment note:** email OTPs are stored in MongoDB (not in-memory), so they survive restarts and work correctly across multiple instances and serverless deployments. This is a deliberate design choice for reliability.
How email verification is enforced for all organizations, clients, and customers.
**When OTP is enabled (`emailOtpEnabled: true`) for an organization, email verification is mandatory.** Every user — whether a platform account (`accounts` collection) or a client end-user (`clients` collection) — must complete email OTP verification before their session becomes usable. There is no bypass.
The enforcement works at three levels:
1. **Session creation** — `/auth/verify` and `/auth/signup` return a provisional session with `pendingOtp: true` when OTP is enabled. The `emailVerified` flag on the identity is `false` until OTP is completed.
2. **Auth guard** — every protected API route calls `requireSession()`, which rejects `pendingOtp` sessions with `403 otp_verification_required`. The guard also supports `requireEmailVerified: true` to enforce the `emailVerified` flag independently of `pendingOtp`.
3. **Session restoration** — `/auth/me` checks the org's `emailOtpEnabled` setting and the identity's `emailVerified` flag. If OTP is enabled but the email is not verified, it returns `pendingOtp: true` even if the JWT does not carry the flag — the client must redirect to `/verify-otp`.
**The verification flag (`emailVerified`) is infallible.** Once set to `true` by a successful OTP verification or a verified Google OAuth login, it is permanent for that identity. It cannot be revoked, downgraded, or bypassed. The only way to prove email ownership is through the OTP channel or a Google-verified email — there is no admin override or manual flag.
**Every organization can enable OTP independently.** Toggle it from the dashboard (Security tab), the admin panel (Organizations tab), or the `PATCH /api/v1/organization/settings` API. When enabled, all new signups and logins for that org require OTP verification. Existing sessions with `pendingOtp: true` are blocked until verified.
**Google OAuth bypasses OTP entirely** because Google already proves email ownership via its `email_verified` field. Google logins create full sessions with `emailVerified: true` — no provisional session is issued.
**The `/auth/email/verify` route is idempotent.** If a user calls it after already being verified, it returns a clean session without consuming an OTP code. This is safe for retry scenarios and widget re-initialization.
A plain link that sends your users to self-IAM to sign in, then returns them with a session token — no npm package, no server code.
For sites that cannot install the `self-iam` package (WordPress, plain HTML, static hosts) you can drop a single link into your page. Build it from your **publishable key** and a callback URL on your own domain:
`https://www.selfiam.site/login/external?key=org_live_…&redirect=https://your-site.com/selfiam-callback`
Clicking the link opens the normal self-IAM login page (all sign-in features work: password, quick-hash, Google, sign-up, email OTP). Any existing self-IAM session on that browser is cleared first so a previous login can never silently complete for the wrong site. After a successful sign-in the user is redirected to `redirect` with three query parameters: `contactkit_token` (the short-lived HS256 JWT session), `contactkit_expires_at` (ISO expiry), and `contactkit_user` (base64url-encoded JSON of `{ id, username, email, phoneNumber }`).
**WARNING — self-IAM keeps NO session state for your site.** The token in the callback URL *is* the login. Your callback page must store it locally (localStorage or a cookie) or the user has to sign in again on every visit. Tokens expire after 15 minutes; slide them forward with `POST /api/v1/auth/refresh` (Bearer token) and revalidate on every page with `GET /api/v1/auth/me` before trusting it.
**WARNING — redirect allowlist is server-wide.** The `redirect` value is validated at page render against `SELFIAM_ALLOWED_REDIRECTS`, the same comma-separated list the Google OAuth flow uses. It is shared by every organization on the server, so only ever add domains you control. If the target is not allowlisted the page shows an error and never renders the login widget — no token can be sent to an unknown host.
Only **publishable** keys belong in the link (they are public by design). Never put secret keys, passwords, or quick-hashes in it. The token appears in the callback URL, so strip it from the address bar immediately after capturing it, and validate it server-side via `/me` before performing any privileged action.
Rate limits are identical to the widget: each login attempt is a keyed call to `POST /api/v1/auth/verify` and counts one operation against the key's daily quota. A plain page load of `/login/external` costs nothing — only actual sign-in attempts spend quota.
<script>
// Callback page example (selfiam-callback). Reads the returned token, stores
// it locally so the user is NOT asked to sign in again, strips it from the
// URL, then validates it against the API.
(function () {
var API = "https://www.selfiam.site";
var params = new URLSearchParams(window.location.search);
var token = params.get("contactkit_token");
if (token) {
localStorage.setItem("selfiam.token", token);
params.delete("contactkit_token");
history.replaceState(null, "", window.location.pathname + params.toString());
fetch(API + "/api/v1/auth/me", {
headers: { Authorization: "Bearer " + token },
})
.then(function (res) {
if (!res.ok) localStorage.removeItem("selfiam.token");
return res.json();
})
.then(function (data) {
var el = document.getElementById("selfiam-welcome");
if (el && data.user) el.textContent = "Welcome, " + data.user.username;
});
return;
}
var saved = localStorage.getItem("selfiam.token");
if (saved) {
fetch(API + "/api/v1/auth/me", {
headers: { Authorization: "Bearer " + saved },
}).then(function (res) {
if (!res.ok) localStorage.removeItem("selfiam.token");
});
}
})();
</script>Free · Pro · Enterprise — see the pricing page for full details.
**Free — ₹0.** 100 API calls/key/day, 10 email-OTP verifications/day, core auth + contact endpoints, shared identity across apps, community support.
**Pro — ₹198/month** (limited-time offer, ~~₹359~~). 10,000 API calls/day org-wide (2,000/key), 50 email-OTP verifications/day, Google OAuth + WhatsApp OTP, multi-organization memberships, priority support.
**Enterprise — contact us.** Unlimited API calls, dedicated support engineer, SLA, custom onboarding & SSO.
/api/v1/auth/signupCreate a user and start a session. Routes to the `accounts` collection for the self-IAM organization and the shared `clients` collection for every other organization. When email OTP verification is enabled for the organization the response contains a provisional session (`pendingOtp: true`) and the client must complete OTP verification before the session becomes usable.
| 400 | validation_error — malformed fields |
| 401 | invalid_api_key — unknown/revoked key |
| 403 | route_forbidden — route not in the org allowedRoutes |
| 409 | identity_conflict — username/email already exists (sign in instead) |
| 429 | rate_limit_exceeded — daily quota used |
curl -X POST https://www.selfiam.site/api/v1/auth/signup \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "username": "jane", "email": "jane@example.com", "password": "SuperSecret!23", "phoneNumber": "+14155550123" }'/api/v1/auth/verifySign a user in by username or email with a password or QuickHash. Adds the calling organization to the identity's memberships (`organizationIds`) so one account works across organizations. When email OTP verification is enabled for the organization the response contains a provisional session (`pendingOtp: true`) and the client must complete OTP verification before the session becomes usable.
| 400 | validation_error — missing method/identity/password |
| 401 | invalid_credentials — wrong password or QuickHash |
| 401 | invalid_api_key — unknown key |
| 403 | route_forbidden — not allowed for the org |
| 429 | rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/auth/verify \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "method": "username", "identity": "jane", "password": "SuperSecret!23" }'/api/v1/auth/meResolve the current user behind a session token (JWT). Used by the widget on load to revalidate sessions and by server helpers for page-level auth.
| 401 | invalid_session — missing/expired/revoked token |
curl https://www.selfiam.site/api/v1/auth/me -H "Authorization: Bearer <jwt>"
/api/v1/auth/refreshSlide a still-valid session forward and return a fresh token + cookie.
| 401 | invalid_session — expired or revoked (must sign in again) |
curl -X POST https://www.selfiam.site/api/v1/auth/refresh -H "Authorization: Bearer <jwt>"
/api/v1/auth/logoutRevoke the current session centrally (deletes the session record, so the JWT can never be refreshed). With `?all=1` it revokes every session for the user.
| 401 | invalid_session |
curl -X POST https://www.selfiam.site/api/v1/auth/logout?all=1 -H "Authorization: Bearer <jwt>"
/api/v1/auth/google/authorizeReturn a Google OAuth consent URL for the calling organization. On success the callback redirects back with `?contactkit_token=` or `?contactkit_error=`.
| 400 | validation_error — redirectUrl missing |
| 503 | google_not_configured — GOOGLE_CLIENT_ID/SECRET not set |
| 401 | invalid_api_key |
| 403 | route_forbidden |
| 429 | rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/auth/google/authorize \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "redirectUrl": "https://my-app.com/auth/callback" }'/api/v1/auth/password/forgotForgot-password entry point. Emails ONE message containing both recovery vehicles: a 6-digit reset code (5-minute TTL, purpose-scoped so it can never authorize login) and a single-use /reset-password link (15 minutes). The response is byte-identical whether or not the account exists — there is no account-enumeration oracle.
| 400 | validation_error — identity missing or email malformed |
| 429 | rate_limit_exceeded — 5 requests/min/IP |
curl -X POST https://www.selfiam.site/api/v1/auth/password/forgot \
-H "Content-Type: application/json" \
-d '{ "identity": "jane@example.com" }'/api/v1/auth/password/resetComplete a forgot-password flow: redeem the emailed LINK token or the emailed CODE plus identity, set a new password, and revoke EVERY session for the account (all devices must sign in again). Codes are purpose-isolated — a login OTP can never authorize a reset, and vice versa.
| 400 | validation_error — weak password / missing fields; invalid_token — link expired, spent, or unknown |
| 401 | otp_error — code invalid, expired, used, or wrong |
| 429 | otp_locked — 5 wrong code attempts destroyed the code; rate_limit_exceeded — 10 requests/5min/IP |
curl -X POST https://www.selfiam.site/api/v1/auth/password/reset \
-H "Content-Type: application/json" \
-d '{ "token": "<from-email-link>", "newPassword": "NewStr0ngPass!" }'/api/v1/auth/email/sendEmail-OTP login: request a one-time verification code for an existing account. Only works when the organization has email-OTP verification enabled (dashboard → Security), the email matches an existing identity, and the plan's daily email budget is available.
| 400 | validation_error — missing/invalid email or an array was sent |
| 401 | invalid_api_key |
| 403 | route_forbidden / email_otp_not_enabled |
| 404 | email_not_found — no account for this email (login is existing-records-only) |
| 429 | email_otp_limit_reached — plan daily budget exhausted |
| 502 | otp_delivery_failed — mail was not triggered (nothing counted) |
curl -X POST https://www.selfiam.site/api/v1/auth/email/send \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com" }'/api/v1/auth/email/verifyComplete email-OTP sign-in: exchange the code from the email for a full session token. The `otpRef` returned at send time must be paired with the exact email that triggered it. Also accepts a pending OTP session token (from `/auth/verify` or `/auth/signup`) in the `Authorization` header — the old provisional session is revoked on success and a clean session is issued. The `emailVerified` flag on the identity is set to `true` permanently — it cannot be revoked or bypassed.
| 400 | validation_error — email/otpRef/otp missing or the otpRef belongs to a different email |
| 401 | invalid_api_key / otp_error — invalid, expired, already-used, or wrong code |
| 403 | route_forbidden |
| 404 | email_not_found — no account registered with this email for this organization |
| 429 | otp_locked — too many wrong attempts; or rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/auth/email/verify \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com", "otpRef": "<otpRef from send>", "otp": "123456" }'/api/v1/auth/email/resendFail-safe email-OTP recovery: request a fresh verification code. When no code is pending one is issued immediately; when a code is active inside the resend cooldown the request is refused (never a duplicate email); once the cooldown elapses the old code is burned and replaced with a fresh one.
| 400 | validation_error — missing/invalid email or an array was sent |
| 401 | invalid_api_key |
| 403 | route_forbidden / email_otp_not_enabled |
| 429 | email_otp_cooldown — wait `retryAfterSeconds`; or email_otp_limit_reached |
| 502 | otp_delivery_failed — mail was not triggered (nothing counted) |
curl -X POST https://www.selfiam.site/api/v1/auth/email/resend \
-H "Authorization: Bearer org_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com" }'/api/v1/auth/email/reverifySession-guarded re-verification: request a fresh verification code for the email already on the signed-in account. The address is read from the database — never from the request body — so it cannot be used to enumerate or verify anyone else's email. Complete it through `/auth/email/verify`.
| 400 | validation_error — account has no email, or super-admin tried to re-verify |
| 401 | invalid_session |
| 403 | forbidden — no longer a member of the session org; or email_otp_not_enabled |
| 429 | email_otp_cooldown — wait `retryAfterSeconds`; or email_otp_limit_reached |
| 502 | otp_delivery_failed |
curl -X POST https://www.selfiam.site/api/v1/auth/email/reverify \ -H "Authorization: Bearer <jwt>"
/api/v1/organization/settingsPOSTRead (GET) or toggle (POST) the organization's email-OTP verification setting. When enabled, the org's API keys may trigger `/auth/email/send`. All users of this organization — platform accounts and client end-users alike — must complete email OTP verification before their sessions become usable. There is no bypass.
| 401 | invalid_session |
| 403 | forbidden — not a member of the organization |
| 404 | organization_error |
curl -X POST https://www.selfiam.site/api/v1/organization/settings \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{ "emailOtpEnabled": true }'/api/v1/keysPOSTList the session organization's API keys with usage stats (GET) or issue a new key (POST). Key hashes never leave the server.
| 400 | validation_error — POST without a key name |
| 401 | invalid_session — not signed in to an accounts session |
curl -X POST https://www.selfiam.site/api/v1/keys \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{ "name": "Production web app" }'/api/v1/usersList legacy IAM users belonging to the session organization — public profile fields only.
| 401 | invalid_session |
curl https://www.selfiam.site/api/v1/users -H "Authorization: Bearer <jwt>"
/api/v1/clientsList client users (end-users created on client websites) that are members of the session organization. Uses the shared `clients` collection.
| 401 | invalid_session |
curl https://www.selfiam.site/api/v1/clients -H "Authorization: Bearer <jwt>"
/api/v1/account/profileUpdate your own username, email, or phone number. Uniqueness is enforced; an activity event is logged and a notification email is sent.
| 400 | validation_error |
| 401 | invalid_session |
| 409 | identity_conflict — username/email already taken |
| 429 | email_limit_exceeded — org daily email budget exhausted |
curl -X PATCH https://www.selfiam.site/api/v1/account/profile \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{ "username": "john_new" }'/api/v1/account/passwordChange your password. Every OTHER session is revoked immediately (the calling session survives) and a notification email is sent.
| 400 | validation_error |
| 401 | invalid_credentials — current password wrong |
| 401 | invalid_session |
curl -X POST https://www.selfiam.site/api/v1/account/password \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{ "currentPassword": "...", "newPassword": "..." }'/api/v1/account/email/change · /api/v1/account/email/confirmTwo-step email change: `change` sends a 6-digit code to the NEW address (the current address stays authoritative); `confirm` applies the change only after the code is verified.
| 400 | validation_error / otp_error (wrong ref+code pairing) |
| 401 | invalid_session / otp_error |
| 403 | email_otp_not_enabled — org has verification disabled |
| 409 | identity_conflict — new email already registered |
| 429 | email_limit_exceeded — daily budget exhausted (nothing changed) |
| 502 | otp_delivery_failed — provider failure, nothing changed |
curl -X POST https://www.selfiam.site/api/v1/account/email/change \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{ "newEmail": "new@selfiam.example" }'/api/v1/account/eventsYour own activity feed ("login board"): newest-first list of self-service changes with what changed, when, and whether the notice email was delivered. IP hashes never leave the server.
| 401 | invalid_session |
curl https://www.selfiam.site/api/v1/account/events -H "Authorization: Bearer <jwt>"
/api/v1/account/email-logsOutbound email delivery log for the session organization. Admins (and super-admins) see every email; members only email addressed to their own account. Cursor-paginated newest-first. Delivery metadata only — codes and bodies are never logged.
| 401 | invalid_session |
| 403 | forbidden — not a member of the session org |
curl https://www.selfiam.site/api/v1/account/email-logs -H "Authorization: Bearer <jwt>"
/api/v1/clients/unamehashVerify a client user's username + password/QuickHash. Mirrors the legacy IAM route against the shared `clients` collection, scoped to the calling organization.
| 400 | username_error / password_error — missing headers |
| 401 | password_error — bad credentials |
| 404 | username_error — no such client user in this org |
| 401 | invalid_api_key |
| 403 | route_forbidden |
| 429 | rate_limit_exceeded |
curl https://www.selfiam.site/api/v1/clients/unamehash \ -H "Authorization: Bearer org_live_..." \ -H "X-Username: jane" -H "X-Password: SuperSecret!23"
/api/v1/clients/emailhashEmail variant of the client verification endpoint — validates `X-Email` + password/QuickHash.
| 400 | email_error / password_error |
| 401 | password_error — bad credentials |
| 404 | email_error — no such client user |
| 401 | invalid_api_key |
| 403 | route_forbidden |
| 429 | rate_limit_exceeded |
curl https://www.selfiam.site/api/v1/clients/emailhash \ -H "Authorization: Bearer org_live_..." \ -H "X-Email: jane@example.com" -H "X-Password: SuperSecret!23"
/api/v1/clients/whatsapp-otpRequest or verify a 6-digit WhatsApp OTP for a client user (requires a linked `phoneNumber`). Codes are stored SHA-256-hashed with a 5-minute TTL and are single-use.
| 400 | username_error / otp_error — missing input |
| 422 | otp_error — no phone number linked |
| 401 | otp_error — invalid/expired/used code |
| 404 | username_error — unknown client user |
| 401 | invalid_api_key |
| 403 | route_forbidden |
| 429 | rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/clients/whatsapp-otp \
-H "Authorization: Bearer org_live_..." -H "X-Username: jane" \
-H "Content-Type: application/json" -d '{ "action": "request" }'/api/v1/unamehashOriginal username + password/QuickHash verification against the legacy `users` collection.
| 400 | username_error / password_error |
| 401 | password_error |
| 404 | username_error |
| 429 | rate_limit_exceeded |
curl https://www.selfiam.site/api/v1/unamehash \ -H "Authorization: Bearer org_live_..." \ -H "X-Username: john_doe" -H "X-Password: CorrectHorseBatteryStaple1!"
/api/v1/emailhashEmail-based password/QuickHash verification against legacy `users`.
| 400 | email_error / password_error |
| 401 | password_error |
| 404 | email_error |
| 429 | rate_limit_exceeded |
curl https://www.selfiam.site/api/v1/emailhash \ -H "Authorization: Bearer org_live_..." \ -H "X-Email: john@selfiam.example" -H "X-Password: CorrectHorseBatteryStaple1!"
/api/v1/whatsapp-otpOriginal WhatsApp OTP request/verify against legacy `users`.
| 400 | username_error / otp_error |
| 401 | otp_error |
| 422 | otp_error — no phone linked |
| 404 | username_error |
| 429 | rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/whatsapp-otp \
-H "Authorization: Bearer org_live_..." -H "X-Username: john_doe" \
-H "Content-Type: application/json" -d '{ "action": "request" }'/api/v1/contact/messagesAccept a contact-form submission and persist it to the `messages` collection. This is the endpoint the `ContactForm` widget posts to.
| 400 | validation_error / invalid_payload |
| 401 | invalid_api_key |
| 403 | route_forbidden |
| 429 | rate_limit_exceeded |
curl -X POST https://www.selfiam.site/api/v1/contact/messages \
-H "Authorization: Bearer org_live_..." -H "Content-Type: application/json" \
-d '{ "name": "Jane Doe", "email": "jane@example.com", "subject": "Pricing", "message": "Do you offer a free tier?" }'/api/v1/orgPublic organization profile (name, slug, allowedRoutes) used for top-bar branding.
| 404 | organization_error — no such slug |
curl "https://www.selfiam.site/api/v1/org?organizationSlug=self-iam"