---
title: "Artifacts"
description: "Listing, inspecting, downloading and deleting stored files."
url: "https://saved.sh/docs/api/artifacts"
---

An artifact is what a successful run produced. It is created only by the pipeline: there is
no endpoint that creates one.

## List [#list]

```bash
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 [#get]

```bash
curl -fsS "https://api.saved.sh/v1/artifacts/$AID" -H "Authorization: Bearer $SAVED_API_KEY"
```

```json
{
  "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 [#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          |

<Callout type="warn">
  **`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.
</Callout>

### Locations [#locations]

```json
{
  "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 [#download]

Two steps. The bytes do not pass through this API.

```bash
curl -fsS -X POST "https://api.saved.sh/v1/artifacts/$AID/download-url" \
  -H "Authorization: Bearer $SAVED_API_KEY"
```

```json
{ "url": "https://...", "method": "GET", "expires_at": "2026-08-08T10:14:02Z" }
```

```bash
curl -fsS -o ./artifact "$URL"
```

Permission: `artifacts:download`. Note it is a `POST`: issuing a URL is a recorded action, not
a read.

<Callout>
  This is the one read on the [audit trail](/docs/security/audit-log), as
  `artifact.download_url_issued`. We never see the fetch itself, so the issued URL is the only
  evidence a copy left.
</Callout>

### When it is refused [#when-it-is-refused]

```json
{
  "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 [#delete]

```bash
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                        |

<Callout type="warn">
  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.
</Callout>

Ordinary expiry is [retention](/docs/backups/retention), applied by a daily sweep. This
endpoint is for a specific artifact you want gone now.

## Checking encryption across a workspace [#checking-encryption-across-a-workspace]

Worth running on a schedule. A backup silently producing plaintext is a failure nothing else
reports.

```bash
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)"'
```

## Next [#next]

<Cards>
  <Card href="/docs/recover/artifact-format" title="Artifact format" description="Opening the bytes you downloaded." />

  <Card href="/docs/backups/delivery" title="Delivery" description="Why an artifact has several locations." />

  <Card href="/docs/backups/retention" title="Retention" description="What expires an artifact." />
</Cards>
