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.

# 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" }'

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.

Initiate — get an upload URL

cURL
UPLOAD=$(curl -s -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" }')
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .objectId)
UPLOAD_URL=$(echo "$UPLOAD" | jq -r .uploadUrl)

The response is a PresignedUpload: { objectId, uploadUrl, method, headers, expiresInSeconds }. Its objectId is the upload id: you use it only to finalize this upload. The URL is valid for 300 seconds.

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.

cURL
curl -X PUT "$UPLOAD_URL" \
  --upload-file ./invoice-042.pdf \
  -H "Content-Type: application/pdf"

Finalize — record the object

cURL
OBJECT_ID=$(curl -s -X POST https://api.dairo.app/v1/buckets/buk_123/objects/$UPLOAD_ID/finalize \
  -H "Authorization: Bearer $DAIRO_API_KEY" | jq -r .id)

You get back a BucketObject with its verified bytes and a scanStatus. Its id is the object's id, and it is not the upload objectId: download, share links and delete all take this id (with the upload id they answer 404). 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 with the same upload id returns the already-recorded object.

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.

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.

curl https://api.dairo.app/v1/buckets/buk_123/objects/$OBJECT_ID/download \
  -H "Authorization: Bearer $DAIRO_API_KEY"
# → { "downloadUrl": "https://…", "expiresInSeconds": 3600 }

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:

OptionBehavior
passwordThe recipient must enter it on the share page before downloading. 8–256 characters, used exactly as typed: spaces count and nothing is trimmed.
maxUsesCaps served downloads, 1–1,000,000. 1 makes the link one-time: the download returns 410 Gone after the first use.
expiresAtAbsolute RFC3339 expiry, in the future (a past time is a 400). 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.

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": "harbor-lantern-42", "maxUses": 5, "expiresAt": "2027-12-31T23:59:59Z" }'
{
  "object": "share_link",
  "id": "shr_9f8e7d6c",
  "objectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "shareUrl": "https://storage.dairo.app/s/dl.9f8e7d6c…",
  "downloadUrl": "https://storage.dairo.app/d/dl.9f8e7d6c…/invoice-042.pdf",
  "hasPassword": true,
  "maxUses": 5,
  "oneTime": false,
  "usesCount": 0,
  "expiresAt": "2027-12-31T23:59:59Z",
  "revoked": false
}

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 and maxUses options apply, enforced on every file in the bundle. A bundle always expires: without expiresAt it lasts 7 days, and expiresAt may lie at most 30 days ahead (anything later is a 400).

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": "harbor-lantern-42",
        "maxUses": 10
      }'

The response is a share_bundle: one shareUrl landing page that lists every file, plus a per-file downloadUrl for each object. The shareUrl carries the bundle's secret dl. token, not its bnd_ id, and like the file links it is returned only at create time.

{
  "object": "share_bundle",
  "id": "bnd_4b8a1c2d",
  "shareUrl": "https://dairo.app/share/dl.7e6f5a4b…",
  "fileCount": 2,
  "hasPassword": true,
  "maxUses": 10,
  "oneTime": false,
  "expiresAt": "2026-11-02T09:00:00Z",
  "revoked": false,
  "files": [
    {
      "objectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "filename": "invoice-042.pdf",
      "downloadUrl": "https://storage.dairo.app/d/dl.2c1b0a9f…/invoice-042.pdf"
    },
    {
      "objectId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "filename": "invoice-043.pdf",
      "downloadUrl": "https://storage.dairo.app/d/dl.5d4c3b2a…/invoice-043.pdf"
    }
  ]
}

Behind the landing page, each file is backed by its own share link over that object, with the bundle's expiry. 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.

List the organization's bundles with GET /v1/share-links/bundles (file count, password flag, expiry and revoked state; no tokens), and revoke a bundle with DELETE /v1/share-links/{bundleId}: its landing page and every per-file download link answer 410 Gone from then on.

curl https://api.dairo.app/v1/share-links/bundles \
  -H "Authorization: Bearer $DAIRO_API_KEY"

# Revoke a bundle and all of its file links — 204 No Content, idempotent
curl -X DELETE https://api.dairo.app/v1/share-links/bnd_4b8a1c2d \
  -H "Authorization: Bearer $DAIRO_API_KEY"

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

# 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"

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:

# 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 for how permissions attach to API keys.

  • Attachments — pull files off inbound mail and reference stored objects when sending.
  • MCP server — the list_storage and manage_storage tools for agents.
  • API reference — every buckets, objects, and share-link endpoint.

On this page