Manual backups
No source and no schedule. You produce the file, we archive it.
View as MarkdownA manual backup has no source and no schedule. You produce a file however you like and submit it; everything after that is identical to a scheduled backup.
What it is for
Manual exists for the cases the other two kinds cannot reach:
- Systems we have no connector for, and that you would rather not wrap in a
scriptsource. - Cadences we do not control. A quarterly export, an end-of-year close, a snapshot taken before a migration.
- Air-gapped or offline sources, where the file is produced somewhere with no path to either your worker or our network.
- One-off archives that should live under the same retention and protection as everything else.
If the thing you want is "run my command on a schedule, on my hardware", that is a
script source on a local backup, not a manual one.
What manual backups do not take
A manual backup is defined by what it omits, and each omission is enforced rather than ignored:
| Field | Result |
|---|---|
source_type | Refused. There is no source |
schedule | Refused. It runs when you submit |
worker | Refused |
source / credentials | Refused. There is nothing to connect to |
Everything else applies normally: compression, encryption, delivery, and retention all work exactly as they do for a scheduled backup.
backups:
- name: quarterly-export
kind: manual
retention:
keep_last: 8
expire_after: 1095d
encryption:
public_key: ./keys/archive.ascA manual backup becomes active as soon as it is created. There is nothing else it needs.
Submitting from sctl
One command does the whole flow.
sctl backup submit <backup-id> ./quarterly-export.tar.gzSubmitted ./quarterly-export.tar.gz as run manual-018f3c2a-.... post-backup is running.Then watch it like any other run:
sctl run list --backup $BACKUP_ID
sctl artifact list --backup $BACKUP_IDSubmitting from the API
Four steps. The bytes never pass through our API: you upload directly to object storage using a presigned URL.
1. Create the run.
curl -fsS https://api.saved.sh/v1/runs \
-H "Authorization: Bearer $SAVED_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"backup_id": "<backup-id>"}'{ "run_id": "manual-018f3c2a-...", "expires_at": "2026-08-08T11:14:02Z" }The run is created and then waits for the bytes. expires_at is how long it waits.
2. Ask for an upload target.
curl -fsS https://api.saved.sh/v1/runs/<run-id>/upload-url \
-H "Authorization: Bearer $SAVED_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"content_length": 5368709120}'{ "method": "PUT", "url": "https://...", "expires_at": "..." }For a large file the response is "method": "MULTIPART" instead, with a part_size and
part_count. See multipart.
3. Upload.
curl -fsS -X PUT --upload-file ./quarterly-export.tar.gz "<url>"4. Confirm.
curl -fsS https://api.saved.sh/v1/runs/<run-id>/confirm \
-H "Authorization: Bearer $SAVED_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"checksum": "sha256:9f86d0...",
"size_bytes": 5368709120,
"filename": "quarterly-export.tar.gz"
}'Confirming is what releases the run. Until then nothing is processed, and an unconfirmed run produces no artifact however completely the upload succeeded.
Upload and confirm are separate steps on purpose. The upload goes straight to storage and we are not in that path, so a completed PUT tells us nothing on its own. Confirm is the only thing that says the bytes are all there and what they hash to.
Multipart uploads
When the file is large enough, upload-url returns "method": "MULTIPART" with a
part_size and part_count.
| Step | Endpoint |
|---|---|
| Request part URLs, in batches | POST /v1/runs/{run}/upload-parts |
| Upload each part | PUT to the returned URL, keeping the ETag |
| Finish | POST /v1/runs/{run}/upload-complete with every part number and ETag |
| Give up | POST /v1/runs/{run}/upload-abort |
Abort a failed multipart upload rather than abandoning it. Parts left behind occupy storage until they are cleaned up.
The checksum
The checksum you send is what the artifact record carries, and it is what you compare against when you download the artifact back.
sha256sum ./quarterly-export.tar.gzSend it as sha256:<hex>. It is computed over the file as you uploaded it, before any
compression or encryption we apply afterwards.
Encryption
A manual artifact is encrypted by us, after upload, if the backup carries a public key. This differs from a local backup, where encryption happens on your worker before anything leaves.
If you need the file to be encrypted before it reaches our storage, encrypt it yourself and upload the ciphertext. A manual backup's plaintext exists on our infrastructure between upload and the encrypt step, exactly as a cloud backup's does.
Encrypting it yourself and also setting a key on the backup is harmless: you get a PGP message inside a PGP message, at the cost of the second one compressing nothing.
Retention applies normally
Manual artifacts are swept by the same daily retention pass as everything else, under the
policy on the manual backup. keep_last counts submissions, and the newest artifact is never
expired.
A quarterly export with keep_last: 8 and expire_after: 1095d keeps two years of quarters
by age and never drops below eight, whichever binds first.