Products
Creator product and media management.
This surface covers:
- paginated content library reads
- product creation from upload sessions
- legacy direct uploads
- sales-link metadata updates
- folder assignment
- collection membership
- media deletion
- product deletion
Product resource
Important fields returned by product endpoints:
| Field | Type | Notes |
|---|---|---|
id | integer | Product id. |
type | string | Product media type inferred from primary media. |
title | string or null | Product title. |
preview | string or null | Preview URL generated by backend storage resolution. |
preview_blurred | string or null | Blurred preview URL. |
price | integer | Price in minor units, for example cents. |
in_collection | boolean | Current collection membership flag. |
link | string or null | Public sales link. |
link_clicks | integer or null | Sales-link view count. |
unlocks | integer | Successful purchase count. |
total_earnings | integer | Total earnings in minor units. |
folder_id | string or null | Assigned content folder id. |
folder | object or null | { id, name }. |
media | array | Array of media resources. |
is_adult_content | boolean | AVS flag. |
is_verif_age | boolean | Buyer age-check flag. |
is_epoch_enabled | boolean | Creator/provider state. |
is_should_consent | boolean | Third-party consent required flag. |
is_downloadable | boolean | Whether buyers can download. |
private_description | string or null | Returned only to the product owner. |
public_description | string or null | Buyer-visible description. |
Media resource
Each media[] item currently includes:
| Field | Type | Notes |
|---|---|---|
id | integer | Media id. |
type | string | image, video, etc. |
preview | string | Preview URL. |
preview_blurred | string | Blurred preview URL. |
veriff_status | string | Human-readable moderation label such as Unchecked, Approved, AI Removal, or Manual Approval. |
removal_description | string or null | Admin/moderation reason if present. |
source | string or null | Original media URL, only exposed when the current user can update the product. |
GET /api/products
Paginated content library endpoint for the authenticated creator.
- Auth required: Yes
- Roles: Authenticated creator
Query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
page | integer | No | Page number. |
limit | integer | No | Items per page. |
sort_by | string | No | newest, most_earnings, most_views, collection. |
folder_id | integer | No | Filter to one folder. |
folder_state | string | No | Currently documented and implemented special value: unassigned. |
filterFolder | string | No | Legacy compatibility filter. Accepts folder id or unassigned. |
Success response example
{
"success": true,
"errors_message": null,
"data": {
"pages_total": 3,
"collection_link": "https://fangate.info/AbCdEf1234",
"data": [
{
"id": 5155,
"type": "image",
"title": "Campaign set",
"preview": "https://fangate.info/storage/users/7/products/preview.png",
"preview_blurred": "https://fangate.info/storage/users/7/products/preview-blurred.png",
"price": 4400,
"in_collection": false,
"link": "https://fangate.info/94w24rWk3v",
"link_clicks": 0,
"unlocks": 0,
"total_earnings": 0,
"folder_id": "12",
"folder": {
"id": "12",
"name": "Campaigns"
},
"media": [],
"is_adult_content": true,
"is_verif_age": false,
"is_epoch_enabled": true,
"is_should_consent": false,
"is_downloadable": true,
"private_description": null,
"public_description": ""
}
]
}
}Pagination and count behavior
Current backend returns:
pages_totalcollection_link- current page results in
data.data
Current backend does not expose separate totalCount, allItemsCount, or folderCounts fields on this API response.
If frontend clients need those additional totals, they must compute them elsewhere or wait for a dedicated backend extension.
GET /api/products/collection
Return the creator's products that are currently part of the shareable collection.
- Auth required: Yes
- Roles: Authenticated creator
- Query params:
page,limit
Response shape matches GET /api/products.
GET /api/products/
Return a single product visible to the authenticated creator.
- Auth required: Yes
- Roles: Product owner / authorized creator
Path parameters
| Parameter | Type | Required |
|---|---|---|
product | integer | Yes |
Success response example
{
"success": true,
"errors_message": null,
"data": {
"id": 5155,
"type": "video",
"title": "My Product",
"preview": "https://...",
"preview_blurred": "https://...",
"price": 4400,
"in_collection": false,
"link": "https://fangate.info/94w24rWk3v",
"link_clicks": 0,
"unlocks": 0,
"total_earnings": 0,
"folder_id": null,
"folder": null,
"media": [
{
"id": 7792,
"type": "video",
"preview": "https://...",
"preview_blurred": "https://...",
"veriff_status": "Manual Approval",
"removal_description": null,
"source": "https://..."
}
],
"is_adult_content": true,
"is_verif_age": false,
"is_epoch_enabled": true,
"is_should_consent": false,
"is_downloadable": true,
"private_description": null,
"public_description": ""
}
}POST /api/products
Create a new product or attach media to an existing draft.
- Auth required: Yes
- Roles: Authenticated creator
- Content type:
multipart/form-data
There are two active creation flows.
Preferred flow: upload sessions
Send a previously finalized upload session:
| Field | Type | Required | Notes |
|---|---|---|---|
upload_session_id | string | Yes for this flow | Session must belong to current user and be ready to attach. |
price | integer | Yes when creating a new product | Price in minor units. |
product_id | integer | No | Attach to an existing product. |
title | string | No | Optional. |
is_adult_content | boolean | No | Defaults from user settings if omitted. |
is_verif_age | boolean | No | Defaults from user settings if omitted. |
is_should_consent | boolean | No | Defaults to false. |
private_description | string | No | Creator-only notes. |
public_description | string | No | Buyer-visible description. |
Legacy flow: direct multipart upload
Send a new file directly:
| Field | Type | Required | Notes |
|---|---|---|---|
media | file | Yes for this flow | Legacy direct upload. |
price | integer | Yes when creating a new product | Minimum current rule is 500 minor units. |
product_id | integer | No | Attach to an existing draft product. |
title | string | No | Optional. |
extension | string | No | Legacy helper. |
is_adult_content | boolean | No | Optional. |
is_verif_age | boolean | No | Optional. |
is_should_consent | boolean | No | Optional. |
private_description | string | No | Optional. |
public_description | string | No | Optional. |
Important validation and side effects
- If
product_idis present, product ownership is enforced. - Large direct uploads are accepted and queued asynchronously via
CreateProductJob. - If upload sessions are used, attachment is handled by
UploadAttachmentService. - After product creation, moderation dispatch runs when either:
- creator has
skip_moderation, or - SightEngine moderation is enabled
- creator has
Success / accepted responses
Immediate success:
{
"success": true,
"errors_message": null,
"data": {
"id": 5155,
"link": "https://fangate.info/94w24rWk3v"
}
}Async accepted for larger direct uploads:
{
"success": true,
"errors_message": null,
"data": {
"id": 5155
}
}Error examples
Chunk still uploading:
{
"success": true,
"errors_message": null,
"data": {
"upload_percentage": 64
}
}Media processing failure:
{
"success": false,
"errors_message": "Media service error",
"data": null
}File too large for legacy path:
{
"success": false,
"errors_message": "File size too large",
"data": null
}PATCH /api/products/
Update product metadata.
- Auth required: Yes
- Roles: Product owner
- Content type:
application/json
Request body
| Field | Type | Required |
|---|---|---|
title | string | No |
is_adult_content | boolean | No |
is_verif_age | boolean | No |
is_should_consent | boolean | No |
is_downloadable | boolean | No |
private_description | string | No |
public_description | string | No |
Important notes
- This endpoint updates metadata only.
- It does not replace media.
- It does not update the price.
Price Editing
Creators can change the price of an existing sales link in two explicit ways:
- update the current link's price
- create a new link with a different price while keeping the original link unchanged
This split is intentional and avoids ambiguous behavior when a link already has traffic or active checkout sessions.
Data model and scope
- A product remains the media/content entity.
- URL links are stored separately in
short_links, but current checkout price is still sourced fromproducts.price. - "Create new link with different price" is implemented by duplicating the product row (same media references, new product id, new short link).
- Title and descriptions can be overridden in the duplicate flow; if omitted, source values are kept.
Checkout and validation behavior
- Price must be revalidated server-side at checkout start.
- Sessions that already started checkout before a price update keep their originally locked price.
- A later price update does not force-invalidate those already pending checkout sessions.
Analytics behavior
- Analytics are tracked per link.
- Future events use the link/price active at event time.
- Historical statistics remain based on prior prices and are not retroactively recomputed.
Concurrency and compatibility
- Standard optimistic concurrency (last successful write wins) is acceptable for this ticket.
- Existing clients keep current behavior until they adopt the dedicated price-edit endpoints below.
PATCH /api/products/{product}/price
Update the price on the existing active sales link for a product.
- Auth required: Yes
- Roles: Product owner
- Content type:
application/json
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
price | integer | Yes | New price in minor units. |
Example request
{
"price": 1000
}Success response example
{
"success": true,
"errors_message": null,
"data": {
"id": 5155,
"link": "https://fangate.info/94w24rWk3v",
"price": 1000
}
}Notes
- Applies only to the existing link associated with the product.
- Does not create a new link id/slug.
- New checkout sessions use the updated price after server-side revalidation.
POST /api/products/{product}/price-links
Create a new priced link by duplicating the current product and assigning a new short link.
- Auth required: Yes
- Roles: Product owner
- Content type:
application/json - Success status:
201 Created
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
price | integer | Yes | Price in minor units for the new link. |
title | string | No | Optional new link display title override. |
private_description | string | No | Optional creator-facing note override. |
public_description | string | No | Optional buyer-facing description override. |
Example request
{
"price": 1200,
"title": "Campaign set - promo",
"public_description": "Limited drop price"
}Success response example
{
"success": true,
"errors_message": null,
"data": {
"id": 6201,
"title": "Campaign set - promo",
"link": "https://fangate.info/AbCdEf1234",
"price": 1200
}
}Notes
- Creates a new product record with a new short link and new price.
- Existing source product and link continue to work at their original price.
- Media rows are duplicated without re-upload; storage files are shared until one side is no longer referenced.
- Existing in-flight checkouts on older links continue with their original locked price.
PATCH /api/products/{product}/folder
Assign or unassign one product to a content folder.
- Auth required: Yes
- Roles: Product owner
- Content type:
application/json
Request body
Assign:
{
"folder_id": 12
}Unassign:
{
"folder_id": null
}Validation
- if a folder id is provided, it must belong to the authenticated creator
nullis valid and removes folder assignment
POST /api/products/{product}/collection
Toggle collection membership for a product.
- Auth required: Yes
- Roles: Product owner
Side effects
- if the creator does not yet have a collection, backend creates one
- if the collection has no short link, backend creates that too
- calling again removes the product from the collection
DELETE /api/products/media/
Delete one media row and remove its stored files.
- Auth required: Yes
- Roles: Media owner / authorized product owner
Side effects
- deletes media DB row
- deletes
media,preview,preview_blurred, andthumbnailpaths only when not referenced by any other media rows - returns the parent product resource
DELETE /api/products/
Delete a product.
- Auth required: Yes
- Roles: Product owner
Side effects
- deletes related media rows; storage objects are deleted only when no other media rows reference the same paths
- deletes related consent records and consent requests
- deletes the short link
- deletes the product
Success response example
{
"success": true,
"errors_message": null,
"data": "deleted"
}Sales-link flow example
- upload media through Upload Sessions or legacy direct upload
POST /api/products- optional
PATCH /api/products/{product}to update descriptions or toggles - optional
POST /api/products/{product}/collection - frontend reads
linkfrom the product resource and exposes the sales link
Price rules
- price is stored in minor units
- minimum current validation is
500 - backend validation currently enforces integer pricing rather than decimal strings