Jobs
List backup and migration jobs, read run results, trigger runs.
List jobs
GET /v1/jobs returns backup jobs (bucket → bucket) and migration jobs as one list. Ids are strings like backup_12 or migration_3. A migration's source.type says where it reads from — ssh (one of your servers, over SFTP), azure (Blob container), or bucket (a managed bucket being copied down to a server); its destination is a bucket, or a server (in which case destination.bucketId is null).
{
"jobs": [
{
"id": "backup_12",
"type": "backup",
"name": "nightly-prod-backup",
"schedule": { "frequency": "daily", "hour": 2, "minute": 0, "dayOfWeek": null },
"enabled": true,
"status": "success",
"lastRunAt": "2026-07-16T02:00:04.000Z",
"lastRunResult": "Copied 1,204 objects (18.2 GB)",
"objectsCopied": 1204,
"bytesCopied": 19541237760,
"source": { "bucketId": 12, "prefix": null },
"partial": false,
"scope": { "kind": "whole", "prefix": "", "note": null },
"destination": { "bucketId": 15, "prefix": "backups/prod/" },
"createdAt": "2026-04-11T09:20:33.000Z"
}
]
}A backup with a source folder is a partial backup: it copies only that folder, not the bucket. partial is true, scope is { "kind": "folder", "prefix": "invoices/", "note": "…" }, and source.prefix carries the folder. Use [GET /v1/buckets/{id}/coverage](#coverage) to see which folders a bucket's backups leave out.
A migration job's source looks like:
{
"id": "migration_3",
"type": "migration",
"source": { "type": "ssh", "sshServerId": 4, "bucketId": null, "path": "/var/exports" },
"destination": { "bucketId": 15, "prefix": "sftp-drops/" }
}GET /v1/jobs/{id} adds lastRunLog — the run's per-object events (capped at the most recent 200, with lastRunLogTruncated set when capped). A run can finish as success and still list per-object errors there, so check the log for skipped objects.
Coverage — what a bucket's backups leave out
GET /v1/buckets/{id}/coverage (any key) answers "is this bucket actually backed up?":
{
"coverage": "partial",
"protected": false,
"partialPaths": ["invoices/"],
"uncoveredFolderCount": 3,
"newUncoveredFolderCount": 1,
"uncoveredFolders": [
{ "prefix": "archive/", "firstSeenAt": "2026-10-01T08:00:00.000Z", "isNew": false },
{ "prefix": "contracts/", "firstSeenAt": "2026-10-14T21:10:00.000Z", "isNew": true }
],
"uncoveredHiddenByPermission": 0
}coverage is whole (something copies the entire bucket), partial (only folders are copied — a partial backup, never reported as protected) or none. uncoveredFolders lists folders no backup copies (first 50, only ones the key's member may read; the rest are counted in uncoveredHiddenByPermission). From the CLI: bucketpilot coverage <bucket>.
Trigger a run
Needs a read/write key. POST /v1/jobs/{id}/run queues a run of an existing job (a backup run writes objects to its destination, so it counts as a change). Returns 202 when accepted, 409 with code ALREADY_RUNNING if the job is mid-run.
curl -X POST https://bucketpilot.io/api/v1/jobs/backup_12/run \
-H "Authorization: Bearer $BUCKETPILOT_KEY"The run lands in the activity feed attributed to the API key, and job status/results are visible on the next GET /v1/jobs/{id}.
Pause or cancel a run
Needs a read/write key. POST /v1/jobs/{id}/pause and POST /v1/jobs/{id}/cancel stop a running job. The difference is how the run is recorded — paused reads as "to be continued", cancelled as deliberately stopped. Either way nothing is lost: copies skip objects that already exist at the destination, so the next run picks up where this one left off. Both return 409 with code NOT_RUNNING if the job isn't currently running.
curl -X POST https://bucketpilot.io/api/v1/jobs/backup_12/cancel \
-H "Authorization: Bearer $BUCKETPILOT_KEY"The stop takes effect within a few seconds (the executing worker picks up the flag at its next progress beat).
Verify a backup's copy
Needs a read/write key (it reads both sides, billed to them as egress). POST /v1/jobs/backup_12/verify compares the job's source and destination object by object: size, then checksum or ETag, and when neither can prove it, both sides are read and hashed. Backups only.
curl -X POST https://bucketpilot.io/api/v1/jobs/backup_12/verify \
-H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" -d '{}'Each call checks one slice (limit objects, maxBytes of reading) and returns counts (match, missing, size_differs, content_differs, unreadable, skipped), methods (how each match was proven), problems, done and nextAfter. Send nextAfter back as after until done is true.
Repair a backup's copy
Needs a read/write key and write access to the destination. POST /v1/jobs/backup_12/repair re-copies objects a verify found missing or different. Send up to 50 source keys:
curl -X POST https://bucketpilot.io/api/v1/jobs/backup_12/repair \
-H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" \
-d '{ "keys": ["photos/a.jpg", "photos/b.jpg"] }'Every key is checked again first — the verify result is never trusted — and one that now matches is left alone (already_ok). A missing object is only ever added. An object that exists at the destination but differs is replaced only when you send "replaceDifferent": true (it overwrites what is there); otherwise it comes back as needs_replace and is untouched. After each write the object is compared again, and only a match is repaired; otherwise still_differs. not_repairable is an object that could not be read or compared inside maxBytes, and failed a write the destination refused. The answer has counts, one items entry per key, and bytesCopied.