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.
/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:
- Open the allus portal → Configuration → Services → your service → API access.
- Click Register API client and give it a name.
- The portal returns a
client_id(asvc_…id) and aclient_secretshown 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 choiceThen 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.
- Authenticate at
https://api.allme.fyi. The front door routes yourclient_credentialsgrant to your home region, which mints the token there. - Read
api_urloff the token response — the base of that home region, e.g.https://api-eu.allme.fyi. - 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..."
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.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:
GET/POST /oauth2/authorize— start the flow and submit credentials; redirects back to yourredirect_uriwith?code=…&state=….POST /oauth2/tokenwithgrant_type=authorization_code, thecode, and the matchingredirect_uri→ an access token plus a refresh token.
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
- Access tokens are valid for one hour (
expires_in: 3600). - Client-credentials tokens carry no refresh token — just request a fresh one with your credentials when the old one nears expiry.
- Authorization-code and refresh-token grants return a
refresh_token; exchange it withgrant_type=refresh_tokenfor a new access token (refresh tokens rotate on use).
401 before surfacing an AuthError. You only supply client_id/client_secret in config.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:
| Pattern | Grants |
|---|---|
/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.
__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.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
client_secretis shown once at registration and stored only as a keyed hash — keep it in a secret manager, never in source control. - Lost it? Every value the portal shows once — this secret, a service key passphrase, a webhook signing secret — is also saved as a copy sealed to your company's recovery key, a key only your account password opens; the platform cannot read it. Recover it in the portal under Settings → Recovery (your password plus an emailed or 2FA code), or on the card where the value was first shown. A value whose copy was not saved is gone.
- Or regenerate the secret on the portal's API access tab. Regenerating revokes all existing tokens for that client, so rotate it during a maintenance window.
- SDK config keeps secrets out of the file via
ALLUS_*env overrides (e.g.ALLUS_CLIENT_SECRET) — see the config reference.
The service private key (zero-knowledge)
Authentication gets you a token; decryption needs your service private key. They are separate concerns:
- Each service has its own RSA-2048 keypair, generated in your browser when you create the service. The public key and the private key — encrypted with a random passphrase — are uploaded; the passphrase is shown once and never reaches the platform in the clear.
- People who connect encrypt their shared field values to your service public key, so the platform only ever stores ciphertext for person data.
- You download the private key from the portal as an OpenSSL-compatible encrypted PKCS#8 PEM and decrypt the values off-platform with it (the SDKs do this for you — you point
service_private_key+key_passphraseat the PEM in config).
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: