Assets
Upload
Section titled “Upload”In the app, drop files anywhere on the library. Through the API it is three steps, and the bytes never pass through the server:
# 1. A presigned upload URL for this projectcurl -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 itcurl -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 statesIn 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.
Review
Section titled “Review”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.
Lifecycle and versions
Section titled “Lifecycle and versions”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.
Delivery
Section titled “Delivery”/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 imageEach 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_pngFrom 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.
Names in links
Section titled “Names in links”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 versionsThe 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.
Shown, not handed out
Section titled “Shown, not handed out”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
licensedgoes 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.
Delete and restore
Section titled “Delete and restore”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).
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.