A small, explicit API.

The website, REST API and MCP server share one account. Backups run on your own private hosts: PostgreSQL credentials stay in each host's runner.json, and archives go to the storage you choose. Optional restore checks run in disposable containers on the host that made the backup.

Authentication

Routes are relative to /api/v1. Send JSON with Content-Type: application/json. Authenticate with a signed-in browser session or an API key from your dashboard: Authorization: Bearer bs_…. A bearer header always wins over cookies, even when it is invalid.

API keys can list sources, storage, backups, logs and restore checks, save sources and storage, create, run and remove backup plans, cancel jobs, and queue backups, connection checks, network checks and restore checks. Hosts, source links, encryption keys, reach settings, storage access and API keys themselves need the signed-in owner. Each route below says which applies.

Errors are JSON: { error, details? }. Common statuses: 400 invalid JSON, 401 not signed in or not allowed with a key, 403 a cross-site browser request or an account still on the waitlist, 404 not yours or not found, 409 a conflict such as a busy or unlinked host or a change that needs confirm: true, 413 too large, 415 wrong content type, 422 invalid fields, 503 not configured or not available. Send a UUID requestId when you queue work; retrying with the same one returns the same job.

From host to first backup

  1. Enroll a host. On Private hosts, or with POST /runners { action: "create", name }, get an enrollment code and install the runner on your server. The databases it may back up are the targets in its runner.json.
  2. Save the source. POST /sources with the example below.
  3. Link it. POST /source-runners { action: "bind", sourceId, runnerId, target }. You can also pass runnerId and target when you create the plan.
  4. Create a plan. POST /backup-plans names the storage and schedule. scheduleMinutes is 15 to 43200, or 0 for manual only.
  5. Run and poll. Run the plan, then poll GET /backups?sourceId=… and GET /backups/{id}/logs until the job finishes.
POST /api/v1/sources
{
  "name": "App database",
  "kind": "database",
  "engine": "postgres",
  "host": "db.example.test",
  "user": "backup",
  "database": "app",
  "storageMode": "hosted",
  "daily": 7, "weekly": 4, "monthly": 3
}
POST /api/v1/backup-plans
{
  "sourceId": "…",
  "name": "Nightly",
  "destinationIds": ["…"],
  "scheduleMinutes": 1440,
  "dumpStreams": 2
}

POST /api/v1/backup-plans
{ "action": "run", "id": "<plan id>", "requestId": "<uuid>" }

Plan storage is SSH or directory storage, up to eight; each backup is copied to each one. Bucket storage is set in the host's own runner.json. selection leaves schemas or tables out (runner 1.9.0 or later). dumpStreams is 1 to 8 (runner 1.10.0 or later). coordinatorId runs the plan on another host the source is available on. Only one backup of a database runs at a time; a second returns 409.

An API key can do all of this. POST /backups { sourceId } also queues a one-off backup on the source's main host, outside any plan, and the same polling applies. New storage starts unverified and becomes verified after a host reaches it in a storage check, a connection check or a committed copy.

Routes

Sources

GET/sources
List your sources, with each one's host link and execution state. API key or session
POST/sources
Save a source (see the example below). Saving does not connect to the database or start a backup. API key or session
PATCH/sources
{ id, patch } changes retention (daily, weekly, monthly) and notification preferences. Nothing else can be patched. API key or session
DELETE/sources
{ id, confirm? }. A source with backups, a schedule or running jobs needs confirm: true, and its queued and running jobs are cancelled. Session only
GET/sources/{id}
What removing the source would affect: schedule, backups, encryption keys, active jobs and copies. Session only
PATCH/sources/{id}
Rename with { name } (1 to 120 characters). Session only
GET/sources/{id}/encryption
Archive key history: key IDs, origin and how many backups use each. Never key material. Session only
POST/sources/{id}/encryption
{ action: "generate" }, { action: "set", key | passphrase } or { action: "reveal", keyId? }. Replacing a key in use needs confirm: true. Passphrase changes are rate limited (429). Session only
GET/sources/{id}/network
Address-family settings, the destinations its plans use, and its latest capability scans. API key or session
POST/sources/{id}/network
{ database?, jump? } sets the address family: auto, prefer-ipv6, prefer-ipv4, ipv6 or ipv4. Session only
PUT/sources/{id}/storages
{ destinationIds } replaces the SSH destinations this source copies each backup to, in order, up to eight. Session only

Private hosts

GET/runners
List your hosts with their approved targets, online state and recent jobs. Session only
POST/runners
{ action: "create", name } returns a one-time enrollmentCode valid for 30 minutes. Also rename { id, name }, revoke { id }, remove { id, confirm? } and host_impact { id }. Schedules belong to backup plans; action "schedule" returns 409. Session only
GET/managed-hosts
Regions where BackupSanity runs hosts for you, with the price per started hour. Needs no key. API key or session
POST/runners
{ action: "create_managed", name, region } creates a managed host (a card on file is required). { action: "set_managed_target", id, alias, engine? ("postgres" or "mysql", kept once saved), address, port?, database, user, tlsName?, password? } saves a database it backs up; the password is encrypted and never returned (leave it out to keep the saved one). { action: "remove_managed_target", id, alias }. Session only
POST/source-runners
{ action: "bind", sourceId, runnerId, target } links a PostgreSQL source to its main host. The target must be listed in that host's runner.json. Session only
POST/source-runners
{ action: "add-host" | "remove-host", sourceId, runnerId, target? } makes the source available on another host that lists the same database. A plan can then run there. A host that plans still run on cannot be removed (409). Session only
POST/source-runners
{ action: "probe", sourceId, requestId } queues a connection check on the main host. API key or session
POST/source-runners
{ action: "restore-policy", sourceId, mode: "off" | "daily" | "every_backup" } or { action: "compression", sourceId, compression: "auto" | "none" | "lz4" | "zstd:N" }. Session only
GET/target-catalog
?runnerId=…&target=… returns the last schema and table scan of a host target. Session only
POST/target-catalog
{ runnerId, target } asks the host to scan it (runner 1.9.0 or later). Runs beside a backup in progress. Session only
POST/runner-agent
The host daemon's own channel (enroll, poll, renew, complete, catalog), authenticated with enrollment or runner tokens. Not for API clients. Host credential

Backup plans and backups

GET/backup-plans
List plans with schedule, storage, the host each runs on, and the last backup. API key or session
POST/backup-plans
Create, or update with id: { sourceId, name, destinationIds, scheduleMinutes, firstRunAt?, enabled?, runnerId?, target?, coordinatorId?, selection?, dumpStreams? }. See the walkthrough. API key or session
POST/backup-plans
{ action: "run", id, requestId? } queues a manual run on the plan's host. { action: "restart", id } cancels the database's current backup and runs the plan again. API key or session
DELETE/backup-plans
{ id } removes the plan and its schedule. Existing backups are kept. API key or session
GET/backups
Up to 100 recent backup jobs (plan, manual and scheduled) with status and the host's reported result. Optional ?sourceId=… API key or session
POST/backups
{ sourceId, requestId? } queues a backup on the source's main host, outside any plan. Returns the job, not a finished backup. API key or session
GET/backups/{id}/logs
Log lines the host wrote for one job. Follow a running job with ?after=<last line id>. API key or session
POST/backups/{id}/cancel
Stop a queued or running job. A running host stops within about 15 seconds and keeps nothing from that run. API key or session

Storage and network

GET/destinations
List storage with its probeStatus and usage, plus the access key and host-key status of SSH storage. API key or session
POST/destinations
{ name, vendor, bucket, place? }. For vendor "ssh", place is ssh://user@host[:port] and bucket an absolute directory. Bucket vendors (r2, b2, s3, s3compat and others) name storage a host writes to with credentials in its own runner.json. API key or session
GET/hosted-storage
Hosted storage this deployment offers: each provider with its regions, price per GB-month (and per GB read back, for S3) and terms. Needs no key. API key or session
POST/destinations
{ vendor: "hosted", provider, region, name? } adds hosted storage: a folder of the account's own in storage BackupSanity runs. Each job's host gets credentials for that folder alone, expiring with the job (runner 1.17.0 or later). Needs a card on file (402 otherwise). API key or session
GET/billing
Hosted storage billing: the card on file (brand and last four only), monthly cap, credit, hostedBlock (why new hosted copies are stopped, or null), this month's usage and charges (accrued and projected, in cents) and past invoices. API key or session
PATCH/billing
{ monthlyCapCents } from 500 to 1000000. New hosted copies stop when the month's projected charges reach it. It never discounts a month: stored copies are billed as measured. Session only
POST/billing/card
Starts Stripe Checkout to save a card and returns { url }. Replacing or removing the card (DELETE) also happens here; cards are never sent to BackupSanity. Session only
PATCH/destinations/{id}
{ name?, bucket?, place? } renames or moves it. Moving storage that holds backups needs confirm: true; storage set in a host's config cannot move. Or { ipFamily } alone. Session only
DELETE/destinations/{id}
{ confirm? }. Storage in use needs confirm: true. Storage set in a host's config is refused. Session only
POST/destinations/{id}/ssh
{ action: "generate" } creates an access key and returns its public half. { action: "confirm", fingerprint } trusts a host key a check fetched. Session only
GET/network
?kind=host|storage|source|plan&id=… returns one element's reach, address-family settings and latest checks. API key or session
POST/network
{ action: "check", kind, id, requestId, runnerId? } queues a host scan, storage check, source check or plan check (runner 1.13.0 or later; 1.14.0 for host and storage). API key or session
POST/network
{ action: "local-storage", runnerId, path, name } adds a directory the host lists under localStorage in its runner.json as storage (runner 1.15.0 or later to check it). API key or session
POST/network
{ action: "directory-ssh", destinationId, place } allows SSH access to directory storage so plans on other hosts can write it over SFTP. place: null turns it off. Session only
POST/network
Reach settings: jump-family { runnerId, jump, ipFamily }, source-reach { sourceId, via?, database? }, storage-reach { destinationId, jump?, runnerId?, ipFamily? }. Session only

Restore checks and restores

GET/restore-checks
Up to 100 recent restore checks with status and evidence. Optional ?sourceId=… API key or session
POST/restore-checks
{ sourceId, backupId, requestId? } queues an isolated check of that backup on the host that made it. Returns the job, not a result. API key or session
GET/restores
Kept recoveries. Always an empty list for now. API key or session
POST/restores
Returns 503. Recover an archive manually into a separate database; the source's Restore page shows how. API key or session

API keys

GET/keys
List key names and prefixes. Session only
POST/keys
{ name } returns { token } once. Only a hash is stored. Up to 20 active keys. Session only
DELETE/keys
{ id } revokes a key. It stops working immediately. Session only

Restore checks and their limits

A check restores one validated archive in a container on the host that made it. The host needs the restore runtime enabled locally (sudo backupsanity-setup --enable-restore-checks). The sandbox has no network, no host mounts and no source credentials. It restores, queries every user table and is removed. Default limits: a 64 MiB compressed archive, 768 MiB RAM, 512 MiB scratch and ten minutes. A missing runtime or a check already running on that host returns 409.

The signed-in owner can set a source's automatic policy to daily or every backup. A successful backup, from any trigger or host the source is linked to, then queues a check of that exact archive on the host that ran it. Daily mode waits at least 24 hours between checks.

A passing check proves the archive decodes and restores into PostgreSQL with readable tables. It does not test your application, prove an offsite copy, or keep the recovered database. Keeping a recovery is a manual restore into a separate database.

Join the waitlist for access

For agents, see MCP setup.