Skip to content

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

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 20, max: 100)
status[]stringModeration status: pending, approved, rejected (a scalar status=approved works too)
media_type[]stringimage, video
consent[]stringConsent bucket: required = the owner has to act (review_open, declined, consent_rescinded), pending = consent_requested, provided = not_required or consent_provided
folder_idintegerVault folder (scope=vault, see Content Folders)
unlinkedbooleanOnly media not linked to any product
livebooleanApproved by moderation and consent cleared - what a sellable product can be built from
standard_onlybooleanOnly non-AI media (ai_origin: none)
ai_onlybooleanOnly AI media
sortstringnewest (default) or oldest
searchstringTitle 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)

json
{
  "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_self or real_person_other - see AI content. ai_label is the buyer-facing disclosure (AI generated, AI modified) or null.
  • consent.status: not_required, review_open, consent_requested, consent_provided, declined, consent_rescinded; required / fulfilled count the consent slots on the file.
  • thumbnail is the 768px tile; preview is the full-size image for photos. source is null in the list and filled by GET /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

json
{
  "upload_session_id": "01knyp9pd50ey9xrpgp1nv0jn0",
  "title": "Optional title",
  "description": "Optional description",
  "folder_id": 12,
  "is_should_consent": false,
  "ai_origin": "none",
  "ai_attested": false
}
FieldTypeRequiredNotes
upload_session_idstringYesA session in status ready.
title, descriptionstringNo
folder_idintegerNoA vault folder of the creator (scope=vault).
is_should_consentbooleanNoMark the file as needing another person's consent (tag them afterwards).
ai_originstringSee notenone, 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_attestedbooleanWith AI valuesMust 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

json
{
  "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)

json
{
  "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

json
{
  "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

json
{ "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.


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:

FieldTypeRequiredNotes
datedateYesDate the consent was given.
tagged_creator_ids[]integer[]One ofRegistered creators (find them with GET /api/creators/search?q=). tagged_creator_id for a single one.
emailstringOne ofA 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.


POST /api/products/{product_id}/media

Attach existing library media to a product (n:m).

Request Body

json
{
  "media_ids": [101, 102, 103]
}

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:

json
{
  "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:

json
{
  "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 statusBackend states
pendingUnchecked
approvedChecked, Manual Approval
rejectedManual Removal, AI Removal

Purchasability still evaluates linked media on each product.

Fangate backend developer documentation