> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dairo.app/llms.txt
> Use this file to discover all available pages before exploring further.

# File storage & share links

> Upload files to storage buckets, then hand them out with revocable share links — one link per object, or one link over many.

Dairo storage keeps your files in buckets. Upload an object once, then create a
revocable link that a recipient can open without an API key — optionally behind
a password, a use cap, or an expiry. Every account starts with a default
bucket, so you can upload without creating anything first.

## Buckets

A bucket is a named container for objects. List your buckets — the default
bucket is seeded on first call, so the list is never empty — or create a named
one. Each bucket reports its live `usedBytes` and `objectCount`.

<CodeGroup>
  ```bash title="cURL" theme={null}
  # List buckets (seeds the default bucket on first call)
  curl https://api.dairo.app/v1/buckets \
    -H "Authorization: Bearer $DAIRO_API_KEY"

  # Create a named bucket
  curl -X POST https://api.dairo.app/v1/buckets \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "name": "invoices", "displayName": "Customer invoices" }'
  ```

  ```ts title="TypeScript" theme={null}
  const { data: buckets } = await dairo.buckets.list();
  const bucket = await dairo.buckets.create({
    name: "invoices",
    displayName: "Customer invoices",
  });
  console.log(bucket.id); // buk_…
  ```

  ```python title="Python" theme={null}
  buckets = dairo.buckets.list()
  bucket = dairo.buckets.create(name="invoices", display_name="Customer invoices")
  print(bucket.id)  # buk_…
  ```

  ```text title="MCP" theme={null}
  Tool: list_storage    (scope buckets:read)   → { "action": "listBuckets" }
  Tool: manage_storage  (scope buckets:write)  → { "action": "createBucket", "name": "invoices", "confirm": true }
  ```
</CodeGroup>

Bucket names are unique per account (case-insensitive, up to 120 characters);
creating a duplicate returns `409`. The default bucket cannot be deleted.
Deleting any other bucket archives it and soft-deletes its objects, so their
bytes stop counting against your storage.

## Upload an object

Uploading is a two-step handshake so bytes flow straight to storage, never
through the API. First initiate the upload to get a time-boxed `PUT` URL, `PUT`
your bytes to it, then finalize so Dairo verifies the true size and records the
object.

<Steps>
  <Step title="Initiate — get an upload URL">
    ```bash title="cURL" theme={null}
    curl -X POST https://api.dairo.app/v1/buckets/buk_123/objects \
      -H "Authorization: Bearer $DAIRO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "filename": "invoice-042.pdf", "contentType": "application/pdf" }'
    ```

    The response is a `PresignedUpload`: `{ objectId, uploadUrl, method, headers, expiresInSeconds }`.
    The URL is valid for 300 seconds.
  </Step>

  <Step title="PUT the bytes">
    Send the file directly to `uploadUrl` with the exact `headers` returned —
    they are part of the signature, and the upload is rejected without them.

    ```bash title="cURL" theme={null}
    curl -X PUT "$UPLOAD_URL" \
      --upload-file ./invoice-042.pdf \
      -H "Content-Type: application/pdf"
    ```
  </Step>

  <Step title="Finalize — record the object">
    ```bash title="cURL" theme={null}
    curl -X POST https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID/finalize \
      -H "Authorization: Bearer $DAIRO_API_KEY"
    ```

    You get back a `BucketObject` with its verified `bytes` and a `scanStatus`.
    Dairo measures the stored bytes itself — a client-declared size is never
    trusted — and rejects the object with `429` if it would exceed your plan's
    storage limit. Finalizing is idempotent: repeating the call returns the
    already-recorded object.
  </Step>
</Steps>

<Note>
  **Objects are scanned before they can be shared.** A freshly finalized object
  starts at `scanStatus: "pending"`: you can still download it yourself, but
  creating a share link over it returns `409` until the verdict is `clean`. An
  `infected` or `failed` verdict blocks downloads for everyone, including you.
</Note>

## Download an object

Ask for a short-lived download URL, then fetch the bytes from it directly. The
URL is valid for 3,600 seconds.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID/download \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  # → { "downloadUrl": "https://…", "expiresInSeconds": 3600 }
  ```

  ```ts title="TypeScript" theme={null}
  const { downloadUrl } = await dairo.buckets.getObjectDownloadUrl("buk_123", objectId);
  ```

  ```python title="Python" theme={null}
  dl = dairo.buckets.download_url("buk_123", object_id)
  print(dl.download_url)
  ```
</CodeGroup>

This URL is for you, not for recipients — it authenticates as you and expires
on its own schedule. To hand a file to someone else, create a share link.

## Share a single object

A share link turns one stored object into a revocable URL anyone can open — no
API key needed. All three policy options are optional:

| Option      | Behavior                                                                                                              |
| ----------- | --------------------------------------------------------------------------------------------------------------------- |
| `password`  | The recipient must enter it on the share page before downloading.                                                     |
| `maxUses`   | Caps served downloads, 1–1,000,000. `1` makes the link one-time: the download returns `410 Gone` after the first use. |
| `expiresAt` | Absolute RFC3339 expiry. Omit for a link that never expires.                                                          |

The `shareUrl` (a `/s/` share page) and `downloadUrl` (a direct `/d/` download)
are returned only at create time, so capture them from the response. Opening
the share page spends nothing; each direct download counts one use.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID/share-links \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "password": "hunter2", "maxUses": 5, "expiresAt": "2026-08-01T00:00:00Z" }'
  ```

  ```python title="Python" theme={null}
  link = dairo.buckets.create_share_link(
      "buk_123",
      object_id,
      password="hunter2",
      max_uses=5,
      expires_at="2026-08-01T00:00:00Z",
  )
  print(link.share_url, link.download_url)
  ```

  ```text title="MCP" theme={null}
  Tool: manage_storage  (scope buckets:write)
  Args: { "action": "createShareLink", "bucketId": "buk_123", "objectId": "…", "maxUses": 5, "confirm": true }
  ```
</CodeGroup>

```json theme={null}
{
  "object": "share_link",
  "id": "shr_9f8e7d6c",
  "objectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "shareUrl": "https://dairo.app/s/shr_9f8e7d6c",
  "downloadUrl": "https://dairo.app/d/shr_9f8e7d6c/invoice-042.pdf",
  "hasPassword": true,
  "maxUses": 5,
  "oneTime": false,
  "usesCount": 0,
  "expiresAt": "2026-08-01T00:00:00Z",
  "revoked": false
}
```

## Share a bundle — one link over many objects

When you want to hand out several files as a single link, create a share
bundle: `POST /v1/buckets/{bucketId}/share-links` with an `objectIds` array —
1 to 50 distinct ids, all in the same bucket. The same `password`, `maxUses`,
and `expiresAt` options apply, enforced on every file in the bundle.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/buckets/buk_123/share-links \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "objectIds": [
            "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "7c9e6679-7425-40de-944b-e07fc1f90ae7"
          ],
          "password": "hunter2",
          "maxUses": 10,
          "expiresAt": "2026-08-01T00:00:00Z"
        }'
  ```

  ```ts title="TypeScript" theme={null}
  const bundle = await dairo.buckets.createShareBundle("buk_123", {
    objectIds: [
      "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    ],
    password: "hunter2",
    maxUses: 10,
    expiresAt: "2026-08-01T00:00:00Z",
  });
  console.log(bundle.shareUrl); // one landing page, many files
  ```

  ```python title="Python" theme={null}
  bundle = dairo.buckets.create_share_bundle(
      "buk_123",
      object_ids=[
          "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      ],
      password="hunter2",
      max_uses=10,
      expires_at="2026-08-01T00:00:00Z",
  )
  print(bundle.share_url)
  ```

  ```text title="MCP" theme={null}
  Tool: manage_storage  (scope buckets:write)
  Args: { "action": "createShareBundle", "bucketId": "buk_123", "objectIds": ["…", "…"], "maxUses": 10, "confirm": true }
  ```
</CodeGroup>

The response is a `share_bundle`: one `shareUrl` landing page that lists every
file, plus a per-file `downloadUrl` for each object.

```json theme={null}
{
  "object": "share_bundle",
  "id": "bnd_4b8a1c2d",
  "shareUrl": "https://dairo.app/share/bnd_4b8a1c2d",
  "fileCount": 2,
  "hasPassword": true,
  "maxUses": 10,
  "oneTime": false,
  "expiresAt": "2026-08-01T00:00:00Z",
  "revoked": false,
  "files": [
    {
      "objectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "filename": "invoice-042.pdf",
      "downloadUrl": "https://dairo.app/d/shr_2c1b0a9f/invoice-042.pdf"
    },
    {
      "objectId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "filename": "invoice-043.pdf",
      "downloadUrl": "https://dairo.app/d/shr_5d4c3b2a/invoice-043.pdf"
    }
  ]
}
```

Behind the landing page, each file is backed by its own share link over that
object. Those per-file links appear in the object's share-link list and can be
revoked individually, exactly like links you create one at a time.

## Manage share links

List the links over an object, read a link's open analytics, or revoke a link
the moment it should stop working.

<CodeGroup>
  ```bash title="cURL" theme={null}
  # Links over one object (policy and usage only — URLs are returned only at create time)
  curl https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID/share-links \
    -H "Authorization: Bearer $DAIRO_API_KEY"

  # Who opened a link (limit 1–200, default 50)
  curl https://api.dairo.app/v1/share-links/shr_9f8e7d6c/opens \
    -H "Authorization: Bearer $DAIRO_API_KEY"

  # Revoke a link — 204 No Content, idempotent
  curl -X DELETE https://api.dairo.app/v1/share-links/shr_9f8e7d6c \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```python title="Python" theme={null}
  links = dairo.buckets.list_share_links("buk_123", object_id)
  opens = dairo.buckets.list_share_link_opens("shr_9f8e7d6c")
  dairo.buckets.revoke_share_link("shr_9f8e7d6c")
  ```

  ```text title="MCP" theme={null}
  Tool: list_storage    (scope buckets:read)   → { "action": "listShareOpens", "shareLinkId": "shr_9f8e7d6c" }
  Tool: manage_storage  (scope buckets:write)  → { "action": "revokeShareLink", "shareLinkId": "shr_9f8e7d6c", "confirm": true }
  ```
</CodeGroup>

The opens response pairs an authoritative summary with the most recent open
events. `summary.servedOpens` counts served downloads — the number measured
against `maxUses` — while share-page views are logged but never consume a use.
Each event carries `openedAt`, `clientIp`, `userAgent`, and an `outcome` such
as `served`.

After a revoke, every future open of the link's share page or download URL
returns `410 Gone`. The same happens when a link expires or its use cap runs
out.

## Delete objects

Deleting an object removes its stored bytes and stops them counting against
your storage. Delete one object, or up to 1,000 in a single call:

```bash theme={null}
# One object
curl -X DELETE https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID \
  -H "Authorization: Bearer $DAIRO_API_KEY"

# Many objects — partial success, one bad id never fails the batch
curl -X POST https://api.dairo.app/v1/buckets/buk_123/objects/batch-delete \
  -H "Authorization: Bearer $DAIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "objectIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6", "7c9e6679-7425-40de-944b-e07fc1f90ae7"] }'
```

The batch response separates `deleted` ids from `failed` entries, each failure
naming its id and reason.

## Scopes

Listing buckets, objects, download URLs, share links, and open analytics
requires `buckets:read`. Creating buckets, uploading, deleting, and minting or
revoking share links require `buckets:write`. See [scopes](/concepts/scopes)
for how permissions attach to API keys.

## Related

* [Attachments](/receiving/attachments) — pull files off inbound mail and
  reference stored objects when sending.
* [MCP server](/agent-first/mcp-server) — the `list_storage` and
  `manage_storage` tools for agents.
* [API reference](/api-reference) — every buckets, objects, and share-link
  endpoint.
