Media Library
Standalone media endpoints for bulk upload, moderation-first workflows, and n:m product linking.
Media items live in a creator's library independently of products. Products reference one or more media rows through the product_media pivot.
List media library
GET /api/media
Returns paginated media owned by the authenticated creator.
Authentication: Required
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
limit | integer | Items per page (default: 20, max: 100) |
status[] | string | Moderation status: pending, approved, rejected (a scalar status=approved works too) |
media_type[] | string | image, video |
consent[] | string | Consent bucket: required = the owner has to act (review_open, declined, consent_rescinded), pending = consent_requested, provided = not_required or consent_provided |
folder_id | integer | Vault folder (scope=vault, see Content Folders) |
unlinked | boolean | Only media not linked to any product |
live | boolean | Approved by moderation and consent cleared - what a sellable product can be built from |
standard_only | boolean | Only non-AI media (ai_origin: none) |
ai_only | boolean | Only AI media |
sort | string | newest (default) or oldest |
search | string | Title search, max 120 characters |
Rate limit: 60 requests per minute per key and IP, plus 120 per minute per IP across all keys (shared with GET /api/media/{media}).
Response (200)
{
"success": true,
"errors_message": null,
"data": {
"data": [
{
"id": 101,
"type": "image",
"title": "Beach photo",
"description": null,
"status": "pending",
"ai_origin": "none",
"ai_classified": true,
"ai_label": null,
"ai_reclassification_requested_at": null,
"is_should_consent": false,
"consent": { "status": "not_required", "required": 0, "fulfilled": 0 },
"preview": "https://...",
"thumbnail": "https://...",
"preview_blurred": "https://...",
"source": null,
"folder_id": null,
"folder": null,
"product_ids": [],
"is_linked": false,
"created_at": "2026-06-10T10:00:00+00:00",
"updated_at": "2026-06-10T10:00:00+00:00"
}
],
"pages_total": 1,
"total": 1,
"per_page": 20,
"current_page": 1
}
}Field notes:
ai_origin:none,synthetic,real_person_selforreal_person_other- see AI content.ai_labelis the buyer-facing disclosure (AI generated,AI modified) ornull.consent.status:not_required,review_open,consent_requested,consent_provided,declined,consent_rescinded;required/fulfilledcount the consent slots on the file.thumbnailis the 768px tile;previewis the full-size image for photos.sourceisnullin the list and filled byGET /api/media/{media}.
Single media item
GET /api/media/{media_id}
Returns one library item with the full source URL, plus linked_products[] (id, title, media_count, would_be_deleted) for the products that use it.
Authentication: Required (owner only). Rate limit 60/min.
Create media from upload session
POST /api/media
Creates a library media item from a finalized upload session without creating a product.
Authentication: Required
Request Body
{
"upload_session_id": "01knyp9pd50ey9xrpgp1nv0jn0",
"title": "Optional title",
"description": "Optional description",
"folder_id": 12,
"is_should_consent": false,
"ai_origin": "none",
"ai_attested": false
}| Field | Type | Required | Notes |
|---|---|---|---|
upload_session_id | string | Yes | A session in status ready. |
title, description | string | No | |
folder_id | integer | No | A vault folder of the creator (scope=vault). |
is_should_consent | boolean | No | Mark the file as needing another person's consent (tag them afterwards). |
ai_origin | string | See note | none, synthetic, real_person_self, real_person_other. Required for generated keys with upload_ai_content unless the upload session already carries it. See AI content. |
ai_attested | boolean | With AI values | Must be true for every value other than none - unless the session already carries the attestation; then it may be omitted here even when ai_origin is repeated. An explicit false is refused. |
Response (201)
Returns a MediaLibraryResource object. Moderation runs on the media row (status: pending until approved).
Errors
422 with errors_code: ai_origin_required when the classification is missing, ai_attestation_required when an AI value comes without ai_attested: true and the session carries no attestation. The session is left untouched; repeat the call with the values.
Bulk upload sessions
POST /api/media/bulk
Starts multiple upload sessions with shared defaults. Use the returned session IDs with the normal upload session flow, then call POST /api/media per file after finalization.
Authentication: Required
Request Body
{
"files": [
{
"file_name": "pic_01.jpg",
"file_size_bytes": 2048000,
"mime_type": "image/jpeg"
},
{
"file_name": "pic_02.jpg",
"file_size_bytes": 1800000,
"mime_type": "image/jpeg"
}
],
"defaults": {
"is_adult_content": true,
"is_downloadable": false,
"is_verif_age": false,
"folder_id": 12,
"ai_origin": "none",
"ai_attested": false
}
}Up to 50 files per call. defaults.ai_origin / defaults.ai_attested put the AI answer on every session of the batch, so the later POST /api/media calls need not repeat it.
Response (201)
{
"success": true,
"errors_message": null,
"data": [
{ "upload_session_id": "01abc...", "session_type": "device_multipart", "staging_key": "staging/..." },
{ "upload_session_id": "01def...", "session_type": "device_multipart", "staging_key": "staging/..." }
]
}Update media
PATCH /api/media/{media_id}
Edit title, description, or folder assignment on a library item.
Authentication: Required (owner only)
Request Body
{
"title": "Updated title",
"description": "Updated description",
"folder_id": 12,
"is_should_consent": true
}is_should_consent: true marks the file as needing consent ("request consent later"); it cannot be switched back to false here - the sale stays blocked until a person is tagged and has accepted. A folder_id that is not one of the creator's own vault folders is refused with 422.
Correct the AI classification
PATCH /api/media/{media_id}/ai-origin
{ "ai_origin": "synthetic", "ai_attested": true, "reason": "optional note" }Changes the file's ai_origin after the fact: versioned and audited, the file is moderated again under the matching ruleset and the product flag follows (exception: a never-classified file from before the AI feature that is set to none is only stamped). An AI value needs ai_attested: true (422, errors_code: ai_attestation_required); correcting back to none does not. Returns the updated media item. Owner only (404 otherwise). Rate limit 60/min per account. See AI content.
Consent tags on a media item
GET /api/media/{media_id}/consent-tags - the consent slots of one file (tagged person, status, who requested it).
POST /api/media/{media_id}/consent-tags - declare who is in the file:
| Field | Type | Required | Notes |
|---|---|---|---|
date | date | Yes | Date the consent was given. |
tagged_creator_ids[] | integer[] | One of | Registered creators (find them with GET /api/creators/search?q=). tagged_creator_id for a single one. |
email | string | One of | A registered address is tagged directly. An unknown address needs tag_invite: true - Fangate emails an invitation, message (max 2000) is your text in that mail; without tag_invite the call answers 400. |
Answers 200 with an empty data; read the resulting tags with GET. Tagged creators answer via GET /api/consent-tags and PATCH /api/consent-tags/{id}. Until every tag is accepted, products containing the file are not purchasable.
Delete media
DELETE /api/media/{media_id}
Deletes a library item. Products that only had this file are deleted with it; other products just lose the file. The object is quarantined, not dropped immediately.
Link media to a product
POST /api/products/{product_id}/media
Attach existing library media to a product (n:m).
Request Body
{
"media_ids": [101, 102, 103]
}Unlink media from a product
DELETE /api/products/{product_id}/media/{media_id}
Removes the pivot link only. The media row remains in the library.
Create product from media library
POST /api/products
Accepts media_ids[] instead of uploading a new file:
{
"media_ids": [101, 102, 103],
"price": 1500,
"title": "Summer Bundle",
"is_adult_content": true,
"is_downloadable": false
}The legacy upload_session_id flow on POST /api/products remains supported as a compatibility shim.
Account upload defaults
PATCH /api/user/{user_id}
Store creator-level defaults used to pre-fill bulk uploads and product creation:
{
"upload_defaults": {
"is_adult_content": true,
"is_downloadable": false,
"is_verif_age": false,
"default_price": 500,
"folder_id": 12
}
}default_price is in cents (500 = $5.00). Returned on GET /api/user as upload_defaults.
Moderation
Moderation status is tracked on media rows (veriff_status), not on products. API status values map as:
| API status | Backend states |
|---|---|
pending | Unchecked |
approved | Checked, Manual Approval |
rejected | Manual Removal, AI Removal |
Purchasability still evaluates linked media on each product.