Skip to content

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:

curl -s "$BASE/api/auth/me" -H "Authorization: Bearer $TOKEN"

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_mode is an alarm, not a detail: an agent in lab mode produces backups that correspond to no real device.
  • unchanged is 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.