Advanced: Portal API
Everything you configure in the allus portal UI — services, request fields, what you share back, webhooks, API clients — is backed by an HTTP API. This page is for teams that prefer to manage that configuration as code instead of clicking through the portal.
Portal API vs. service API
allus exposes two distinct API surfaces. They use different OpenAPI specs, different auth, and never overlap. Both are region-bound in the same way: you authenticate at the global front door and address every authenticated call to the api_url the token response returned — see Regions.
| Portal API (this page) | Service API | |
|---|---|---|
| Purpose | Configure your services (the same thing the portal UI does) | Read auto-updating person data |
| Path prefix | /api/services/* (+ /api/contact-data, /api/contact-fields for the company profile) | /api/company-data/* and the rest of the client whitelist |
| Auth as | Your company portal login — the allus-client (OAuth authorization-code + PKCE) — or an account automation client (client_credentials) | A registered service API client (client_credentials) |
| Scope | All your company's services (owner-scoped — 404 for services you don't own) | Exactly one service (auto-scoped to the calling client) |
| OpenAPI spec | ?spec=portal | ?spec=service |
A registered service API client cannot call the portal API — its whitelist covers the service data patterns only, and none of them reach /api/services/*. Conversely the portal allus-client cannot read /api/company-data/*. They are deliberately separate.
Authenticating to the portal API
The portal API is authenticated as your company portal account through the public allus-client using the OAuth authorization-code + PKCE flow — the same flow the portal SPA runs on login. For scripted configuration, provision an account automation client instead: a client_credentials credential bound to your company account, granted the service_management capability for the routes on this page. Either way the calls act as your company user and are scoped to your company's services. See Authentication for the token mechanics.
Every /api/services/{id}/* route is owner-scoped: a service you don't own returns 404 (never a 403 that would leak its existence).
Managing services
A service is the unit a person connects to. Each has a name, an audience, a per-company share code, its own zero-knowledge keypair, plus request-field, share-field, webhook and client configuration.
| Method & path | Purpose |
|---|---|
GET /api/services | List your services: {id, name, share_code, audience, connection_count, created_at}. |
GET /api/services/{id} | Service detail + keys + audience, connection_count, pending_consent_count. |
GET /api/services/stats | Dashboard rollup: {total_connections, total_service_links, pending_consents}. |
PUT /api/services/{id}/audience | Set {audience: b2b | b2p | any} (default b2p). Persons can only connect to b2p/any services. |
PUT /api/services/{id}/share-code | Set {share_code} — uppercase [A-Z0-9], 1–8 chars, unique per company. 400 services.share_code_invalid / 409 services.share_code_taken. |
The full connect handle a person uses is COMPANYCODE/SERVICECODE — your company profile share code, a slash, the service share code. See How it works → connect handles.
Request fields (what you ask for)
Request fields declare the data a person provides when they connect. They are configured as a draft + publish set: you send the whole list, the server diffs it against the live config.
| Method & path | Purpose |
|---|---|
GET /api/services/{id}/request-fields | The current set: {fields: [...], config_token}. Each row carries provided_count / pending_consent_count for grandfather visibility. |
PUT /api/services/{id}/request-fields | Replace the whole set (draft+publish). Body: {fields: [...], config_token}. Answers consent_requests_created; the consent requests themselves land in the seconds after the response. |
Each request-field row:
{
"id": "…", // omit for a new row; present to update an existing one
"slug": "billing_email", // optional; ^[a-z0-9_]{1,40}$, derived from the label when omitted, stable after
"field_type": "email", // immutable once set
"suggested_field_id": "…", // optional advisory: a global person|any field of the same type
"label": "Billing email", // optional display label
"hint": "Where we send invoices", // optional helper text
"mandatory_provide": true, // hard gate — the person cannot connect without answering
"mandatory_connected": false, // must be a LIVE field (no share-once); implies mandatory_provide
"one_time": false, // a frozen snapshot; locked once values exist
"audience": "person", // person (default) | company | both — which customer type the row is asked of
"verified": false, // demand an answer from a verified field (person rows, never one_time)
"verified_max_age_days": null // optional: the verification may be at most this many days old
}A plugin row is "field_type": "plugin" with a plugin_spec {plugin_id, field_type, inputs}; it is always one-time and asked of people only. A refused plugin row answers 400 services.request_field_plugin_invalid with the reason in error.
An example publish call:
curl -X PUT "https://api-eu.allme.fyi/api/services/$SERVICE_ID/request-fields" \
-H "Authorization: Bearer $PORTAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config_token": "<from the GET>",
"fields": [
{ "field_type": "email", "label": "Billing email", "mandatory_provide": true },
{ "field_type": "phone", "label": "Support phone" }
]
}'How the diff is applied
- Known ids update cosmetics + flags (
field_typeis immutable;one_timecan't change once values exist). - New rows (no
id) are inserted. - Missing ids are removed; the provided values and pending consents they held are cleaned up shortly afterwards.
- Rows added on a live service trigger a consent fan-out: one merged consent request per existing connection plus a visible push to each person, delivered in the seconds after the response. The response includes
consent_requests_created. - The
config_tokenguards against concurrent edits: a stale token returns409 services.config_conflict. AlwaysGETfirst, send back the token you received.
mandatory_connected ⇒ mandatory_provide (auto-set). The suggested_field_id must be a global person|any field of the same type — it's only an advisory pre-selection on the connect screen. For the field-type model and which types may be requested, see Reference → Field types.Sharing back & the company profile
Share fields are what your service shares with everyone connected to it — stored as plaintext by design (so new connections see your support email, address, etc. instantly). The company profile is your company-level data, shared with every connection on any of your services.
| Method & path | Purpose |
|---|---|
GET /api/services/{id}/share-fields | Enabled fields + their plaintext master values. |
POST /api/services/{id}/share-fields | Enable a field: {field_id, value?, sort_order?}. The field must be a global company/any field; documents are blocked, photos are allowed (value via the photo endpoint). |
PUT /api/services/{id}/share-fields/{sfId} | Set the value (type-validated; 400 for photo rows). Pushes company_share_changed to linked persons. |
DELETE /api/services/{id}/share-fields/{sfId} | Stop sharing (deletes photo files). |
POST /api/services/{id}/share-fields/{sfId}/photo | Upload/replace a photo value: {image: base64 data URI ≤5MB, avatar?} → server resize. |
GET/POST/PUT/DELETE /api/contact-data | The company profile — reuses the standard contact-data endpoints (plaintext carve-out for users.type='company', with server-side type validation). Profile photos use POST/GET /api/photos/{fieldId}. |
Webhooks
Webhook endpoints are configured per service. The config is documented in detail on the Webhooks page (events, format, auth modes, the account-key envelope); here are the routes:
| Method & path | Purpose |
|---|---|
GET /api/services/{id}/webhooks | List configured webhooks (secrets masked). |
POST /api/services/{id}/webhooks | Create one. An omitted HMAC secret is auto-generated and returned once as hmac_secret_once. |
PUT /api/services/{id}/webhooks/{whId} | Update. Omitting a secret keeps the stored value. |
DELETE /api/services/{id}/webhooks/{whId} | Remove it (drops its parked backlog). |
curl -X POST "https://api-eu.allme.fyi/api/services/$SERVICE_ID/webhooks" \
-H "Authorization: Bearer $PORTAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/allus/webhook",
"events": [], // empty = all events
"format": "json",
"auth_type": "hmac", // hmac (default) | bearer | basic | header | none
"encrypt_payload": false,
"enabled": true
}'
# response includes "hmac_secret_once" — capture it; it is never shown againRegistered API clients
A service's machine-to-machine credentials (used to call the service API) are managed here. Exactly one client per service.
| Method & path | Purpose |
|---|---|
GET /api/services/{id}/clients | The registered client for the service (identity only — never the secret). |
POST /api/services/{id}/clients | Register one: {name}. Returns the client id + the secret shown once. A second client returns 409 services.client_exists. |
POST /api/services/{id}/clients/{clientId}/regenerate-secret | Rotate the secret (revokes existing tokens; new secret shown once). |
DELETE /api/services/{id}/clients/{clientId} | Delete the client (hard delete — removes tokens, whitelist, and the link). |
A registered client is confidential and supports two grants: client_credentials (the default for the data API) and authorization_code + PKCE (needs a redirect URI). Its reach is fixed to the service data patterns and cannot be widened by you. The pattern list itself, the full registration model, the deny-all sentinel, and how to obtain a token are on the Authentication page. To call this portal API (the surface on this page) from automation rather than the portal login, provision an account automation client with the service_management capability.
Service activity log
Each service has an operational log (never person data) — stale-queue email alerts and 7-day purge events:
| Method & path | Purpose |
|---|---|
GET /api/services/{id}/logs?offset&limit | Newest-first log entries (type = email | purge | webhook, a message, and type-specific metadata). Owner-scoped to the portal client; not available to the data client. |
The portal renders this as the Logs tab. Webhook state transitions and config changes also appear here.
Next steps
- Portal configuration guides — the illustrated, click-by-click version of everything above.
- Service API reference — reading the data your request fields collect.
- Reference — field types, error keys, the encryption spec.
- Account automation clients — drive everything on this page from a machine-to-machine credential bound to your company account.
- Open the portal API spec ↗