Skip to main content

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) — set favorited, write notes, attach your own metadata.
  • POST /assets/{assetId}/archive (archiveAsset) and POST /assets/{assetId}/unarchive (unarchiveAsset) — set an asset aside, and bring it back.
  • DELETE /assets/{id} (deleteAsset) and POST /assets/{assetId}/undelete (undeleteAsset) — delete an asset, and restore it.
  • ?showDeleted=true on GET /assets (listAssets) and GET /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."
}
FieldWhat it does
favoritedtrue shortlists the asset, false takes it off the shortlist.
notesA free-text note of up to 500 characters. "" clears it.
metadataYour 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 sendResult
"key": "value"The key is added, or its value is replaced.
"key": nullThe key is removed.
A key you leave outUntouched.
"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.

ArchiveDelete
What it means"Set this aside; I may want it back.""I don't want this."
What changesThe asset's status becomes archived.The asset is hidden: deletedAt is set.
Shown by GET /assetsNot 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 withunarchive, to the status it had before.undelete, exactly as it was.
Time limit on restoringNone.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-Match is optional. Leave it out to write regardless of what changed.
  • If-Match: * matches any asset that exists.
  • Send the ETag exactly as you received it, quotes included. A value starting with W/ never matches.
  • GET /assets/{id}?showDeleted=true on a live asset returns a weak ETag you can't use for If-Match; read it without the parameter. On a deleted asset it returns the strong one.

Retries and Idempotency-Key​

OperationIdempotency-KeySame key, same request
PATCH /assets/{id}Optional200, the asset; nothing recorded again
DELETE /assets/{id}Optional200, the original delete answer
POST .../archive, .../unarchive, .../undeleteRequired200, 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.

StatuscodeWhat happenedWhat to do
400IDEMPOTENCY_KEY_REQUIREDarchive, unarchive or undelete without an Idempotency-Key.Add one.
400INVALID_IDEMPOTENCY_KEYThe 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.
403API_TOKEN_SCOPE_DENIEDThe key doesn't carry assets:write.Mint a key with assets:write. Scopes can't be added to an existing key.
404ASSET_NOT_FOUNDNo 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.
409IDEMPOTENCY_KEY_REUSEDThis key was already used for a different request.Use a new key.
409IDEMPOTENCY_IN_PROGRESSAn earlier attempt under this key hasn't finished.Wait Retry-After, then resend the identical request with the same key.
412PRECONDITION_FAILEDIf-Match no longer matches: the asset changed since you read it. Nothing was changed.Read the asset again and retry with the new ETag.
422VALIDATION_ERRORThe 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.
503WRITES_PAUSEDWrites 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.