Provider console API
In short
Section titled “In short”The provider console has an API of its own, separate from the customer API in every way: a different listener, a different base path, a different session cookie, and no API keys. The console’s web app is built on it. It exists only on a hosted installation with the console switched on.
| Listener | SP_PROVIDER_ADDR, never the customer port |
| Base path | /provider/api/v1 |
| Authentication | A staff session cookie, set by signing in. No API keys: any request with an Authorization header is answered 401 |
| OpenAPI description | GET /provider/api/v1/openapi.json (and .yaml). There is no interactive documentation page |
| Scope | One region. On a multi-region service each region’s console answers for its own workspaces |
Nothing in this API returns what a workspace holds: no customers, transactions, alerts, cases or reports.
Signing in and sessions
Section titled “Signing in and sessions”| Endpoint | Does | Answers |
|---|---|---|
POST /auth/login |
Signs in with email, password and, once enrolled, totp_code (six digits) |
200 with the session and a Set-Cookie; 401 with invalid_credentials (wrong details or a wrong code), totp_required (send the code) or temporary_password_expired; 429 when rate limited |
GET /auth/session |
The signed-in member of staff | 200 |
POST /auth/logout |
Signs out | 204 and a cookie that clears the session |
POST /auth/password |
Changes the password (current, new) and signs out the other sessions |
204; 400 if current is wrong; 422 if the new one is too short or the same |
POST /auth/totp/setup |
Starts second-factor enrolment, after the temporary password was changed. Answers the secret once | 200; 403 while the temporary password is unchanged; 409 if already set up |
POST /auth/totp/verify |
Finishes enrolment with a code |
200 with the session; 400 for a wrong code; 429 after 10 tries in 15 minutes |
The session object:
| Field | Meaning |
|---|---|
staff_id, email, name, role |
Who is signed in. role is owner, operations, support, billing or viewer |
permissions |
What the role may do |
mfa |
The second factor was verified for this session |
must_change_password, mfa_enrol_required |
What still has to be done before the account works |
restricted |
true until both are done: only the sign-in endpoints work |
csrf_token |
Send it as X-CSRF-Token on every request that is not a GET |
idle_timeout_seconds, expires_at |
The idle timeout, and the session’s absolute end |
Rules for every request
Section titled “Rules for every request”- Requests that change something must come from the console’s own origin (the
OriginorSec-Fetch-Siteheader is checked) and carry the session’sX-CSRF-Token. Otherwise403. Signing in is the one exception to the token. - A session that is
restrictedgets403complete account setup first (change the temporary password, then enrol two-factor) everywhere else. - A role without the permission gets
403your role does not allow this (needs …). The check runs before the request body is read. - From outside
SP_PROVIDER_ALLOWED_CIDRS, everything but/healthzis403not allowed from this network. - Request bodies are limited to 1 MB. Nothing is cached (
Cache-Control: no-store).
| Status | Meaning |
|---|---|
401 |
Not signed in, or the session ended |
403 |
The role doesn’t allow it, the account isn’t set up, dual control forbids it, or a workspace’s policy refuses a support request |
404 |
No such workspace, request, notice or member of staff |
409 |
The state doesn’t allow it (already decided, already scheduled, last owner, your own account) |
422 |
The request itself is wrong |
429 |
Too many attempts |
System
Section titled “System”| Endpoint | Does | Needs |
|---|---|---|
GET /system/info |
The build, the region, the environment, whether scheduled deletions run by themselves (purge_enabled), and what each role may do |
Any member of staff |
Workspaces
Section titled “Workspaces”| Endpoint | Does | Needs |
|---|---|---|
GET /workspaces |
Lists workspaces. Query: search, status (active, suspended, closed), plan, limit (up to 100, default 50), offset. Also answers total, the plans in use and how many were deleted |
workspaces.view |
POST /workspaces |
Creates a workspace and its first administrator. Answers 201 with workspace_id, admin_email and a temp_password shown once |
workspaces.create |
GET /workspaces/deleted |
What is kept of deleted workspaces | workspaces.view |
GET /workspaces/{id} |
A workspace’s summary | workspaces.view |
GET /workspaces/{id}/health |
Health in numbers | workspaces.view |
GET /workspaces/{id}/usage |
Usage per calendar month, and AI assistant calls and tokens. Query: months, 1 to 24, default 6 |
usage.view |
GET /workspaces/{id}/plan |
The plan, limits and staff notes | plans.view |
PUT /workspaces/{id}/plan |
Changes them | plans.manage |
POST /workspaces/{id}/ai-topups |
Adds AI assistant calls for the current calendar month, unasked or in answer to a request | plans.manage |
POST /workspaces/{id}/ai-requests/{request_id}/decline |
Declines a workspace’s request for more AI calls. Answers 204 |
plans.manage |
Fields of a workspace in the list:
| Field | Meaning |
|---|---|
id, slug, name, kind, country, status, created_at |
The workspace |
region, move_state |
Its region, and a move in progress |
plan |
The plan’s name |
user_count |
People who can sign in (active and invited) |
last_activity_at |
When someone last signed in |
delete_after |
Set when deletion is scheduled |
over_limit |
Over an agreed limit: people, or transactions this calendar month |
ai_request_pending |
true when the workspace has asked for more AI assistant calls this month and nobody has answered; left out otherwise. An expired request from an earlier month doesn’t count |
events_percent, users_percent |
Usage as a percentage of each agreed number, rounded down; absent when none was agreed |
GET /workspaces/{id} adds base_currency, timezone, jurisdiction, active_user_count,
suspended_at, suspended_reason, deletion_requested_at and, when deletion is scheduled,
deletion_state (scheduled, due, awaiting_manual_offboard or blocked) and
deletion_blocked_by (not_suspended, moving, legal_hold, no_dual_control or
reports_not_exported). See
Scheduled deletion.
A plan:
{ "plan": "standard", "limit_monthly_events": 2000000, "limit_users": 25, "limit_monthly_ai_calls": 500, "notes": "Renewal in March."}Leave a limit out for “none agreed”. limit_monthly_ai_calls is the workspace’s monthly allowance of
AI assistant calls on the installation’s key, and unlike the other two it is enforced: left out,
the installation’s default (SP_AI_MONTHLY_CALLS) applies; 0 means the installation’s key is not
in the plan. PUT replaces the whole plan, so a request that leaves the field out clears an
agreed allowance. See Plans and limits.
GET /workspaces/{id}/usage also answers limit_monthly_ai_calls and ai: one object per month,
model and key, with month (2026-10), model, key_source (installation, billable, or own,
the workspace’s own key), calls, input_tokens and output_tokens, added up over what the
assistant was asked to do. Counts only.
It also answers what the More AI calls panel shows:
| Field | Meaning |
|---|---|
ai_requests |
The workspace’s latest requests for more calls, newest first, at most 20: id, month (the month it was asked in), calls, note (the workspace’s words), status (pending, granted, declined, or expired for one left unanswered when its month ended; worked out when read, nothing is written), and once answered granted_calls, decided_by (the member of staff), decided_at and decision_note. Never who in the workspace asked |
ai_topups |
Calls added for the months shown, newest first: id, month, calls, note (staff’s), request_id when it answered a request, granted_by, created_at |
ai_topup_this_month |
The calls added for the current calendar month, on top of the agreed allowance |
Adding calls and answering requests
Section titled “Adding calls and answering requests”POST /workspaces/{id}/ai-topups adds calls to the workspace’s allowance on the installation’s key
for the current calendar month (UTC). They are available at once and do not carry over.
{ "calls": 200, "note": "Order 2026-115", "request_id": "01a133e9-4c63-74a3-9e24-3522780eb136"}| Field | Rule |
|---|---|
calls |
1 to 1,000,000 |
note |
Optional, up to 1,000 characters. With request_id it becomes the answer shown to the workspace; without, it stays in the console |
request_id |
Optional. The pending request this answers; it is marked granted with calls as the number granted |
It answers 200 with the top-up (id, month, calls, note, request_id, granted_by,
created_at). POST /workspaces/{id}/ai-requests/{request_id}/decline takes {"note": "…"}
(optional, shown to the workspace) and answers 204.
| Status | When |
|---|---|
403 |
your role does not allow this (needs plans.manage) |
404 |
No such workspace, or no such request for this workspace |
409 |
this request has already been answered; on a top-up also this request was for an earlier month and has expired; add the calls without it (a request_id whose month has ended), this workspace’s plan does not include the assistant on the installation’s key; change the plan before adding calls (the plan’s limit_monthly_ai_calls is 0) and this workspace is closed |
422 |
calls must be between 1 and 1,000,000, or the note can be at most 1000 characters |
Both write to the console’s audit log (workspace.ai_topup, workspace.ai_request_declined) and
to the workspace’s (provider.ai_topup, provider.ai_request_declined), and notify the
workspace’s administrators. Nothing checks that the workspace has a limit to add to: see
Adding calls for a month.
Creating a workspace takes slug, name, kind (psp, fintech, bank, mobile_money,
microfinance, other), admin_email, and optionally country, base_currency, timezone,
jurisdiction, admin_name and plan. plan is an object like the one above, not a name; inside it
plan and notes must be present (notes may be empty), as in PUT /workspaces/{id}/plan. A warning in the answer means the workspace exists but
something around it needs attention, such as the global directory not listing it yet.
Change requests
Section titled “Change requests”Suspending, reactivating, scheduling a deletion and cancelling one take two people. See Suspending and deleting a workspace.
| Endpoint | Does | Needs |
|---|---|---|
GET /changes |
Lists requests. Query: status (pending, approved, rejected, cancelled, expired, or decided for everything no longer pending), workspace_id, mine, limit (up to 200), offset |
changes.view |
POST /changes |
Proposes a change. Answers 201. Changes nothing about the workspace |
changes.request |
GET /changes/{id} |
One request | changes.view |
POST /changes/{id}/approve |
Approves and applies it. Optional note. 403 for whoever proposed it |
changes.approve |
POST /changes/{id}/reject |
Rejects it. Optional note. 403 for whoever proposed it |
changes.approve |
POST /changes/{id}/cancel |
Withdraws your own request | changes.request |
{ "kind": "workspace.schedule_deletion", "workspace_id": "0198c1de-0000-7000-8000-000000000000", "reason": "Contract ended on 30 September; export handed over.", "deletion_days": 30}kind is workspace.suspend, workspace.reactivate, workspace.schedule_deletion or
workspace.cancel_deletion. reason is 10 to 2,000 characters. deletion_days is 30 to 3650 and
applies only to scheduling a deletion. A request lapses 72 hours after it is made (expires_at).
Notices
Section titled “Notices”| Endpoint | Does | Needs |
|---|---|---|
GET /notices |
Lists notices. Query: active (only those in force now), limit, offset |
notices.view |
POST /notices |
Posts one. Answers 201 |
notices.manage |
PUT /notices/{id} |
Changes its text, severity and dates. Its audience is fixed. 409 if it was withdrawn |
notices.manage |
POST /notices/{id}/withdraw |
Takes it down at once | notices.manage |
| Field | Meaning |
|---|---|
severity |
info, warning or critical |
title_en, body_en |
Required. Up to 200 and 4,000 characters |
title_fr, body_fr |
Both or neither. French falls back to English |
starts_at, ends_at |
Default: now, and until withdrawn |
workspace_id |
On create only. Absent: every workspace |
A workspace’s people read the notices addressed to them through the customer API,
GET /api/v1/notices. See Service notices.
Owners only (staff.manage), except the names list.
| Endpoint | Does |
|---|---|
GET /staff |
Lists staff: email, name, role, status (invited, active, disabled), whether the second factor is enrolled, last sign-in, and any lockout |
GET /staff/names |
Names and status only, for filters. Open to every member of staff and not recorded in the audit log |
POST /staff |
Invites someone (email, name, role). Answers 201 with a temp_password shown once |
PUT /staff/{id}/role |
Changes a role. Not your own, and not the last active owner’s |
POST /staff/{id}/deactivate |
Deactivates and ends their sessions |
POST /staff/{id}/reactivate |
Lets them sign in again |
POST /staff/{id}/reset-totp |
Removes their second factor, ends their sessions and answers a new temp_password |
POST /staff/{id}/reset-password |
Ends their sessions and answers a new temp_password. The second factor stays |
Every temporary password answer carries temp_password_valid_hours.
| Endpoint | Does | Needs |
|---|---|---|
GET /audit |
The console’s audit log, newest first | audit.view |
| Query | Meaning |
|---|---|
action |
An action or its prefix, such as workspace. or auth.login_failed |
workspace_id, staff_id |
Narrow to one |
before, before_id |
Page backwards: the at and id of the last entry seen |
after, after_id |
Page forwards: the entries that follow the newest one seen. Not together with before |
limit |
Up to 200, default 100 |
Each entry has id, at, staff_id, staff_email (cli for the command line), action,
workspace_id, workspace_slug, details, ip and request_id. Reading the log writes an
audit.view entry. See The console’s audit log for every action.
Support access
Section titled “Support access”| Endpoint | Does | Needs |
|---|---|---|
POST /workspaces/{id}/support-requests |
Asks a workspace for support access. Answers 201. Opens nothing |
support.request |
GET /support-requests |
Lists requests. Query: status, workspace_id, limit, offset |
support.view |
GET /support-requests/{id} |
One request | support.view |
POST /support-requests/{id}/cancel |
Withdraws your own pending request | support.request |
POST /support-requests/{id}/session |
Answers a one-time url into the customer app and when the link stops working (expires_at). Only for whoever made the request, while the grant runs |
support.request |
POST /support-requests/{id}/end |
Ends a running grant. Whoever asked for it, or an owner | support.request |
{ "reason": "Webhook deliveries to the core banking system fail since Tuesday; need to see the endpoint settings.", "ticket_ref": "SUP-4821", "scope": "configuration", "duration_minutes": 60}reason is 20 to 2,000 characters. scope is configuration (the default) or full_read.
duration_minutes is 15 to 480, default 60. A request is refused with 403 when the workspace’s
policy doesn’t take requests, doesn’t allow the scope, or allows less time.
A request’s status is pending, approved, denied, cancelled, expired, revoked or
ended; granted_scope and granted_minutes say what the workspace gave, which can be narrower
than what was asked; active is true while a session can be opened. See
Support access.