Authicall · API
Developer documentation
OpenID Connect for multi-product companies. One issuer, many products, one person per email. Products keep local profiles and store sub.
1. Overview
Authicall is an authorization server. Products never see passwords after migration. The browser authenticates on Authicall; the product receives an authorization code, then exchanges it for tokens.
| Layer | Who calls it | Auth |
|---|---|---|
OIDC (/authorize, /oauth/token, …) | Browser + product backend | client_id + PKCE / client_secret_post |
Management (/api/v2/*) | Product backend only | HTTP Basic or X-Client-Id / X-Client-Secret |
Dashboard (/admin) | Operators | Admin session cookie |
Issuer for this deployment: https://authicall.com
2. Quickstart
- Create an application and product in the dashboard.
- Verify every hostname that will receive callbacks or legacy-login POSTs.
- Configure the product with Authicall issuer and client credentials.
- Redirect the browser to
/authorizewith PKCE. - Exchange the code at
/oauth/token, then call/userinfo. - Link the local user by email and store
sub.
AUTHICALL_ISSUER=https://authicall.com AUTHICALL_CLIENT_ID=your-client-id AUTHICALL_CLIENT_SECRET=your-client-secret AUTHICALL_BASE_URL=https://app.example.com AUTHICALL_SESSION_SECRET=generate-a-long-random-session-secret
All callback, logout, origin, and legacy-login URLs must be HTTPS on a verified public host. Localhost, loopback, and private networks are rejected.
3. Test credentials
Use these when integrating against a local or seeded issuer. Production dashboard secrets stay in environment variables and are not printed here.
Dashboard
| Environment | URL | Password | |
|---|---|---|---|
| Local defaults | http://localhost:3000/admin |
admin@localhost |
changeme |
| This deployment | https://authicall.com/admin | ADMIN_EMAIL (env) |
ADMIN_PASSWORD (env) |
Demo end user
| Password | Notes | |
|---|---|---|
existing@example.com |
legacy-password |
First login can migrate through a verified legacy-login webhook, then Authicall stores Argon2. |
Demo product credentials (Management API)
Authenticate with HTTP Basic client_id:client_secret or headers X-Client-Id / X-Client-Secret.
| Product | client_id | client_secret |
|---|---|---|
| Product A | demo-product-a |
demo-product-a-secret |
| Product B | demo-product-b |
demo-product-b-secret |
curl -u demo-product-a:demo-product-a-secret \ "https://authicall.com/api/v2/users-by-email?email=existing@example.com"
Create real product credentials in the dashboard. Copy the client_secret when the product is created — it is shown once.
4. Discovery & keys
GET https://authicall.com/.well-known/openid-configuration GET https://authicall.com/.well-known/jwks.json
Use discovery for endpoint URLs. Do not hard-code token or JWKS paths if you can avoid it. Signing keys rotate; verify JWTs against JWKS.
| Purpose | Method | Path |
|---|---|---|
| Discovery | GET | /.well-known/openid-configuration |
| JWKS | GET | /.well-known/jwks.json |
| Authorize | GET | /authorize |
| Token | POST | /oauth/token |
| UserInfo | GET | /userinfo |
| Logout | GET | /v2/logout |
| Revoke | POST | /oauth/revoke |
| Introspect | POST | /oauth/introspect |
6. Token
Auth method: client_secret_post. Supported grants: authorization_code, refresh_token.
POST https://authicall.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &client_id=YOUR_CLIENT_ID &client_secret=YOUR_SECRET &code=… &redirect_uri=https://app.example.com/callback &code_verifier=…
Refresh:
grant_type=refresh_token &client_id=YOUR_CLIENT_ID &client_secret=YOUR_SECRET &refresh_token=…
Response includes access_token, id_token, and optionally refresh_token. Access tokens are bearer tokens for UserInfo.
7. UserInfo
GET https://authicall.com/userinfo Authorization: Bearer ACCESS_TOKEN
Returns OIDC claims for the authenticated user. Always prefer UserInfo (or a verified ID token) over trusting the frontend alone.
8. Logout
GET https://authicall.com/v2/logout?client_id=YOUR_CLIENT_ID&returnTo=https://app.example.com/
returnTo is accepted as an alias for post_logout_redirect_uri. The URL must be registered and on a verified HTTPS host.
9. Identity & claims
subis alwaysauth0|{id}(stable subject format).- Same email is the same person across every product in the tenant.
- Tokens may include issuer-namespaced claims:
{
"sub": "auth0|clx…",
"email": "user@example.com",
"email_verified": true,
"name": "User",
"https://authicall.com/workspace": {
"slug": "workspace",
"name": "Workspace",
"primary_domain": "app.example.com"
},
"https://authicall.com/product": {
"slug": "crm",
"name": "CRM",
"public_domain": "www.example.com",
"path": "/crm"
}
}
10. Universal Login
- Identifier first: email → Continue.
- Password → Continue.
- Consent once per application. Sibling products reuse the SSO session.
- Sign up:
/interaction/{uid}?screen=register. - Abort:
/interaction/{uid}/abort.
Products must not embed a password form for Authicall users. Redirect to /authorize.
11. Management API
Server-to-server only. Authenticate the product:
Authorization: Basic base64(client_id:client_secret) # or X-Client-Id: your-client-id X-Client-Secret: your-client-secret
Query-string and JSON-body secrets are rejected. A product credential can only see its own application.
| Method | Path | Body / query | Result |
|---|---|---|---|
| POST | /api/v2/users | { email, name?, email_verified? } | Provision by email. No passwords. |
| POST | /api/v2/users/import | { users: [{ email, name?, email_verified? }] } | Bulk import. No passwords. |
| GET | /api/v2/users-by-email?email= | email required | Array of matching users |
| GET | /api/v2/users?email= | email required | Same lookup; unscoped lists are forbidden |
| GET | /api/v2/users/:id | auth0|{id} or raw id | Single user |
| GET | /api/v2/workspaces | — | Calling product’s application only |
curl -u YOUR_CLIENT_ID:YOUR_SECRET \ "https://authicall.com/api/v2/users-by-email?email=user@example.com"
Provision response
{
"user_id": "auth0|…",
"sub": "auth0|…",
"email": "user@example.com",
"email_verified": true,
"name": "User",
"identities": [{ "connection": "Username-Password-Authentication", "provider": "auth0", "isSocial": false }]
}
12. Domain ownership
Before a host can receive OAuth callbacks, logout returns, or legacy-login POSTs, verify it:
- Add the host in the dashboard (Domains).
- Serve the exact token at
https://{host}/.well-known/authicall-domain-verification. - Click Verify. Authicall fetches over HTTPS, does not follow redirects, and rejects localhost / private IPs.
# Example challenge body (plain text, exact match) authicall-domain=…
13. Legacy login webhook
For existing product passwords, expose an HTTPS webhook on a verified host. Authicall POSTs the email and password once, signed with HMAC-SHA256, then stores an Argon2 hash.
POST https://app.example.com/sso/legacy-login
Content-Type: application/json
X-Sso-Timestamp: <unix-seconds>
X-Sso-Signature: hex(hmac_sha256(secret, timestamp + "." + email))
X-Client-Id: your-client-id
{ "email": "user@example.com", "password": "…", "client_id": "your-client-id" }
- Reject if timestamp age > 60 seconds or signature mismatches.
- Return
401/404on bad password. - On success return
{ "email", "name?", "email_verified?" }.
14. Product SDK
Use @sso/product in Node apps:
import { SsoProductClient, linkByEmail, legacyLoginHandler } from "@sso/product";
const sso = new SsoProductClient({
issuer: process.env.AUTHICALL_ISSUER!,
clientId: process.env.AUTHICALL_CLIENT_ID!,
clientSecret: process.env.AUTHICALL_CLIENT_SECRET!,
baseUrl: process.env.AUTHICALL_BASE_URL!,
});
app.get("/login", async (req, res) => {
const login = sso.randomLogin();
req.session.oidc = login;
res.redirect(await sso.authorizationUrl(login));
});
app.get("/callback", async (req, res) => {
const { profile } = await sso.handleCallback(
new URL(req.originalUrl, process.env.AUTHICALL_BASE_URL),
req.session.oidc,
);
const local = await linkByEmail(adapters, profile);
req.session.userId = local.id;
res.redirect("/");
});
15. Errors & limits
| Status | When |
|---|---|
| 400 | Missing email on user lookup, unverified callback host, invalid body, HTTP (non-HTTPS) URL |
| 401 | Missing/wrong product credentials; wrong Universal Login password |
| 404 | Unknown user_id |
Management errors use JSON: { statusCode, error, message }. Import payload limit is 2 MB.
16. Security rules (enforced)
- No localhost, loopback, or private hosts anywhere (callbacks, origins, logout, legacy login, domain verification).
- HTTPS only for product URLs.
- Product APIs are scoped to the calling application.
- Unscoped user listing is forbidden.
- Passwords are never accepted on provision/import.
- Client secrets are never accepted from query strings or JSON bodies.
- Legacy login does not follow redirects and times out in 4 seconds.
Machine-readable OpenAPI: /docs/openapi.json