Skip to content

Assets

In the app, drop files anywhere on the library. Through the API it is three steps, and the bytes never pass through the server:

Terminal window
# 1. A presigned upload URL for this project
curl -X POST https://assets.example.com/api/v1/uploads -H "Authorization: Bearer $KEY" \
-H 'content-type: application/json' -d '{"filename":"hero.png","mime":"image/png","size":20135}'
# 2. PUT the file to the uploadUrl it returned
# 3. File it
curl -X POST https://assets.example.com/api/v1/assets -H "Authorization: Bearer $KEY" \
-H 'content-type: application/json' -d '{"token":"<token>","filename":"hero.png","mime":"image/png"}'

Step 3 hashes what arrived. The same bytes uploaded twice are one asset (deduped: true). Or skip the PUT and give a URL: {"url": "https://..."} has the server fetch it; a Figma or Google Docs link is kept as the link and shows as its embed.

Files go up to 512 MB each. A bigger one is refused at step 1, before any bytes move, with 413 too_large.

On ingest, EXIF, IPTC and XMP are read (title, caption, creator, copyright, camera), embedded keywords become tags, and C2PA Content Credentials say how the file was made.

/api/v1/assets?q=fox her every word, as a prefix
/api/v1/assets?tag=mascot&tag=autumn assets carrying every tag
/api/v1/assets?collection=Autumn 26 a collection, by id or by name
/api/v1/assets?f.channel=web&f.budget.gte=10 custom fields
/api/v1/assets?status=draft&status=archived other lifecycle states

In the app, Assets’ address is its view: /assets?q=winter opens that search (with the brands, collections and rules it finds above the assets), /assets every asset, and /assets/{id} one asset over it, with where it is used and “Can I use this?” first. What a search or a filter finds can be kept as a collection: Save as collection beside the view options makes one of what it finds now (later uploads don’t join it on their own).

q covers filenames, tags, text field values and embedded metadata. Every response carries facet counts over the same filter. A page is 100 assets (limit, up to 200); total counts every match.

Custom fields are defined per project (Settings, Custom fields): text, number, date, boolean and select, required or not. A collection can carry field values its members inherit.

What a propose key, an agent or an upload link adds lands proposed: out of the library, in Review (/review), with who suggested it, where it came from (AI-made, by an agent, imported) and what is missing, such as a model release. Approving is PATCH {"status": "active"}; rejecting, {"status": "rejected", "reviewNote": "..."}, which keeps it so the agent can read why.

An agent can also suggest tags and custom field values for an asset already in the library (propose_tags, propose_fields, or POST /api/v1/assets/{id}/proposed-tags and proposed-fields). Nothing changes until a person accepts: each suggestion shows on the asset with a check and a cross, and the asset waits in Review meanwhile. Values are checked against their field when suggested, so what waits can be accepted as it is.

An asset goes from draft to proposed (in review) to active (approved), and to archived when it is retired. Expired is a date, not a status: an approved asset past rights.expires reads expired the next day. The library shows approved, unexpired assets unless asked for others.

A new file for the same thing is a new version: POST /api/v1/assets with "versionOf": "{id}". Once approved it becomes the stack’s current version, and the old ones are superseded by it. POST /api/v1/assets/{id}/versions/{n}/current rolls back.

/a/{id} the original, exactly as uploaded
/a/{id}?download with the library's metadata written in
/a/{id}/w_800,f_webp 800px wide, WebP
/a/{id}/w_1200,h_630,fit_cover,q_82,f_jpeg an Open Graph image

Each URL is pinned: /a/{id} serves that version’s bytes for as long as it may be used. To embed an asset so it follows its versions, use /c/ in place of /a/:

/c/{id}/h_64,f_png 302 to /a/{current id}/h_64,f_png

From any version, it redirects to the stack’s current one, or to the asset a person replaced it with. The redirect is cached for a minute, so a new version, a rollback or a replacement reaches every site and document that embeds it within one. It follows the same access as /a/: anyone when the asset is public, otherwise people who can see it. A signed URL names one version, so it stays an /a/ URL.

Links Artbucket hands out (copied links, share links, portal downloads, the API’s and the MCP tools’ URLs, artbucket url) end in the asset’s name, so a link pasted in a deck, an email or a CMS reads as what it is:

/a/{id}/summer-hero.jpg the original
/a/{id}/w_800,f_webp/summer-hero.webp 800px wide, WebP
/c/{id}/w_800,f_webp/summer-hero.webp the same, following its versions

The name is the asset’s title, or its file name when it has none, in plain lowercase letters, digits and dashes, and the extension is the format the link serves. It is for people only: the id and the transform say what is served, and the name is never read. A link without one works as it always has, and renaming an asset breaks no link, though old links keep their old name. A segment with a dot is a name; a transform never has one.

?download saves the file as the asset’s name now, with the extension of what is downloaded (Summer hero.webp for that rendition), whatever the link says: Content-Disposition carries it in UTF-8 (RFC 6266 and 5987), with an ASCII fallback for clients that read only filename.

Some files may be shown but not given away: a stock photo licensed for your site, a foundry’s typeface licensed for your pages. An asset’s rights.downloadable says whether people outside the project may take the file itself. Left empty, its license decides:

  • A font goes out only under an open license (SIL Open Font License, Apache, Ubuntu Font License). An uploaded font with no license of its own takes the one its file names, so a Google Fonts family is downloadable and a foundry’s is not until you say so.
  • A file whose origin is licensed goes out only under an open license, such as Creative Commons.
  • Anything else goes out.

To anyone a public or signed URL lets in (portals, share links, BrandHub, an embed), a file kept from them is shown only: renditions up to 1600 px a side, its original only into a page of this app that draws it (a portal’s typeface, a video playing), and ?download answers 403 shown_only. Your team gets everything, as always. Portals, share links and BrandHub offer no download for it, and brand.json leaves its font files out. In the app, an asset’s Rights show Downloads (Automatic, Allowed or Shown only) and what that means for whoever sees it; releasing a brand, its Sharing tab and a portal’s door say how many of its files visitors can download.

A browser asked for a file by one of this app’s pages says so in its request headers, and that is what lets a typeface load into a portal and not open on its own. A script outside a browser can claim the same: what it gets is what every visitor’s browser already gets, as with any font a website serves.

Public goes with the asset: a new version that takes over from a public one is public too, when whoever uploads it, or approves it if it was proposed, may share it. Otherwise it waits for someone who may, and /c/ sends anonymous visitors to a URL that answers 404 until then.

Transforms: w h (1 to 8000), fit (cover, contain, inside, outside, fill), q (1 to 100), f (jpeg, png, webp, avif). A rendition is made on the first request and kept for 30 days. Through a public or signed URL, people outside the project get any width in WebP and the presets, and up to 16 other renditions of a file (what a rendition URL can cost).

The bytes are private: these URLs work for people who can see the asset in the library, signed in or with a key, as far as its permissions go. For anyone else, share it: a signed URL that expires (?s=..., which works for every rendition of it), or the asset made public, for embedding. Signed or public, only an approved, unexpired asset out of embargo is served; archived, expired or deleted ones answer 410, so embeds stop on time.

Deleting an asset takes it out of the library, its collections’ counts, its links and its stack at once, and its URLs answer 410. For 30 days it is still there: pick Deleted in the Status filter to see it, and Restore to bring it back (or Undo on the toast, right after).

Terminal window
curl -X DELETE https://assets.example.com/api/v1/assets/{id} -H "Authorization: Bearer $KEY"
curl -X POST https://assets.example.com/api/v1/assets/{id}/restore -H "Authorization: Bearer $KEY"

After 30 days it is purged, and its file with it, unless another asset, in any project, holds the same bytes. Uploading the same bytes again in the meantime restores it, for whoever may restore it; anyone else gets 409.