Theme
Open the portal

Authentication

Register a service API client, exchange your credentials for a token at /oauth2/token, and understand the deny-all → whitelist model that scopes every call to your service.

The service API is machine-to-machine. There is no user login — your backend authenticates as a registered service API client using OAuth2, and every token it issues is implicitly scoped to the one service that client belongs to. This page covers creating that client, getting a token, refreshing it, the endpoint whitelist, and how the zero-knowledge service private key fits in.

New here? Read How it works for the mental model first, then follow the Quickstart — most developers never call /oauth2/token directly because an SDK manages the token for them. This page covers the per-service data client (reads person data); to automate your own account (services, profile, fields) over the API, see Account automation clients.

Register a service API client

Every service has at most one registered API client. You create it in the portal, on the service's API access tab:

  1. Open the allus portal → Configuration → Services → your service → API access.
  2. Click Register API client and give it a name.
  3. The portal returns a client_id (a svc_… id) and a client_secret shown exactly once — copy it now; it is stored server-side only as a keyed hash. The portal also saves a copy sealed to your company's recovery key, from which you can recover it later.

The client is created as a confidential OAuth client with grant_types = [client_credentials, authorization_code, refresh_token] and an editable redirect_uri (only needed for the authorization-code flow). A second registration attempt for the same service returns 409 services.client_exists.

See the portal configuration guides for the screenshot walkthrough, or drive registration over the portal API.

Get a token: client credentials (default)

The data API uses the client-credentials grant. POST your client_id/client_secret form-encoded to /oauth2/token and you get back a bearer token:

curl -s -X POST https://api-eu.allme.fyi/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=svc_1a2b3c... \
  -d client_secret=...
# → { "token_type": "Bearer", "expires_in": 3600, "access_token": "ey..." }

The same exchange from code (the SDKs do this for you and manage refresh):

import requests

resp = requests.post("https://api-eu.allme.fyi/oauth2/token", data={
    "grant_type": "client_credentials",
    "client_id": "svc_1a2b3c...",
    "client_secret": "...",
})
token = resp.json()["access_token"]
const resp = await fetch("https://api-eu.allme.fyi/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: "svc_1a2b3c...",
    client_secret: "...",
  }),
});
const { access_token } = await resp.json();
$resp = file_get_contents('https://api-eu.allme.fyi/oauth2/token', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'grant_type'    => 'client_credentials',
            'client_id'     => 'svc_1a2b3c...',
            'client_secret' => '...',
        ]),
    ],
]));
$token = json_decode($resp, true)['access_token'];
import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
)

resp, err := http.PostForm("https://api-eu.allme.fyi/oauth2/token", url.Values{
    "grant_type":    {"client_credentials"},
    "client_id":     {"svc_1a2b3c..."},
    "client_secret": {"..."},
})
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var body struct {
    AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
    log.Fatal(err)
}
token := body.AccessToken
fmt.Println("bearer token:", token)
using var http = new HttpClient();
var resp = await http.PostAsync("https://api-eu.allme.fyi/oauth2/token",
    new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["grant_type"]    = "client_credentials",
        ["client_id"]     = "svc_1a2b3c...",
        ["client_secret"] = "...",
    }));
var token = JsonDocument.Parse(await resp.Content.ReadAsStringAsync())
    .RootElement.GetProperty("access_token").GetString();
HttpClient http = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api-eu.allme.fyi/oauth2/token"))
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(
        "grant_type=client_credentials"
        + "&client_id=svc_1a2b3c..."
        + "&client_secret=..."))
    .build();
String json = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
// pull access_token out of the JSON with your JSON library of choice

Then send the token as a bearer header on every API call — addressed to the api_url the token response returned, not to the front door you authenticated at (Regions, below):

curl -s https://api-eu.allme.fyi/api/company-data/changes \
  -H "Authorization: Bearer ey..."

The token's client_id resolves server-side to your service (company_service_clients.client_id → service_id), so you never pass a service id — every request only ever sees your service's connections and data.

Regions: authenticate at the front door, then call your home region

The platform runs in several regions, and your company's data lives in exactly one of them — its home region. https://api.allme.fyi is the global front door: it is where you authenticate, and it is not a data endpoint.

  1. Authenticate at https://api.allme.fyi. The front door routes your client_credentials grant to your home region, which mints the token there.
  2. Read api_url off the token response — the base of that home region, e.g. https://api-eu.allme.fyi.
  3. Send every data call to that base, for as long as the token lives. Keep authenticating at the front door: it is what resolves your current home region, so a company that moves region is followed by the next token request.
curl -s -X POST https://api.allme.fyi/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=svc_1a2b3c... \
  -d client_secret=...
# → { "token_type": "Bearer", "expires_in": 3600, "access_token": "ey...",
#     "api_url": "https://api-eu.allme.fyi" }

# then target that base, not the front door:
curl -s https://api-eu.allme.fyi/api/company-data/connections \
  -H "Authorization: Bearer ey..."
A token is valid only at the region that minted it, and a data call to https://api.allme.fyi is not served — the front door refuses it with 421 Misdirected Request and error_key: "region.rebase_required", carrying the api_url you should have used:
{ "error": "Send this request to your home region",
  "error_key": "region.rebase_required",
  "api_url": "https://api-eu.allme.fyi" }
Treat that as a directive to re-target and retry once, not as a redirect to follow blindly: surface a second 421 as the error it is rather than retrying in a loop.
If you use an SDK, all of this is already done for you: it stores api_url off the token response and sends every subsequent request there — including the next token request, so a company that moves region is followed by the next mint — and on a 421 region.rebase_required it stores the returned base and retries the call once. You configure the front door as api_url and never think about it again.

The routes served at the front door are the ones that carry no region-bound state: POST /oauth2/token, GET /api/regions, GET /api/health, the OpenAPI documents and the Swagger UI, /.well-known/* and /oauth2/jwks. Everything under /api/company-data/* belongs to your home region.

An authorization_code exchange runs only through the front door. Addressed at a regional base, it is refused with the same 421 region.rebase_required, whose api_url names the front door; retry it there once. The client_credentials and refresh_token grants are answered at a regional base.

Authorization code + PKCE (optional)

The registered client also supports the authorization-code grant with PKCE, for the cases where you want a user to authorize the integration interactively. It uses the same two endpoints the apps use:

curl -s -X POST https://api-eu.allme.fyi/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d client_id=svc_1a2b3c... \
  -d client_secret=... \
  -d code=<AUTH_CODE> \
  -d redirect_uri=https://your-app.example/callback

Set the client's redirect_uri on the API access tab before using this flow. The client-credentials flow is the default and the one the SDKs use; reach for auth-code only if your integration genuinely needs a user-authorized session.

Token lifetime & refresh

If you use an SDK, token caching and refresh are automatic: the SDK fetches a token on the first call, caches it with its expiry, refreshes before it lapses, and retries exactly once on a mid-flight 401 before surfacing an AuthError. You only supply client_id/client_secret in config.
Cache the token — do not fetch a fresh one on every call. A client_credentials token request is on its own rate-limit bucket, generously sized and kept separate from person logins — but that bucket is keyed by source IP, not by your client_id, so it is shared with every other client_credentials caller reaching the API from the same address (a shared office network, a NAT gateway, several services on one host). A client that skips caching and requests a new token before every single API call spends that shared window on its own and can rate-limit itself and every other integration behind the same IP on /oauth2/token. Request one, hold onto it for its expires_in lifetime, and only refresh when it is close to expiring — exactly what every official SDK already does for you.

The deny-all → whitelist endpoint model

The API enforces a per-client endpoint whitelist (oauth_client_endpoint_access, checked by ClientEndpointChecker with fnmatch patterns). A registered service client is whitelisted for exactly the data API surface:

PatternGrants
/api/company-data/*The whole service data API (connections, changes, files, logs, request-fields).
/api/contact-fields*The field catalog, so you can resolve field ids → labels/types.
/api/contact-field-types*The field-type registry, so you can resolve what each of those types means — how a value is shaped, validated and stored. Its own pattern: neither it nor the catalog above matches the other.
/api/keys/*Public keys, so you can encrypt a per-person document to its recipient.
/api/service-2fa/*Challenge initiate + poll for 2FA by allme.

The token endpoint /oauth2/token is not part of the whitelist — it authenticates your client by its credentials. Every other endpoint returns 403 {"error":"Client not authorized for this endpoint","error_key":"auth.client_endpoint_forbidden"}. A registered client can never reach the portal API, the consumer-app API, or another service's data.

Why "deny-all". A client with no whitelist entries can call nothing — absent configuration denies. A client that should hold no endpoint at all carries an explicit __deny_all__ sentinel that matches no path. You never see either: by the time you have credentials, the patterns above are in place. Your client's reach is fixed to the data API and cannot be widened by you.
An older client and a surprise 403. The list above grows as the data API does, and a client only receives what existed when it was registered; the platform backfills the newer patterns onto existing clients when the API is updated. If a call that should work returns 403 auth.client_endpoint_forbidden, your client predates the pattern and has not been backfilled yet. Registering a fresh data client on the portal always yields the current list.

Secret handling

The service private key (zero-knowledge)

Authentication gets you a token; decryption needs your service private key. They are separate concerns:

The service key passphrase is shown once — store it securely. The portal saves a copy sealed to your company's recovery key, which only your account password opens; without that copy a lost passphrase is gone. The platform is zero-knowledge by design: it cannot decrypt person data, and it cannot open a stored copy for you.

Full crypto details — the {"_enc":1,…} wrapper and the PEM format — are in the Reference; the encryption model is explained in How it works.

Try it

Explore and call the auth + service endpoints interactively in the live API explorer:

Open the Service API explorer →