Using the API¶
Everything the interface does goes through this API: there is no write path reserved for the screens that would escape permissions or auditing.
The exhaustive reference, generated from the code, lives on /api/docs
(Swagger) and /api/redoc. This document covers what Swagger does not say:
scope, rights, and the sequences that are genuinely useful.
Conventions used here¶
BASE=https://backup.mssp.example.net
TOKEN=… # session token
CUSTOMER=… # a customer's UUID
DEVICE=… # a device's UUID
Getting a token¶
TOKEN=$(curl -s -X POST "$BASE/api/auth/login" \
-H 'Content-Type: application/json' \
-d '{"email":"ops@mssp.example.net","password":"…"}' | jq -r .token)
The token is presented in an Authorization: Bearer $TOKEN header. It lasts
BKP_SESSION_TTL_MINUTES minutes (480 by default).
To check who you are, and with what scope:
Scope: the tenant parameter¶
This is the point to understand before all the rest.
| Caller's role | Without ?tenant= |
With ?tenant=<uuid> |
|---|---|---|
platform_* |
every customer | that customer only, and the access is marked “cross-customer” in the audit |
tenant_* |
its own customer | its customer; 403 if it names another one |
Writes require a named customer: creating a device without knowing whose it is
makes no sense, and the API answers 400 saying so.
The scope is never deduced from the request body: it comes from the identity and the parameter. That is what makes forgetting a filter impossible.
Rights¶
Every endpoint requires a permission; see the matrix in Accounts. The most common ones:
| Permission | Minimum role | Example endpoints |
|---|---|---|
device:read |
tenant_readonly |
inventory, search, platforms |
config:read |
tenant_readonly |
artifacts, versions, comparisons |
report:read |
tenant_readonly |
reports, runs, effective retention |
config:download |
tenant_operator |
downloading a version, an archive |
backup:trigger |
tenant_operator |
triggering, testing, cancelling |
device:write |
tenant_admin |
creating and editing devices, imports |
credential:write |
tenant_admin |
credentials, fingerprint approval |
retention:write |
tenant_admin |
retention, trust postures |
audit:read |
tenant_admin |
audit log, storage compliance |
agent:manage |
platform_operator |
agents, enrolment tokens |
tenant:manage |
platform_admin |
customers, settings, global exports |
purge:force |
platform_admin |
key destruction, storage purge |
Response codes¶
| Code | What it means here |
|---|---|
200 / 201 |
success |
202 |
job accepted and queued — it has not run yet |
400 |
incoherent request: customer not named, versions from different lineages, unknown cascade level |
401 |
token absent, expired or invalid |
403 |
the role lacks the permission, or aims at another customer. The message names what is missing |
404 |
unknown object — or invisible from your scope, which is indistinguishable by design |
405 |
method not allowed: the case of a change made deliberately impossible, such as an agent's customer |
409 |
state conflict: deleting an agent that is not revoked, testing a device with no agent online |
413 |
content too large for the operation requested — the message distinguishes preview from comparison |
422 |
field value refused: invalid cron expression, malformed fingerprint, short name not conforming |
503 |
external dependency unreachable — the object store, in particular |
A 403 always carries a usable reason, never an opaque refusal.
Retrying safely: Idempotency-Key¶
A request that triggers work can fail on the way back. The job started, the answer never arrived, and the caller has no way to tell that from a job that never started at all. Retrying then triggers the work twice; not retrying leaves it undone.
Send an Idempotency-Key header on POST /api/backups/trigger and the question
disappears. The first call does the work and remembers what it created; a repeat
with the same key returns the same jobs, with the same identifiers, without
starting anything new.
KEY=$(uuidgen)
curl -s -X POST "$BASE/api/backups/trigger?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' -d '{"all_devices": true}'
Reusing a key for a different request is refused with 409: the key
identifies one operation, and silently accepting a second one under the same
name would make the first irretrievable. Pick a fresh key per intent — a UUID
does the job.
Keys are kept for seven days, which is far longer than any retry worth making. The jobs themselves keep their identity for good, so a scheduled run can never be queued twice for the same occurrence even if two schedulers overlap.
Recipes¶
1. Take stock of what is going wrong¶
The daily question. The report in exception view answers it in one call:
curl -s "$BASE/api/reports/daily?tenant=$CUSTOMER" -H "Authorization: Bearer $TOKEN" \
| jq '.summary, (.exceptions[] | {name, state, last_success_age_label, last_error})'
Without ?tenant=, the same request returns the consolidated view of every
customer, grouped by customer.
For collection failures alone, finer-grained:
curl -s "$BASE/api/runs?tenant=$CUSTOMER&failed_only=true&limit=50" \
-H "Authorization: Bearer $TOKEN" | jq '.[] | {device_id, status, error, queued_at}'
2. Trigger a bulk backup¶
# A customer's whole fleet
curl -s -X POST "$BASE/api/backups/trigger?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"all_devices":true}'
# The FortiGates only
curl -s -X POST "$BASE/api/backups/trigger?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"all_devices":true,"platform":"fortinet_fortios"}'
Answers 202 with the jobs created. A device that already has a pending job does
not receive a second one: the response can therefore be shorter than the fleet.
3. Fetch a device's latest configuration¶
Three calls: the lineages, the versions, the content.
ARTIFACT=$(curl -s "$BASE/api/devices/$DEVICE/artifacts?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.[] | select(.key=="running-config" and .transport=="ssh") | .id')
VERSION=$(curl -s "$BASE/api/artifacts/$ARTIFACT/versions?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')
curl -s "$BASE/api/artifacts/versions/$VERSION/content?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" > running-config.cfg
The filter on transport matters: a device can carry two lineages for the same
artifact key, one per access path.
4. Compare two versions¶
curl -s -G "$BASE/api/artifacts/$ARTIFACT/diff" \
--data-urlencode "from=$VERSION_A" --data-urlencode "to=$VERSION_B" \
--data-urlencode "tenant=$CUSTOMER" -H "Authorization: Bearer $TOKEN" \
| jq -r '.unified'
And to find where to look — the largest transitions:
curl -s "$BASE/api/devices/$DEVICE/divergence?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" \
| jq '.ecarts[] | {artifact_key, to_at, added, removed}'
5. Export a customer's inventory¶
curl -s "$BASE/api/tenants/$CUSTOMER/export" -H "Authorization: Bearer $TOKEN" \
> acme.json
curl -s "$BASE/api/devices/export?tenant=$CUSTOMER&format=csv" \
-H "Authorization: Bearer $TOKEN" > acme-fleet.csv
No secret appears in them; credentials and agents are cited by name, which makes the export re-importable on another instance.
6. Onboard a complete customer¶
The onboarding sequence, from the customer to the enrolment token:
# 1. The customer
CUSTOMER=$(curl -s -X POST "$BASE/api/tenants" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"slug":"acme","name":"ACME Industries"}' | jq -r .id)
# 2. Its agent
AGENT=$(curl -s -X POST "$BASE/api/agents?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"slug":"agent-paris","name":"Agent Paris DC1"}' | jq -r .id)
# 3. The enrolment token — shown once only
curl -s -X POST "$BASE/api/agents/$AGENT/enrollment-token?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" | jq -r '.token, .expires_at'
# 4. A credential
CRED=$(curl -s -X POST "$BASE/api/credentials?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"netops","username":"netops","password":"…"}' | jq -r .id)
# 5. A device
curl -s -X POST "$BASE/api/devices?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\":\"par-core-sw-01\",\"hostname\":\"10.20.1.2\",
\"platform\":\"cisco_iosxe\",\"schedule\":\"0 2 * * *\",
\"credential_id\":\"$CRED\"}"
7. Approve the pending fingerprints¶
A device stuck on an unapproved fingerprint is not backed up. To work through the batch:
curl -s "$BASE/api/trust/fingerprints?pending_only=true" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.[] | "\(.id) \(.kind) \(.fingerprint)"'
curl -s -X POST "$BASE/api/trust/fingerprints/$FINGERPRINT/approve?tenant=$CUSTOMER" \
-H "Authorization: Bearer $TOKEN"
Approving without checking is of no interest whatsoever. Compare the fingerprint with what the device itself displays — that is the entire point of the mechanism. See Device identity.
8. Sample the storage usage¶
# Consolidated, with the breakdown per customer
curl -s "$BASE/api/storage/usage" -H "Authorization: Bearer $TOKEN" \
| jq '{stored, growth_per_day, basis, full_at, disk,
by_tenant: [.by_tenant[] | {slug, stored}]}'
# Force a sample rather than waiting for the daily pass
curl -s -X POST "$BASE/api/storage/sample" -H "Authorization: Bearer $TOKEN"
basis says what the trend rests on: measured (slope of the samples),
transferred (transfers corrected by the ratio) or insufficient. Monitoring
that displays growth_per_day without displaying basis gives a false
impression of precision.
9. Extract the audit log for a period¶
# Access review: who read which customer's data
curl -s "$BASE/api/audit?cross_tenant_only=true&limit=5000" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.[] | [.ts, .actor, .role, .action, .object] | @tsv'
# Filter on an action
curl -s "$BASE/api/audit?action=config.download&limit=1000" \
-H "Authorization: Bearer $TOKEN"
Filtering by date is done on the caller's side: the endpoint returns the most recent events, up to 5,000.
10. Feed monitoring or an AI agent¶
The JSON report is designed for that: one call, the whole state of the fleet.
curl -s "$BASE/api/reports/daily" -H "Authorization: Bearer $TOKEN" \
| jq '{day: .day,
fleet: .summary.devices,
exceptions: .summary.exceptions,
coverage: .summary.coverage_pct,
lab: [.agents[] | select(.lab_mode) | .name],
to_handle: [.exceptions[] | {name, state, last_error}]}'
Three precautions for an automatic consumer:
lab_modeis an alarm, not a detail: an agent in lab mode produces backups that correspond to no real device.unchangedis a success. Counting “changes” as successful backups seriously understates coverage.- Freeze the report (
POST /api/reports/daily/persist) if the figure has to stand up later: a subsequent recomputation would run over data that has moved.
11. Check the state of the platform¶
Without authentication:
curl -s "$BASE/health" # the process answers
curl -s "$BASE/ready" # the database answers AND isolation is active
/ready is the one to wire into a probe: it refuses to declare the platform
ready if an expected table is not under an isolation policy.