Artifacts
Listing, inspecting, downloading and deleting stored files.
View as MarkdownAn artifact is what a successful run produced. It is created only by the pipeline: there is no endpoint that creates one.
List
curl -fsS "https://api.saved.sh/v1/workspaces/$WID/artifacts?limit=50" \
-H "Authorization: Bearer $SAVED_API_KEY"
curl -fsS "https://api.saved.sh/v1/backups/$BID/artifacts" \
-H "Authorization: Bearer $SAVED_API_KEY"Permission: artifacts:read, which grants metadata only. Reading the bytes needs
artifacts:download.
Get
curl -fsS "https://api.saved.sh/v1/artifacts/$AID" -H "Authorization: Bearer $SAVED_API_KEY"{
"id": "018f5a2c-...",
"backup_id": "018f3c2a-...",
"run_id": "scheduled-018f3c2a-1754640000",
"state": "archived",
"size_bytes": 1483920128,
"original_bytes": 4192104448,
"checksum": "sha256:9f86d081884c7d65...",
"filename": "app.dump.gz.gpg",
"mime_type": "application/octet-stream",
"compressed": true,
"encrypted": true,
"key_id": "3AA5C34371567BD2",
"lock": { "enabled": true, "retain_until": "2026-09-07T02:04:11Z" },
"locations": [
{
"kind": "permanent",
"bucket": "saved-archive",
"object_key": "4b7e1d90-.../018f3c2a-.../scheduled-018f3c2a-1754640000/artifact",
"state": "stored",
"stored_bytes": 1483920128,
"written_at": "2026-08-08T02:04:11Z"
}
],
"second_copy": false,
"downloadable": true,
"created_at": "2026-08-08T02:04:11Z"
}Fields worth understanding
| Field | Meaning |
|---|---|
state | archived, expired or deleted |
size_bytes | The size of what is stored |
original_bytes | The size of what was uploaded, before our transforms |
checksum | SHA-256 of the file as uploaded |
key_id | Fingerprint of the public key it was encrypted to |
downloadable | Whether we hold a copy we can serve |
locations | One row per copy: ours and each of your buckets |
checksum is of the uploaded file, not the stored one. For a local backup those are the
same object, so it matches the downloaded bytes directly. For cloud and manual backups where
we applied compression or encryption, it matches the file only after you decrypt it.
original_bytes != size_bytes is the signal that a transform was applied.
Locations
{
"kind": "destination",
"destination_id": "7c1e...",
"destination_name": "prod-archive-eu",
"bucket": "my-bucket",
"object_key": "prefix/4b7e1d90-.../018f3c2a-.../scheduled-.../artifact",
"state": "failed",
"error": "AccessDenied",
"written_at": "2026-08-08T02:04:09Z"
}kind | Where |
|---|---|
permanent | Our archive |
destination | One of your own buckets |
state | Meaning |
|---|---|
stored | The copy exists |
failed | It was attempted and did not land. error says why |
deleted | It was removed |
A failed location is how a partially delivered run is visible: the artifact exists, the run is
Failed, and locations says exactly which copies you have.
Download
Two steps. The bytes do not pass through this API.
curl -fsS -X POST "https://api.saved.sh/v1/artifacts/$AID/download-url" \
-H "Authorization: Bearer $SAVED_API_KEY"{ "url": "https://...", "method": "GET", "expires_at": "2026-08-08T10:14:02Z" }curl -fsS -o ./artifact "$URL"Permission: artifacts:download. Note it is a POST: issuing a URL is a recorded action, not
a read.
This is the one read on the audit trail, as
artifact.download_url_issued. We never see the fetch itself, so the issued URL is the only
evidence a copy left.
When it is refused
{
"error": {
"code": "not_in_our_storage",
"message": "this artifact was delivered only to your own buckets, so we have no copy to serve; fetch it from the bucket named in its locations",
"request_id": "req_..."
}
}A backup with skip_permanent sends the bytes only to your destinations. Read locations for
the bucket and key, and fetch it with your own credentials.
downloadable on the artifact tells you this before you try.
Delete
curl -fsS -X DELETE "https://api.saved.sh/v1/artifacts/$AID" -H "Authorization: Bearer $SAVED_API_KEY"Returns 204. Permission: artifacts:delete.
| Refusal | Meaning |
|---|---|
409 artifact_locked | Inside its lock_for window |
409 last_artifact | The only artifact left for that backup |
409 conflict | Already deleted |
Deleting removes our copy. Copies in your own destination buckets are left where they are, because we hold write credentials rather than a mandate to destroy data in your storage.
Ordinary expiry is retention, applied by a daily sweep. This endpoint is for a specific artifact you want gone now.
Checking encryption across a workspace
Worth running on a schedule. A backup silently producing plaintext is a failure nothing else reports.
curl -fsS "https://api.saved.sh/v1/workspaces/$WID/artifacts?limit=200" \
-H "Authorization: Bearer $SAVED_API_KEY" \
| jq -r '.data[] | select(.encrypted == false) | "\(.id) \(.backup_id) \(.filename)"'