Managing assets
An asset is an image, video or 3D model in your library. You can manage your assets through the API:
PATCH /assets/{id}(updateAsset) — setfavorited, writenotes, attach your ownmetadata.POST /assets/{assetId}/archive(archiveAsset) andPOST /assets/{assetId}/unarchive(unarchiveAsset) — set an asset aside, and bring it back.DELETE /assets/{id}(deleteAsset) andPOST /assets/{assetId}/undelete(undeleteAsset) — delete an asset, and restore it.?showDeleted=trueonGET /assets(listAssets) andGET /assets/{id}(getAssetById) — read deleted assets too.
None of these spends credits. Every write returns the asset as GET /assets/{id} returns it, with an
ETag header.
1. Mint a key
In the Mocart dashboard, go to Settings → API keys and create a key with the assets:write
permission. Like every write key it must expire, within 365 days. assets:write implies read, so the
same key can read back what it changed. See Authentication.
export API_KEY="mcf_74b5dd1e6a29150ab5ab6081d0b30ac043aadf7d2c948e80d330775d0b6382e8"
2. Edit an asset — PATCH /assets/{id} (updateAsset)
Send only the fields you want to change. All three are optional; any other field — including status
— is a 422. To change an asset's status, use archive and unarchive.
curl -s -X PATCH https://api.mocart.io/api/public/v1/assets/ast_3d8f1b6a9c2e \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"favorited": true,
"notes": "Hero shot for the autumn campaign.",
"metadata": { "externalId": "dam-88421", "license": "owned" }
}'
200 OK, with an ETag header, and the asset:
{
"object": "asset",
"assetId": "ast_3d8f1b6a9c2e",
"productId": "prod_8f2a1c9b4d6e",
"type": "STATIC",
"url": "https://storage.googleapis.com/.../ast_3d8f1b6a9c2e.png",
"thumbnailUrl": null,
"favorited": true,
"status": "active",
"name": "Front view",
"width": 2048,
"height": 2048,
"durationSec": null,
"playerUrl": null,
"embedCode": null,
"createdAt": "2026-09-12T10:04:31.000Z",
"metadata": { "externalId": "dam-88421", "license": "owned" },
"notes": "Hero shot for the autumn campaign."
}
| Field | What it does |
|---|---|
favorited | true shortlists the asset, false takes it off the shortlist. |
notes | A free-text note of up to 500 characters. "" clears it. |
metadata | Your own string labels, merged into the stored map — see below. |
notes and metadata appear in the response only when they hold something, so an asset with neither
reads exactly as it did before.
metadata
metadata is a map of your own string labels — an id from your DAM, a licence, a batch name. Mocart
stores it and returns it; it has no other meaning. A PATCH merges what you send into the stored
map, key by key:
| You send | Result |
|---|---|
"key": "value" | The key is added, or its value is replaced. |
"key": null | The key is removed. |
| A key you leave out | Untouched. |
"metadata": {} | Nothing changes. |
There is no "replace everything": to clear the map, send null for each key.
The limits: at most 50 keys in the stored map, key names of 1–40 characters (letters, digits, _ and
-, and not starting with __), and values of up to 500 characters. One request may name up to 100 keys,
so it can delete a full map and set a new one. A merge that would leave more than 50 keys is a 422, and
nothing is changed.
A request that changes nothing — values already in place, an empty body or no body at all — is a 200
that records nothing.
3. Archive or delete?
Both take an asset out of your everyday view, and both can be undone. They are for different jobs.
| Archive | Delete | |
|---|---|---|
| What it means | "Set this aside; I may want it back." | "I don't want this." |
| What changes | The asset's status becomes archived. | The asset is hidden: deletedAt is set. |
Shown by GET /assets | Not by default. ?status=archived lists archived assets. | Never, unless you pass ?showDeleted=true. |
GET /assets/{id} | Still returns it, with "status": "archived". | 404, unless you pass ?showDeleted=true. |
| Can it be edited? | Yes. | No — restore it first. |
| Restored with | unarchive, to the status it had before. | undelete, exactly as it was. |
| Time limit on restoring | None. | None. |
Neither one destroys the asset's files.
4. Archive and unarchive
curl -s -X POST https://api.mocart.io/api/public/v1/assets/ast_3d8f1b6a9c2e/archive \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f2a9c1e-84b7-4d30-a5e1-0c9b7d2e3f58" \
-d '{}'
200 OK, with the asset and "status": "archived". Archiving remembers the status the asset had, and
unarchive puts it back — or active when that was never recorded. Archiving an archived asset, or
unarchiving one that isn't archived, is a 200 that changes nothing, so a retry is always safe.
As on every POST, send a body ({}): one with none is refused by the load balancer with 411.
Because archived assets are left out of GET /assets unless you filter for them, list them with
GET /assets?status=archived.
5. Delete and undelete
curl -s -X DELETE https://api.mocart.io/api/public/v1/assets/ast_3d8f1b6a9c2e \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: 0d7c4a19-3e6b-42f8-9b05-e1a8c6f2d473"
200 OK, with the asset and deletedAt set:
{
"object": "asset",
"assetId": "ast_3d8f1b6a9c2e",
"productId": "prod_8f2a1c9b4d6e",
"type": "STATIC",
"url": "https://storage.googleapis.com/.../ast_3d8f1b6a9c2e.png",
"thumbnailUrl": null,
"favorited": true,
"status": "active",
"name": "Front view",
"width": 2048,
"height": 2048,
"durationSec": null,
"playerUrl": null,
"embedCode": null,
"createdAt": "2026-09-12T10:04:31.000Z",
"metadata": { "externalId": "dam-88421", "license": "owned" },
"notes": "Hero shot for the autumn campaign.",
"deletedAt": "2026-10-01T08:30:12.000Z"
}
Restore it with POST /assets/{assetId}/undelete:
curl -s -X POST https://api.mocart.io/api/public/v1/assets/ast_3d8f1b6a9c2e/undelete \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: b81e5d07-2a94-4c63-8f1d-7e0a3c9b5d26" \
-d '{}'
The asset comes back exactly as it was: status, notes and metadata untouched. Undeleting an asset that is
already live is a 200 that changes nothing.
One case is a 404 ASSET_NOT_FOUND: if the asset's product no longer exists, there is nowhere to
restore the asset to.
Read deleted assets — ?showDeleted=true
GET /assets and GET /assets/{id} hide deleted assets unless you ask:
curl -s "https://api.mocart.io/api/public/v1/assets?showDeleted=true" \
-H "Authorization: Bearer $API_KEY"
With showDeleted=true, every asset in the response carries deletedAt: an ISO 8601 time for a deleted
asset, null for a live one. Without the parameter the response has no deletedAt. Any value other than
true is treated as if you had left it out; it is never an error.
Guard a write with If-Match
GET /assets/{id} returns a strong ETag: a fingerprint of the asset as you just read it. Send it back
as If-Match on any write, and the write only goes through if the asset is still exactly that. If someone
changed it in between, you get 412 PRECONDITION_FAILED and nothing is changed. On a 412, read the
asset again, decide whether your change still makes sense, and retry with the new ETag.
curl -s -X PATCH https://api.mocart.io/api/public/v1/assets/ast_3d8f1b6a9c2e \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "4a7e2c9d1b6f83e05a4c7d92f1b0e8a36c5d4f70e9a2b1c8d7f6e5a4b3c2d1e0"' \
-d '{ "favorited": false }'
If-Matchis optional. Leave it out to write regardless of what changed.If-Match: *matches any asset that exists.- Send the
ETagexactly as you received it, quotes included. A value starting withW/never matches. GET /assets/{id}?showDeleted=trueon a live asset returns a weakETagyou can't use forIf-Match; read it without the parameter. On a deleted asset it returns the strong one.
Retries and Idempotency-Key
| Operation | Idempotency-Key | Same key, same request |
|---|---|---|
PATCH /assets/{id} | Optional | 200, the asset; nothing recorded again |
DELETE /assets/{id} | Optional | 200, the original delete answer |
POST .../archive, .../unarchive, .../undelete | Required | 200, the asset; nothing recorded again |
Without a key, a PATCH or DELETE simply runs. A DELETE you retry without a key after a lost response
finds the asset already deleted and gets 404; send a key, and the retry returns the original answer
instead. Rules for the key itself (16–255 characters, IDEMPOTENCY_KEY_REUSED,
IDEMPOTENCY_IN_PROGRESS) are in Idempotency.
Handling each error
Every error is application/problem+json except 429. Branch on code.
| Status | code | What happened | What to do |
|---|---|---|---|
400 | IDEMPOTENCY_KEY_REQUIRED | archive, unarchive or undelete without an Idempotency-Key. | Add one. |
400 | INVALID_IDEMPOTENCY_KEY | The key is under 16 or over 255 characters, or the header was sent twice. | Send exactly one header, 16–255 characters. |
401 | — | The key is missing, unknown, revoked or expired, or the account no longer has API access. | Mint a new key. See Authentication. |
403 | API_TOKEN_SCOPE_DENIED | The key doesn't carry assets:write. | Mint a key with assets:write. Scopes can't be added to an existing key. |
404 | ASSET_NOT_FOUND | No asset with this id that you can change: it doesn't exist, belongs to another account, is deleted (except for DELETE and undelete), has no usable file, or — on undelete — its product no longer exists. | Check the id against GET /assets. Don't retry unchanged. |
409 | IDEMPOTENCY_KEY_REUSED | This key was already used for a different request. | Use a new key. |
409 | IDEMPOTENCY_IN_PROGRESS | An earlier attempt under this key hasn't finished. | Wait Retry-After, then resend the identical request with the same key. |
412 | PRECONDITION_FAILED | If-Match no longer matches: the asset changed since you read it. Nothing was changed. | Read the asset again and retry with the new ETag. |
422 | VALIDATION_ERROR | The PATCH body is invalid: an unknown field (status is never writable), notes over 500 characters, or a bad metadata key, value or key count. errors[] names the field. | Fix the body. |
429 | — (plain-text body) | Too many writes in the current window. | Wait Retry-After, then resend. See Rate limits. |
503 | WRITES_PAUSED | Writes are paused for your account. Nothing changed and the key wasn't used. | Wait Retry-After, then resend with the same key. See Reviewing creatives. |
Rate limits
Asset edits, archives, deletes and restores share one budget of 60 requests per minute per key, separate
from reads and from creative writes. Read the RateLimit headers rather than hard-coding it — see
Rate limits.
The SDK version
The same flow using @mocart-io/api 1.6.0 or later. It is
adapted from docs-site/snippets/manage-assets.ts in the repo, type-checked against the SDK by
npm run sdk:check.
import { randomUUID } from 'node:crypto';
import {
client,
updateAsset,
archiveAsset,
unarchiveAsset,
deleteAsset,
undeleteAsset,
type Asset,
} from '@mocart-io/api';
client.setConfig({
baseUrl: 'https://api.mocart.io/api/public/v1',
auth: () => process.env.MOCART_API_KEY,
});
function failure(action: string, error: unknown): Error {
// A 429 body is plain text, not a problem+json object.
return new Error(
`${action} failed: ${typeof error === 'object' && error !== null && 'code' in error ? error.code : 'rate limited'}`,
);
}
/** Shortlist an asset and label it. `metadata: { key: null }` would remove a key. */
export async function shortlist(assetId: string, externalId: string): Promise<Asset> {
const { data, error } = await updateAsset({
path: { id: assetId },
body: { favorited: true, metadata: { externalId } },
});
if (!data) {
throw failure('updateAsset', error);
}
return data;
}
/** Archive or unarchive. One key per logical request; reuse it if you retry. */
export async function setArchived(assetId: string, archived: boolean): Promise<Asset> {
const request = { path: { assetId }, headers: { 'Idempotency-Key': randomUUID() } };
const { data, error } = archived ? await archiveAsset(request) : await unarchiveAsset(request);
if (!data) {
throw failure(archived ? 'archiveAsset' : 'unarchiveAsset', error);
}
return data;
}
/** Delete with a key, so a retry after a lost response returns the original answer instead of a 404. */
export async function removeAsset(assetId: string): Promise<Asset> {
const { data, error } = await deleteAsset({ path: { id: assetId }, headers: { 'Idempotency-Key': randomUUID() } });
if (!data) {
throw failure('deleteAsset', error);
}
return data;
}
export async function restoreAsset(assetId: string): Promise<Asset> {
const { data, error } = await undeleteAsset({ path: { assetId }, headers: { 'Idempotency-Key': randomUUID() } });
if (!data) {
throw failure('undeleteAsset', error);
}
return data;
}
Webhooks
Every one of these changes — from the API or the dashboard — can notify your own endpoint:
asset.updated, asset.archived, asset.unarchived, asset.deleted and asset.restored. See
Webhooks.