API Endpoints
All v1 endpoints are mounted under https://your-freedam-instance.com/api/v1 and require an API bearer token. Every request is rate limited (60/minute per token) and audited.
For authentication and token scopes see API Authentication. For rate-limit headers and the back-off contract see Rate Limiting. For the signed image transformation URLs (/ip/, /is/) and other asset delivery URLs that live outside /api/v1, see Image Transformations.
Conventions
- Base URL -
https://your-freedam-instance.com/api/v1 - Auth header -
Authorization: Bearer <your-token> - JSON body - send
Content-Type: application/jsononPOST/PUT, except file uploads, which usemultipart/form-data. - Asset identifier - assets are addressed by their GAID (Global Asset ID), a stable public id such as
IM-20260309-UDXZH8PP6VPW. Every field that names an asset takes GAIDs, and responses return them:asset_gaid,asset_gaids,cover_asset_gaid. - Numeric routes -
{collection},{webhook},{batch}and{share}take numeric ids. - Errors - validation errors return
422with amessageand anerrorsobject keyed by field. A missing ability returns403withrequired_abilitiesbefore the id in the path is looked up, so the answer is the same whether that resource exists or not. See Authentication.
Pagination
List endpoints accept page and per_page. Two response shapes exist:
| Endpoints | Default per_page |
Shape |
|---|---|---|
| Assets, collections | 24 (max 100) | data, links, meta with current_page, last_page, per_page, total |
| Shares, operations | 20 | data, links, meta |
| Webhooks, webhook deliveries | 20 | Flat: data, current_page, last_page, per_page, total, next_page_url at the top level |
| Consent history | 50 | data, meta with current_page, per_page, total |
Endpoint catalogue
User
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/v1/user |
any | Authenticated user: id, name, email. |
Tokens
Ability: admin:full, and the token's user needs the admin.api_management permission. Token management for the user who owns the token. A token without admin:full, or whose user lacks the permission, gets 403 on all three endpoints. See Managing tokens through the API.
| Method | Path | Description |
|---|---|---|
GET |
/v1/tokens |
List your tokens with abilities, last_used_at, last_used_ip and expires_at. |
POST |
/v1/tokens |
Create a token. The plain-text token is returned once. |
DELETE |
/v1/tokens/{token} |
Revoke a token immediately. |
POST /v1/tokens body:
| Field | Rules |
|---|---|
name |
Required string. |
abilities |
Required array of ability names. You can't grant abilities your own role doesn't allow, or that the token making the request doesn't hold. |
description |
Optional string. |
expires_at |
Optional date in the future. When the token making the request has an expiration date, required and no later than that date. |
Assets
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/v1/assets |
assets:read |
Search and list assets. |
GET |
/v1/assets/{gaid} |
assets:read |
One asset with its technical, descriptive and AI fields. |
PUT |
/v1/assets/{gaid} |
assets:write |
Update an asset. Fields you omit are left unchanged. |
DELETE |
/v1/assets/{gaid} |
assets:delete |
Move an asset to the Trash. |
Results only include assets the token's user is allowed to see. Assets you cannot view answer 404 on GET, PUT and DELETE /v1/assets/{gaid}, exactly like an asset that does not exist.
Assets that share the value they are sorted by, such as the files of one upload created within the same second, are ordered by descending internal id, so a page never repeats or skips an asset. When q is set, meta.search holds query, did_you_mean, total_results and has_filters.
Every asset carries a media_urls object with thumbnails, base_preview, original, hls_master, converted_pdf and download. Each value is a signed, short-lived URL, or null when it does not apply to the asset. thumbnails is an object keyed by tier (w_128 to w_5120), and is null for audio and for any asset that has no rendition: one still being processed, one whose preview generation failed, or an animated image that skips preview generation. base_preview is null in the same cases. Use original or download for those assets. See Image Transformations.
capture_date is the date and time the camera's clock recorded, without a UTC offset (2024-05-01T23:30:00), because Exif stores no time zone for it. A date read from the filename is that day at 00:00:00. Unlike created_at and the other instants, it is not converted to UTC. To filter on it, send a day (2024-05-01) or the value as returned: a value with a UTC offset is converted to UTC first.
Search parameters for GET /v1/assets
| Parameter | Description |
|---|---|
q |
Full-text and semantic search, up to 500 characters. |
sort_by |
created_at (default), updated_at, title, original_filename, size, or relevance (default when q is set). |
sort_direction |
asc or desc (default). |
asset_class_id |
Only assets of this asset class. |
mime_type |
MIME type or part of one, such as image/jpeg or video. |
ingestion_batch_id |
Only assets from this upload batch. |
filters |
Advanced rules as a JSON string, see below. |
page, per_page |
Pagination, per_page up to 100. |
filters accepts a single rule or nested and/or groups:
{
"type": "group",
"operator": "and",
"rules": [
{ "type": "filter", "field": "asset.file_category", "operator": "equals", "value": "Image" },
{ "type": "filter", "field": "asset.width", "operator": "greater_than", "value": 1920 },
{ "type": "filter", "field": "metadata.campaign", "operator": "contains", "value": "Spring" }
]
}
- Operators:
equals,not_equals,contains,not_contains,starts_with,ends_with,greater_than,greater_than_or_equal,less_than,less_than_or_equal,in,not_in,between([min, max]),is_empty,is_not_empty,color_equals,color_near. - Common fields:
asset.title,asset.original_filename,asset.mime_type,asset.file_category(Image,Video,Audio,Document),asset.extension,asset.width,asset.height,asset.file_size,asset.created_at,asset.updated_at,asset.capture_date,asset.creator,asset.copyright,asset.keywords,asset.ai_caption,asset.ai_tags,asset.ocr_text,asset.workflow_status,asset.collections(within/not_in), andmetadata.{key}for custom fields.
Invalid JSON or an invalid rule tree returns 422.
Body for PUT /v1/assets/{gaid}
| Field | Rules |
|---|---|
title |
String, up to 255 characters. |
description |
String up to 2,000 characters, or null. |
keywords |
Array of strings, each up to 100 characters. Replaces the existing keywords. |
ai_caption, ocr_text |
String up to 5,000 characters, or null. |
ai_tags |
Array of strings. |
metadata |
Object of custom field values keyed by the field's key, for example {"campaign": "Spring 2026", "product_line": "outdoor"}. |
workflow_status |
The status to move the asset to, when its workflow allows that transition. |
workflow_transition_confirmed |
Boolean. Send true with workflow_status when the transition is marked Confirmation required. |
workflow_transition_reason |
String up to 1,000 characters. Send it with workflow_status when the transition is marked Log reason from user; it is kept in the workflow history. |
asset_class_id |
Move the asset to another asset class. |
Any other field is refused with 422 and an error keyed by that field. A workflow_status the workflow does not allow from the current status returns 422 with a workflow_status error. So does a transition sent without the confirmation or the reason it asks for: the error names the missing field, and no other field of the request is saved.
curl -X PUT https://your-freedam-instance.com/api/v1/assets/IM-20260309-UDXZH8PP6VPW \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Hero shot Q3", "keywords": ["outdoor", "summer"], "metadata": {"campaign": "Spring 2026"}}'
The response is the updated asset in data. Every successful update sends an asset.updated webhook.
Video Embeds
Generate iframe embed codes for video assets. Each call mints (or reuses) a long-lived public share dedicated to embedding so you can drop the returned iframe_html straight into a CMS, blog post, or third-party site. Embed shares are isolated from regular /v1/shares and cannot be reached through that endpoint.
Required abilities: shares:read for GET, shares:manage for POST and DELETE.
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/v1/assets/{gaid}/embed |
shares:read |
Return the existing embed code, or 404 if none. |
POST |
/v1/assets/{gaid}/embed |
shares:manage |
Generate or refresh an embed code. |
DELETE |
/v1/assets/{gaid}/embed |
shares:manage |
Revoke the embed code. |
Only assets with a video/* MIME type can be embedded; calling these endpoints on any other asset returns 422. An asset you cannot view answers 404 on all three endpoints, whatever its type, exactly like an asset that does not exist.
An embed is a public share, so POST also checks what you may publish at the time of the call, like POST /v1/shares: you need the assets.share permission (a shares:manage token no longer creates embeds once your role loses it) and a clean download of the original video, with no watermark or download request (as for POST /v1/shares), and the asset's rights policy must allow publish. A refused POST answers 403, with "error": "rights_blocked" and the blocked_assets when a rights policy is the reason, and creates, refreshes or revokes nothing. Once those checks pass, a workspace that requires accepting share terms of use answers 409 with "error": "consent_required" until the token's user has accepted them in Freedam, again without touching an existing embed. GET and DELETE need only the token ability, so you can still look up and revoke an embed you created.
Request body for POST (all fields optional)
| Field | Type | Default | Notes |
|---|---|---|---|
width |
integer | 640 |
Player width in pixels for the non-responsive iframe (160–3840). |
height |
integer | 360 |
Player height in pixels (90–2160). |
responsive |
boolean | true |
If true, the response also includes a responsive_html snippet. |
autoplay |
boolean | false |
Browsers force muted when autoplay is on. |
muted |
boolean | false |
Start muted. |
loop |
boolean | false |
Restart playback on end. |
controls |
boolean | true |
Show the player chrome. |
start |
integer | 0 |
Start offset in seconds. |
allowed_domains |
array of string | null |
Restrict the iframe to these origins via CSP frame-ancestors. |
force_new |
boolean | false |
Revoke the existing embed share and mint a fresh one. |
Response
{
"data": {
"token": "shr_…",
"asset_gaid": "A_01HXYZ…",
"embed_url": "https://your-freedam-instance.com/embed/shr_…/A_01HXYZ…?autoplay=1",
"iframe_html": "<iframe src=\"…\" width=\"640\" height=\"360\" …></iframe>",
"responsive_html": "<div style=\"position:relative;padding-bottom:56.25%…\">…</div>",
"width": 640,
"height": 360,
"expires_at": "2027-05-06T12:00:00+00:00",
"allowed_domains": null,
"options": {
"autoplay": false,
"muted": false,
"loop": false,
"controls": true,
"start": 0
}
}
}
A repeated POST for the same asset returns 200 with the existing share (and refreshes its expiry when it is within 30 days of expiring); a fresh share returns 201.
Collections
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/v1/collections |
collections:read |
List collections you can view. |
GET |
/v1/collections/{collection} |
collections:read |
One collection. |
POST |
/v1/collections |
collections:write |
Create a collection. |
PUT |
/v1/collections/{collection} |
collections:write |
Update a collection. |
DELETE |
/v1/collections/{collection} |
collections:delete |
Delete a collection. |
POST |
/v1/collections/{collection}/assets |
collections:write |
Add assets. |
DELETE |
/v1/collections/{collection}/assets |
collections:write |
Remove assets. |
- List filters:
search(part of the name),collection_type_id,parent_id(children of that collection),root_only=1(top-level collections only),is_active,sort_by(defaultcreated_at),sort_direction(ascordesc, defaultdesc),page,per_page(up to 100). - Create body:
name(required),collection_type_id(required),description,parent_id(a collection you can view),is_active,is_public,cover_asset_gaid(an asset you can view). - Update body: any of
name,description,parent_id,is_active,is_public,cover_asset_gaid. Send"cover_asset_gaid": nullto remove the cover. - Parent collection: a
parent_idyou cannot view is refused with the same422parent_iderror as an id that does not exist. A parent you can view but may not add collections to returns403. Sending a collection's currentparent_idagain leaves it unchanged. - Hierarchy in responses:
childrenlists only the sub-collections you can view.parentisnullwhen you cannot view the parent collection, whileparent_idstill names it. - Add assets:
POSTa JSON body{"asset_gaids": ["IM-…", "VI-…"]}, with 1 to 500 GAIDs per request. - Remove assets:
DELETEwith the GAIDs in the query string:?asset_gaids[]=IM-…&asset_gaids[]=VI-…. - Delete: a collection that cannot be deleted because of a conflict returns
409. - Collections you cannot view answer
404on every{collection}endpoint, exactly like a collection that does not exist.
Adding and removing need assets.view, and every GAID must name an asset you can view. If one is unknown, in the Trash or hidden from you by access rules, nothing is added or removed: the request returns 422 with a single asset_gaids error that does not say which GAID was refused. Otherwise the response contains a message such as "2 asset(s) added to collection.", the number of assets that succeeded, the number that failed (already in the collection when adding, not in it when removing) and any errors that stopped the change. Check them rather than assuming every GAID was processed.
Uploads
Ability: uploads:manage, and the token's user needs the assets.upload permission ("Upload assets"). A token keeps its abilities when the permission is removed from its owner's role, so both are checked on every call: without the permission, creating a batch and uploading a file answer 403. A collection_id also needs the right to add assets to that collection. Uploading is a three-step flow.
| Method | Path | Description |
|---|---|---|
POST |
/v1/uploads/batches |
Create an upload batch. |
POST |
/v1/uploads/batches/{batch}/files |
Upload one file into the batch. |
GET |
/v1/uploads/batches/{batch} |
Read the batch status and its file counters. |
- Create a batch. All fields are optional defaults for the files in the batch:
batch_name,asset_class_id,collection_id,target_workflow_status,title,description,keywords(array),creator,copyright,location,metadata(object keyed by field key). The response (201) contains the batchidandbatch_uuid. - Upload each file as
multipart/form-data, one file per request, with the binary infile. You can override the batch defaults per file withtitle,description,creator,copyright,location,asset_class_idandcollection_id. Because multipart forms can't nest values, sendkeywordsas a JSON array string (["landscape","sunset"]) or a comma-separated string (landscape,sunset), andmetadataas a JSON object string. The response contains amessage, theingestion_job_idof the file and thebatch_uuid. When the workspace storage is full, the request returns413with"error_type": "storage_limit_exceeded"and the file is not stored. - Poll the batch until
is_in_progressisfalse. The batch reports itsstatus,total_files,processed_files,successful_files,failed_files,is_in_progress,is_completed,has_failed,started_at,completed_at,failed_atanderror_message. It does not list the individual files.
Batch status is one of:
status |
Meaning | Flags |
|---|---|---|
draft |
Created, no file received yet. | is_in_progress |
processing |
Files are being processed. | is_in_progress |
completed |
Every file was processed. | is_completed |
completed_with_failures |
Some files were processed and some failed. | is_completed |
failed |
Every file failed, or the batch could not be processed. | has_failed |
cancelled |
The batch was cancelled. | none |
A file that fails is retried by the queue first. It is counted in failed_files once, when the last attempt has failed, and the batch stays in processing until then. error_message is only set on a failed batch, as a short fixed sentence. The cause of the failure of an individual file is not returned by the API.
Files are uploaded one request at a time, so a batch can finish before you send the next file. Uploading another file to a completed or completed_with_failures batch returns it to processing.
curl -X POST https://your-freedam-instance.com/api/v1/uploads/batches/42/files \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "[email protected]" \
-F "title=Hero shot Q3" \
-F 'keywords=["outdoor","summer"]'
Instead of polling, subscribe to the upload.batch_completed and upload.batch_failed webhooks. upload.batch_completed is sent each time the batch finishes, so it can be sent more than once for a batch that received more files afterwards. To find the assets a batch created, list assets with ingestion_batch_id.
- Target collection:
collection_id, on the batch or on a file, must name a collection you can view. Any other id is refused with the same422collection_iderror as an id that does not exist. A collection you can view but may not add assets to returns403, and no batch is created or file stored. - Batches of other users answer
404on both{batch}endpoints, exactly like a batch that does not exist.
Metadata
Ability: metadata:read. A read-only catalogue of field definitions, vocabularies and terms, to build your own forms without hard-coding workspace-specific values.
| Method | Path | Description |
|---|---|---|
GET |
/v1/metadata/definitions |
Field definitions. Filter with asset_class_id. |
GET |
/v1/metadata/vocabularies |
Vocabularies with id, name, code, is_tree. Filter with search (part of the name). |
GET |
/v1/metadata/vocabularies/{vocabulary} |
One vocabulary with its terms. |
GET |
/v1/metadata/vocabularies/{vocabulary}/terms |
Top-level terms of a vocabulary. For tree vocabularies, pass parent_id to list the children of a term, or root_only=0 to list every term. Filter with search (part of a label). |
GET |
/v1/metadata/vocabularies/{vocabulary}/terms/{term} |
One term. |
Use the definition key as the property name in metadata objects when updating assets or uploading.
Shares
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/v1/shares |
shares:read |
Shares created by the token's user. Filter with shareable_type. Only shares that are active now are listed; pass include_inactive=1 to add paused, revoked, expired and not yet started ones. |
GET |
/v1/shares/{share} |
shares:read |
One share. |
POST |
/v1/shares |
shares:manage |
Create a share link. |
PUT |
/v1/shares/{share} |
shares:manage |
Update access, permissions, dates or is_active. |
DELETE |
/v1/shares/{share} |
shares:manage |
Delete a share. |
POST |
/v1/shares/{share}/revoke |
shares:manage |
Stop a share immediately. |
POST /v1/shares body:
| Field | Rules |
|---|---|
shareable_type |
Required: collection, asset or selection. |
shareable_id |
For collection: the collection's numeric id. |
asset_gaid |
For asset: the asset's GAID. |
asset_gaids |
For selection: an array of asset GAIDs. |
access_type |
Required: public, email_restricted, password_protected or private. |
allowed_emails, allowed_domains |
Arrays. At least one of them is required for email_restricted. |
password |
At least 6 characters, for password_protected. |
permissions |
{"view": true, "download": false} |
download_resolution |
original, w_5120, w_4096, w_2048, w_1024, w_512, w_256 or w_128. |
watermark_id |
A watermark id, when your plan includes watermarks. |
starts_at, expires_at |
ISO 8601 dates such as 2027-05-06T12:00:00+02:00, also when sent with PUT. A date without a UTC offset is read as UTC, and responses return both in UTC. expires_at must be after starts_at. |
acknowledge_rights_warnings |
true to create the share even when rights policies block some assets. |
A share created through the API behaves exactly like one created in Freedam:
- You need the right to share what you send:
assets.share("Share publicly") for an asset or a selection; for a collection, bothassets.shareandcollections.share("Share own collections") on a collection you created, or on any collection you can see if you also holdcollections.manage. In every case you also need a clean download of every shared asset's original. An asset your access rules hide, keep from download, watermark (previews or downloads), limit beloworiginalor put behind a download request, or whose rights policy requires a watermark on downloads, refuses the share with403. An unknown GAID is refused with the same403as an asset you may not share. - Changing
access_type,permissions,download_resolutionorwatermark_idwithPUTruns the same check on the shared assets for the token's user, and answers403without changing the share when it fails. Resending a stored value is not a change. - Rights policies are checked: a
publicshare needs thepublishaction on every asset, other access typesdistribute. A blocked share returns403with"error": "rights_blocked"and theblocked_assets(with theirasset_gaid). Resend withacknowledge_rights_warnings: trueto create it anyway; blocked assets stay hidden from recipients. - If your workspace requires accepting share terms of use, the request returns
409with"error": "consent_required"until the token's user has accepted them in Freedam. - Each address in
allowed_emailsreceives an invitation email, and theshare.createdwebhook is sent. Adding addresses withPUTinvites the new ones and tells existing recipients what changed.
Share responses identify what is shared with shareable_type plus shareable_id (collections), asset_gaid (a single asset) or asset_gaids (a selection). See Share Links for how each option behaves for recipients.
Shares you cannot view, those created by another user unless you have the admin.shares permission, answer 404 on every {share} endpoint, exactly like a share that does not exist.
Operations
Long-running asynchronous operations. POST /v1/operations accepts an optional Idempotency-Key header. Sending it makes a retry safe: replaying a key you already used returns the original operation with 200 and "idempotent": true instead of creating a new one. A new operation returns 201. The body is {"type": "...", "params": {...}}. Any type not listed below is rejected with 422, and a token without the ability of the type gets 403 with required_abilities.
type |
What it does | params |
Ability |
|---|---|---|---|
asset_upload_batch |
Creates an upload batch to send files into. | Optional: batch_name, asset_class_id (integer), collection_id, target_workflow_status |
uploads:manage |
asset_bulk_update |
Applies the same metadata updates to many assets. | Required: asset_gaids (array of at most 500 GAIDs), updates (object) |
assets:write |
| Method | Path | Description |
|---|---|---|
GET |
/v1/operations |
Your operations. Filter with type and status. |
POST |
/v1/operations |
Start an operation. |
GET |
/v1/operations/{operation} |
Status, progress_percentage, current_stage, result and error. |
POST |
/v1/operations/{operation}/cancel |
Cancel a pending or processing operation. |
Operation status is one of pending, processing, completed, partially_completed, failed or cancelled.
asset_bulk_update only changes the assets the token's user can view and edit. The operation's result lists every other GAID in errors as {"asset_gaid": "…", "error": "Asset not found or not updatable."}, whether the asset does not exist or you may not edit it; the operation then ends as partially_completed or failed.
updates takes the fields of PUT /v1/assets/{gaid} with the same rules, and the operation fails with VALIDATION_FAILED before any asset changes when a field is unknown, has the wrong type, or names a metadata field you may not edit. Each asset is updated as a whole or not at all: an asset reported in errors keeps its previous values. When no asset could be updated, the error code says why: SERVICE_UNAVAILABLE when an unexpected error stopped an update (retry later), VALIDATION_FAILED when a workflow transition was refused, and INSUFFICIENT_PERMISSIONS when no GAID named an asset you may edit.
Webhooks
Each token manages the webhook endpoints of its own user. A webhook endpoint of another user answers 404 on every {webhook} endpoint, exactly like one that does not exist. See Webhooks for bodies, events and signatures.
Every route needs webhooks:manage. Routes marked "+ events" also need the ability of every event the endpoint subscribes to (for PUT, the events it has once updated), listed per event in Available events, and the permission behind it. * needs all of them. A missing ability returns 403 with the missing ones in required_abilities.
| Method | Path | Ability |
|---|---|---|
GET |
/v1/webhooks |
webhooks:manage |
POST |
/v1/webhooks |
webhooks:manage + events |
GET |
/v1/webhooks/{webhook} |
webhooks:manage |
PUT |
/v1/webhooks/{webhook} |
webhooks:manage + events |
DELETE |
/v1/webhooks/{webhook} |
webhooks:manage |
POST |
/v1/webhooks/{webhook}/test |
webhooks:manage |
GET |
/v1/webhooks/{webhook}/deliveries |
webhooks:manage + events |
POST |
/v1/webhooks/{webhook}/rotate-secret |
webhooks:manage + events |
A delivery only reaches an endpoint whose owner may see what the event is about, checked when the event happens. See Who receives an event.
The url of an endpoint must be on a public host: POST and PUT return 422 for a private, loopback or internal host, and a delivery whose host resolves to a private address is refused without retries. See URL requirements.
Consents
Compliance and audit endpoints. Ability: consents:read. The two /users/{user} endpoints also need the admin.terms_of_use permission on the token's user, and answer 403 without it.
| Method | Path | Description |
|---|---|---|
GET |
/v1/consents/me |
Terms of use you have accepted and those still pending. |
GET |
/v1/consents/users/{user} |
The same for another user, by numeric id. |
GET |
/v1/consents/users/{user}/history |
The append-only acceptance history for a user, newest first, paginated (per_page defaults to 50). |
OpenAPI specification
A machine-readable OpenAPI 3.1 spec for every endpoint above is available at /docs/api.json. It is the source of truth used to generate the official SDKs. Generate a client in any language with openapi-generator or Kiota.