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.

LayerWho calls itAuth
OIDC (/authorize, /oauth/token, …)Browser + product backendclient_id + PKCE / client_secret_post
Management (/api/v2/*)Product backend onlyHTTP Basic or X-Client-Id / X-Client-Secret
Dashboard (/admin)OperatorsAdmin session cookie

Issuer for this deployment: https://authicall.com

2. Quickstart

  1. Create an application and product in the dashboard.
  2. Verify every hostname that will receive callbacks or legacy-login POSTs.
  3. Configure the product with Authicall issuer and client credentials.
  4. Redirect the browser to /authorize with PKCE.
  5. Exchange the code at /oauth/token, then call /userinfo.
  6. 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

EnvironmentURLEmailPassword
Local defaults http://localhost:3000/admin admin@localhost changeme
This deployment https://authicall.com/admin ADMIN_EMAIL (env) ADMIN_PASSWORD (env)

Demo end user

EmailPasswordNotes
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.

Productclient_idclient_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.

PurposeMethodPath
DiscoveryGET/.well-known/openid-configuration
JWKSGET/.well-known/jwks.json
AuthorizeGET/authorize
TokenPOST/oauth/token
UserInfoGET/userinfo
LogoutGET/v2/logout
RevokePOST/oauth/revoke
IntrospectPOST/oauth/introspect

5. Authorize

Authorization Code + PKCE only. response_type=code and code_challenge_method=S256 are required.

GET https://authicall.com/authorize?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://app.example.com/callback
  &response_type=code
  &scope=openid%20email%20profile%20offline_access
  &state=RANDOM
  &nonce=RANDOM
  &code_challenge=BASE64URL(SHA256(verifier))
  &code_challenge_method=S256
  &login_hint=user@example.com
ParameterRequiredNotes
client_idyesProduct client id from the dashboard
redirect_uriyesMust exactly match a registered HTTPS callback on a verified host
response_typeyescode only
scopeyesInclude openid; add email, profile, offline_access as needed
stateyesCSRF protection; validate on return
noncerecommendedBind to the ID token
code_challengeyesS256 PKCE
login_hintnoPre-fills Universal Login email

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

  • sub is always auth0|{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

  1. Identifier first: email → Continue.
  2. Password → Continue.
  3. Consent once per application. Sibling products reuse the SSO session.
  4. Sign up: /interaction/{uid}?screen=register.
  5. 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.

MethodPathBody / queryResult
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 requiredArray of matching users
GET/api/v2/users?email=email requiredSame lookup; unscoped lists are forbidden
GET/api/v2/users/:idauth0|{id} or raw idSingle user
GET/api/v2/workspacesCalling 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:

  1. Add the host in the dashboard (Domains).
  2. Serve the exact token at https://{host}/.well-known/authicall-domain-verification.
  3. 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 / 404 on 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

StatusWhen
400Missing email on user lookup, unverified callback host, invalid body, HTTP (non-HTTPS) URL
401Missing/wrong product credentials; wrong Universal Login password
404Unknown 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