Skip to content

Products ​

Creator product and media management.

This surface covers:

  • paginated content library reads
  • product creation from upload sessions
  • sales-link metadata updates
  • folder assignment
  • collection membership
  • media deletion
  • product deletion

Product resource ​

Important fields returned by product endpoints:

FieldTypeNotes
idintegerProduct id.
typestringProduct media type inferred from primary media.
titlestring or nullProduct title.
previewstring or nullPreview URL generated by backend storage resolution.
preview_blurredstring or nullBlurred preview URL.
priceintegerPrice in minor units, for example cents.
buyer_priceintegerWhat the buyer pays at checkout, in minor units of the same currency: price plus the buyer fee, plus the age-check share on a product with is_verif_age. The number Fangate's Telegram card and the link preview show. A buyer whose country requires an age check on a product without it pays that share on top - see GET /api/fees/preview.
in_collectionbooleanCurrent collection membership flag.
linkstring or nullPublic sales link. Append ?ref= for your own reference per link.
link_clicksinteger or nullSales-link view count.
unlocksintegerSuccessful purchase count.
total_earningsintegerTotal earnings in minor units.
folder_idstring or nullAssigned content folder id.
folderobject or null{ id, name }.
mediaarrayArray of media resources.
is_adult_contentbooleanAlways true: every product is AVS+ (18+ notice, age check for buyers in countries that require one).
is_verif_agebooleanBuyer age-check flag.
is_epoch_enabledbooleanCreator/provider state.
is_should_consentbooleanDerived summary: true when any linked media requires third-party consent (media.is_should_consent).
is_downloadablebooleanWhether buyers can download.
is_accessiblebooleantrue when moderation + consent satisfy the purchasability matrix (same rules as server-side purchase checks).
purchase_block_codestring or nullWhy is_accessible is false: moderation_not_approved, manual_review_pending, consent_recipient_missing, consent_pending, consent_declined, consent_rescinded, price_review_pending, media_unavailable, ai_rail_unavailable, sales_stopped, removed_by_fangate - what each means and what ends it: Error codes.
consent_statusstringAggregated consent state across the product's media.
consent_outstanding_countintegerConsent slots still open.
needs_price_review_approvalbooleanPart of Fangate's compliance programme: a product priced above the review threshold is created normally but waits for manual approval by Fangate before it can be bought (purchase_block_code: price_review_pending meanwhile). The threshold is published in GET /api/app-data as products.manual_price_review_threshold_cents - currently 15000 (150.00).
is_ai_contentbooleantrue when at least one linked file is AI (ai_origin other than none). Sold on the AI domain; see AI content.
ai_labelstring or nullBuyer-facing disclosure of the first AI file: AI generated or AI modified.
telegram_linkstring or nullDeep link that opens this product's checkout in the Telegram Mini App (https://t.me/<bot>/<app>?startapp=<short link code>), usable as a URL button. One bot serves all products; AI products open their checkout on the AI domain through a second app of that bot, so use the value as returned instead of building it. No Telegram connection on the creator's account is needed. null when no bot is configured - fall back to link. Your own reference goes after the start parameter as --<reference>, see Your own reference per link.
private_descriptionstring or nullReturned only to the product owner.
client_referencestring or nullYour own reference for the product (order number, internal id), up to 64 characters. Returned only to the product owner and echoed in every webhook payload as data.client_reference.
show_creator_nameboolean or nullOwner only. true: the item page prints the account's stage name next to its header - Exclusive content from <stage name> - provided the account chose a stage name itself. false (the default): plain Exclusive content.
sales_stoppedboolean or nullOwner only. true while the product is taken off sale with the switch of PATCH /api/products/{product_id}.
removed_by_fangateboolean or nullOwner only. true while Fangate has taken the product off sale (purchase_block_code: removed_by_fangate). The files are untouched; neither sales_stopped nor sales_end_at lifts it - contact Fangate.
sales_end_atstring or nullOwner only. ISO 8601 moment from which the product is off sale, or null.
payment_optionsobject or nullOwner only. The checkout tiles, tip button and smallest tip that apply to this product, plus which of them still follow the account - see Payment options.
public_descriptionstring or nullBuyer-visible description.

Media resource ​

Each media[] item currently includes:

FieldTypeNotes
idintegerMedia id.
typestringimage, video, etc.
previewstringPreview URL.
preview_blurredstringBlurred preview URL.
duration_secondsnumber or nullVideo length in seconds (videos only; null for images and until the file has been processed).
veriff_statusstringHuman-readable moderation label such as Unchecked, Approved, AI Removal, Manual Approval, or Manual Removal. AI Removal means the automated checker removed the file - it says nothing about the file being AI-generated.
removal_descriptionstring or nullAdmin/moderation reason if present.
titlestring or nullMedia title from the library.
ai_originstringnone, synthetic, real_person_self, real_person_other (AI content).
ai_labelstring or nullAI generated, AI modified or null.
is_should_consentbooleanThis file needs another person's consent.
sourcestring or nullOriginal media URL, only exposed when the current user can update the product.

Use is_accessible on each product resource for the matrix outcome (purchasable / not purchasable). For detail:

  • Moderation — each media[].veriff_status
  • Consent — GET /api/products/{product}/consent-creator-tags (per-tag status and media_id) and derived product is_should_consent

Deleted products, blocked accounts, or missing media may still return 404 on fetch; is_accessible applies when the product is returned in list/detail responses.

See Consent & adult content — per upload status via API.

Product–media relationship ​

Products link to media through an n:m product_media pivot (product.media[] in API responses). One library asset can appear on multiple sales links without duplicating storage paths. Legacy media.product_id is kept in sync for backward compatibility.

Outbound webhook payloads expose purchased media as data.media_ids — always a string array of ids in the form m_{id} (see Webhooks — outbound delivery payload), even when a product has a single file.

See Media Library for standalone upload, bulk sessions, and attach/detach endpoints.

Payment options: checkout tiles and tips ​

A creator decides what a buyer sees at the checkout: which payment tiles are shown (paypal, card = "Credit Card / Other", crypto - see what each tile offers), whether the Send a tip button is offered on the product's pages, and the smallest tip. The account holds the value (dashboard: Settings → Payment options for customers); a product follows the account until it carries its own value - which is what payment_options on POST and PATCH /api/products sets.

json
{
  "payment_options": {
    "tiles": { "paypal": true, "card": true, "crypto": false },
    "tips_enabled": false,
    "min_tip_cents": 500
  }
}
FieldTypeNotes
tilesobject or nulltrue = the tile is shown. Send only the tiles you want to change; null = follow the account again.
tips_enabledboolean or nullThe tip button on this product's pages. null = follow the account.
min_tip_centsinteger or nullSmallest tip from this product's pages, minor units, within the platform range (500 - 30000) and a whole amount (a multiple of 100). null = follow the account.
  • Hiding a tile only hides it. The payment method itself stays active - a buyer who reaches the payment provider can still pick another method there.
  • At least one tile has to stay shown (422 otherwise). If none of the shown tiles is available for a product - for example only crypto is left and the account cannot take crypto - the checkout shows all available tiles, so a product always stays buyable. The fallback offered after a declined card payment is always shown.
  • On POST the field is stored by the call that creates the product; a call that only attaches a further file (product_id) does not change it. Without the field the product follows the account. "payment_options": null on PATCH resets all three.
  • Tips: switching them off removes the button from the product's pages; the creator's own tip page is not affected. The smallest tip is enforced by the server.
  • A price link created from the product (POST /api/products/{product}/price-links) keeps the product's own payment options.
  • In a multipart/form-data request send the booleans of payment_options as 1 / 0 - the strings true / false are refused there. JSON requests use real booleans.

The product resource returns the values that apply (owner only), with follows_account telling which of them are the account's:

json
{
  "payment_options": {
    "tiles": { "paypal": true, "card": true, "crypto": false },
    "tips_enabled": true,
    "min_tip_cents": 500,
    "follows_account": { "tiles": false, "tips_enabled": true, "min_tip_cents": true }
  }
}

The account-wide values (GET / PATCH /api/user/payment-options) are an account setting and need the creator's login token; a generated API key sets the options per product.


Posting a product card from your own Telegram bot ​

Fangate's bot posts product cards for creators itself (inline search, and the share button inside Fangate's Mini App). That share button works only inside Fangate's Mini App with the creator's own Telegram account - another bot cannot call it. Your bot posts the card with an ordinary Bot API message, in one of two forms: as text with a link preview, built from GET /api/products/{product} (below), or as a photo with Fangate's card picture. The inline search of Fangate's bot sends the first form (see below).

  1. Wait until the product can be sold - product.sellable or is_accessible: true. A link Telegram previews while the product is still in review shows a page without title and image, and Telegram keeps that preview for the link.
  2. The text: title and the price the buyer pays (buyer_price, formatted in the product's currency), for example Beach set — €6.40. Put link on the title as a text_link entity and ask for the preview above the text. Telegram builds the preview from the link: the blurred teaser image and one line with the stage name (when show_creator_name is on) and the same price.
  3. One button: a URL button with telegram_link, for example Unlock (€6.40). It opens the checkout in Fangate's Mini App; payment and delivery run there. Where telegram_link is null, use link.
json
{
  "chat_id": 123456789,
  "text": "Beach set — €6.40",
  "entities": [{ "type": "text_link", "offset": 0, "length": 9, "url": "https://fangate.info/94w24rWk3v" }],
  "link_preview_options": { "show_above_text": true },
  "reply_markup": { "inline_keyboard": [[{ "text": "Unlock (€6.40)", "url": "https://t.me/Fngatebot/vip?startapp=94w24rWk3v" }]] }
}

length counts UTF-16 code units, as the Bot API does. Telegram fetches a link's preview once and keeps it; after a price change it may still show the old price for a link it already fetched. Add a query parameter of your own that changes with the price - for example ?v=640 - and Telegram fetches the page again; the sales page ignores it.

As a photo with Fangate's card picture ​

Your bot can also send the card as a photo with Fangate's card picture: the locked preview with the lock burned in and one line on top, From <stage name>: <title> or the title alone. GET /api/products/{product}/telegram-card returns the picture's url and the button text with the price the fan pays. Send it with sendPhoto and one URL button: button as its text, telegram_link as its url. A caption is optional.

json
{
  "chat_id": 123456789,
  "photo": "<url from GET /api/products/{product}/telegram-card>",
  "reply_markup": { "inline_keyboard": [[{ "text": "Unlock 5 photos ($21.77)", "url": "https://t.me/Fngatebot/vip?startapp=94w24rWk3v" }]] }
}

The url is signed for two hours: send the photo right away, or call the endpoint again. Telegram stores the picture when the message is sent, so a sent card keeps its image. Unlike the link preview, the photo does not depend on Telegram's preview cache; after a price change, fetch the card again for the new button text.

Where your bot can send it. In every chat your bot can write to - and into the creator's own private chats as the account's Telegram Business chatbot: with the business_connection_id of that connection, the card goes out as a message of the creator's account, without a "via" line. Telegram allows this only for an account with Telegram Premium, in private chats with an incoming message in the last 24 hours; URL buttons are allowed there. Both forms of the card work this way.

From the creator's own Telegram account ​

Only bots can attach buttons to a message - a regular Telegram account cannot. A client signed in as the creator therefore sends the card as a bot's inline result, and Telegram marks the message "via @<bot>". To send it from the creator's account without that mark, let your bot send it as the account's Business chatbot (see above).

An integration that works inside the creator's own Telegram account - a Telegram client signed in as the creator, not a bot - can send the exact card Fangate's bot builds. Ask Fangate's bot (the one named in telegram_link) for the product with an inline query of its id - messages.getInlineBotResults with the query 11779 - and send the result into the chat with messages.sendInlineBotResult. A person does the same by typing @<bot> 11779 in a chat. The query finds the creator's products by id or title. It needs the creator's Telegram account connected to Fangate (dashboard: Settings → Telegram mini app) and a product that can be sold.

As a photo. Ask with crm:<product id>:<key> instead of the id - key from GET /api/products/{product}/telegram-card, called right before - and the bot answers with Fangate's card picture as a photo with the same button. Text after a space becomes the photo's caption, for example crm:11779:07b96184a9c6a251 Just shot this for you; an inline query holds 256 characters in all. # and a reference right after the key - crm:11779:07b96184a9c6a251#chatter-anna Just shot this for you - puts your own reference on the card's button. Telegram marks this card "via @<bot>" as well. If the bot finds no picture for the key, it answers with the link-preview card, your text on top. If it answers with no result at all (account not connected, product not for sale), send the sales link as text instead.

Why POST /api/user/telegram/miniapp/share stays closed to API keys. It prepares a card that only Fangate's Mini App, open in the creator's own Telegram, can send. Another bot cannot send it at all, and a client in the creator's account gets the identical card through the inline query above without it. Opening it to keys would add nothing, so it stays login-only.

Add a reference of your own to a product's link, and Fangate keeps it with the sale and returns it unchanged as link_reference in the payment webhooks - for example to tell which campaign, chat or chatter a sale came from. Product and price stay the same; one link per recipient is all it takes.

LinkHowExample
Sales link (link)Query parameter refhttps://fangate.info/94w24rWk3v?ref=chatter-anna
Telegram link (telegram_link)-- and the reference after the start parameterhttps://t.me/<bot>/<app>?startapp=94w24rWk3v--chatter-anna
Card from the creator's own account# and the reference right after the key of the crm: querycrm:11779:07b96184a9c6a251#chatter-anna
  • Format: 1 to 64 characters - letters, digits, _ and -, but no -- (the Telegram link uses it to separate the reference). On the sales link a reference outside these rules is ignored: the purchase goes through with link_reference: null. A Telegram link with such a reference does not open the checkout; the Mini App opens Fangate's website instead.
  • Every payment method of the checkout page keeps the reference.
  • On a card, the button carries it - the button that opens the Mini App, not the sales link in the text of a link-preview card.
  • Visible to the fan. The reference is part of the address the fan opens; the fan can read, change or remove it, and Fangate does not check it. Put no personal data or secrets into it, and use it to attribute sales, not as proof.
  • Only the webhooks return it: every payment event (payment.*, refund_request.rejected) carries link_reference, null when the purchase carried none. Wallet rows and the product resource do not show it.

GET /api/products ​

Paginated content library endpoint for the authenticated creator.

  • Auth required: Yes
  • Roles: Authenticated creator

Query parameters ​

ParameterTypeRequiredNotes
pageintegerNoPage number.
limitintegerNoItems per page.
sort_bystringNonewest, most_earnings, most_views, collection.
folder_idintegerNoFilter to one folder.
folder_statestringNoCurrently documented and implemented special value: unassigned.
filterFolderstringNoLegacy compatibility filter. Accepts folder id or unassigned.

Success response example ​

json
{
  "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_total
  • collection_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 (generated API keys: read scope, always on)
  • Roles: Product owner / authorized creator
  • Rate limit (generated keys only): 60 requests per minute per key and IP, plus 120 per minute per IP across all keys - one counter shared with the product list. Creator login tokens are not limited here. An agency's key working inside its accounts has a higher ceiling per IP - see Rate limits.

Path parameters ​

ParameterTypeRequired
productintegerYes

Success response example ​

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

GET /api/products/{product}/telegram-card ​

Fangate's card picture and the button text for a product's Telegram card that a bot of your own sends as a photo. How to send it: As a photo with Fangate's card picture.

  • Auth required: Yes (generated API keys: api-key:urls.read)
  • Roles: Product owner / authorized creator; an agency's key names the account with X-Fangate-Account
  • Rate limit (generated keys): one counter with GET /api/products/{product}

Parameters ​

ParameterInTypeRequiredMeaning
productpathintegerYes
stage_namequerystring, up to 80 charactersNoThe name in the picture's line: From <stage name>: <title>. Empty or absent: the title alone. Emoji and other symbols the font cannot draw are left out of the line.

Success response example ​

json
{
  "success": true,
  "errors_message": null,
  "data": {
    "url": "https://...",
    "key": "07b96184a9c6a251",
    "button": "Unlock 5 photos ($21.77)",
    "line": "From Mia: Sunset set"
  }
}
FieldMeaning
urlThe card picture: the locked preview as JPEG, up to 1,200 px on the long edge, with the lock and the line burned in. Signed for two hours.
buttonThe unlock text with the price the fan pays (price plus buyer fee), for the URL button next to telegram_link.
lineThe line drawn into the picture.
keyFor the inline query crm:<product id>:<key> from the creator's own account (see From the creator's own Telegram account); a bot of your own does not need it.

Errors ​

HTTPerrors_codeWhen
403Not a product of this account.
409card_picture_unavailableThe picture cannot be made yet - usually right after the upload, before the product's blurred preview exists. Try again later, or wait for product.sellable.

POST /api/products ​

Create a new product or attach media to an existing draft.

  • Auth required: Yes
  • Roles: Authenticated creator
  • Content type: application/json or multipart/form-data

There are two creation flows: vault files (media_ids) and upload sessions (upload_session_id).

Price floor and review threshold. price is in minor units of the creator's currency and must be at least 500 (5.00); a lower price answers 422 with errors_code: price_below_minimum. Read the floor from GET /api/app-data (products.min_price_cents) instead of hardcoding it.

Payment limits. The API sets no upper limit on price, but not every payment method takes every amount. PayPal is offered up to a price of 250.00 USD or 224.00 EUR. Card payments are set up for a price of up to 440.00 USD, 395.00 EUR or 200.00 GBP; the card processor's limit applies to the total the buyer pays, including the buyer fee. Above that a buyer can only pay with crypto. Fangate's own app accepts prices up to these card limits - keep to them unless your buyers pay with crypto. The limits can change.

Text minimum: min. 15 characters ​

Announced - in force from 27 September 2026 (changelog). A sales page needs words: at least 15 characters in title or in public_description - either one is enough, spaces at the ends do not count. From 27 September 2026 a call without that answers 422 with errors_code: product_text_too_short (reported on public_description). A call that only attaches a further file to an existing product (product_id) is never refused for this. Send a title or a description of that length today and nothing changes for you. Prices above the compliance-review threshold (products.manual_price_review_threshold_cents, currently 15000) are accepted, but the product waits for Fangate's manual approval before it can be bought (needs_price_review_approval, purchase_block_code: price_review_pending).

Library media flow ​

Link existing approved library items (see Media Library):

FieldTypeRequiredNotes
media_idsinteger[]Yes for this flowMedia ids owned by the current creator.
priceintegerYes when creating a new productPrice in minor units.
titlestringNoOptional on its own; min. 15 characters in title or public_description (one of the two) from 27 September 2026 - see Text minimum.
is_adult_contentbooleanNoStill accepted, but ignored: every product is AVS+, the value is stored and answered as true.
is_verif_agebooleanNoDefaults from user upload_defaults or profile settings.
is_downloadablebooleanNoAbsent means false on this call - send it explicitly.
is_should_consentbooleanNoDefaults to false.
per_media_consentbooleanYes, with media_consentsWithout per_media_consent: true the media_consents list is silently ignored.
media_consents[], consent_date-NoDeclare consent for specific files in the same call: "per_media_consent": true, "consent_date": "2026-09-12", "media_consents": [{ "media_id": 101, "tagged_creator_ids": [55] }].
private_descriptionstringNoCreator-only notes.
client_referencestringNoYour own reference for this product, up to 64 characters: starts with a letter or digit, then letters, digits and . _ : - (a UUID fits). Stored by the call that creates the product, returned in the product and in every webhook payload (data.client_reference). Not unique, never shown to buyers.
show_creator_namebooleanNotrue prints the account's stage name next to the header of the item page: Exclusive content from <stage name>. Off by default. The name appears only when the account chose a stage name itself - the default every account starts with (Creator plus a number) is never printed, and the header stays Exclusive content. An AI model carries the name its agency gave it. Stored by the call that creates the product; a call that only attaches a further file (product_id) does not change it - use PATCH.
public_descriptionstringNoBuyer-visible description. Min. 15 characters here or in title (one of the two) from 27 September 2026 - see Text minimum.

No ai_origin on this flow: library files are classified already. A product with at least one AI file becomes an AI product (is_ai_content: true).

json
{
  "media_ids": [101, 102, 103],
  "price": 1500,
  "title": "Summer Bundle",
  "is_adult_content": true,
  "is_downloadable": false
}

Preferred flow: upload sessions ​

Send a previously finalized upload session:

FieldTypeRequiredNotes
upload_session_idstringYes for this flowSession must belong to current user and be ready to attach.
priceintegerYes when creating a new productPrice in minor units.
product_idintegerNoAttach to an existing product.
titlestringNoOptional on its own; min. 15 characters in title or public_description (one of the two) from 27 September 2026 - see Text minimum.
is_adult_contentbooleanNoStill accepted, but ignored: every product is AVS+, the value is stored and answered as true.
is_verif_agebooleanNoDefaults from user settings if omitted.
is_downloadablebooleanNotrue = the buyer may download the file, false = view-only in the browser (videos are re-encoded for playback). Honoured like on the other flows; without it the product takes the account's upload default (view-only when none is stored). Applies to the call that creates the product - attaching a further file with product_id does not change it.
is_should_consentbooleanNoDefaults to false.
ai_originstringSee noteThe new file's AI classification (none, synthetic, real_person_self, real_person_other). Required for generated keys with upload_ai_content unless the session already carries it; a value here wins over the session's. 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.
private_descriptionstringNoCreator-only notes.
client_referencestringNoYour own reference for this product, up to 64 characters: starts with a letter or digit, then letters, digits and . _ : - (a UUID fits). Stored by the call that creates the product, returned in the product and in every webhook payload (data.client_reference). Not unique, never shown to buyers.
show_creator_namebooleanNotrue prints the account's stage name next to the header of the item page: Exclusive content from <stage name>. Off by default. The name appears only when the account chose a stage name itself - the default every account starts with (Creator plus a number) is never printed, and the header stays Exclusive content. An AI model carries the name its agency gave it. Stored by the call that creates the product; a call that only attaches a further file (product_id) does not change it - use PATCH.
public_descriptionstringNoBuyer-visible description. Min. 15 characters here or in title (one of the two) from 27 September 2026 - see Text minimum.

Responds 200 with the product resource (201 on the library flow).

A product built over several calls. On this flow one call attaches one file: the first call creates the product, every further call carries product_id. A product can be bought as soon as one of its files is approved - files still in moderation do not block it, buyers receive the approved files and the rest once approved. Consent is different: one file with open consent blocks the whole product. So if a file that needs another person's consent follows in a later call, send is_should_consent: true together with per_media_consent: true on the creating call. That keeps the product blocked (purchase_block_code: consent_recipient_missing) until the declared file has arrived with its consent_tagged_creator_ids; without it the product can turn sellable - and be reported by the product.sellable webhook - before that file exists, and is blocked again the moment you attach it.

Important validation and side effects ​

  • If product_id is present, product ownership is enforced.
  • If upload sessions are used, attachment is handled by UploadAttachmentService.
  • After product creation the content moderation of the new files is queued.
  • From 27 September 2026: at least 15 characters in title or public_description (see the text minimum above); otherwise 422, errors_code: product_text_too_short. Not on a call that carries product_id.

Success / accepted responses ​

Immediate success:

json
{
  "success": true,
  "errors_message": null,
  "data": {
    "id": 5155,
    "link": "https://fangate.info/94w24rWk3v"
  }
}

Error examples ​

A call with an API key that brings neither upload_session_id nor media_ids - for example a file sent directly - is refused before any other check (integrations upload through upload sessions):

json
{
  "success": false,
  "errors_message": "Direct file upload is not available with an API key. Upload through an upload session (POST /api/upload-sessions) and create the product with upload_session_id.",
  "errors_code": "forbidden",
  "data": null
}

PATCH /api/products/ ​

Update product metadata.

  • Auth required: Yes
  • Roles: Product owner
  • Content type: application/json

Request body ​

FieldTypeRequired
titlestringNo
is_adult_contentbooleanNo
is_verif_agebooleanNo
is_should_consentbooleanNo
is_downloadablebooleanNo
private_descriptionstringNo
client_referencestring or nullNo
show_creator_namebooleanNo
payment_optionsobject or nullNo
public_descriptionstringNo
sales_stoppedbooleanNo
sales_end_atstring (ISO 8601) or nullNo

Important notes ​

  • sales_stopped: true takes the product off sale at once, false lifts the switch (an end date that has passed keeps the product off sale until you clear it with null). sales_end_at does the same from a moment you choose, ISO 8601 with a time zone offset or Z; it must lie in the future (otherwise 422), and null removes it. While either applies, the sales link and the Telegram link refuse new purchases and the product reports is_accessible: false with purchase_block_code: sales_stopped. Buyers who already bought keep their access. Every product is switched on its own - a price link of the same content is not affected. Subscribers of product.blocked receive it with reason: sales_stopped once per stop; for an end date within five minutes of the moment passing. Putting a product back on sale sends no product.sellable; a block that still applies (a rejected file, a missing consent) is reported with product.blocked at that moment - also when it had been reported before the stop.
  • client_reference sets, changes or clears (null) your own reference for the product; same format as on POST /api/products.
  • show_creator_name switches the stage name next to the item page's header on or off (Exclusive content from <stage name>); same rules as on POST /api/products. A call that does not name it leaves it as it is.
  • payment_options sets the checkout tiles, the tip button and the smallest tip of this product - see Payment options. null makes the product follow the account again.
  • From 27 September 2026: a call that changes title or public_description must leave the product with at least 15 characters in one of the two - the value after the change counts, and a field the call does not name keeps its current value. Otherwise 422 with errors_code: product_text_too_short. A call that names neither field is never refused for this: an older product without any text keeps taking price and setting changes.
  • 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:

  1. update the current link's price
  2. 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 from products.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 ​

FieldTypeRequiredNotes
priceintegerYesNew price in minor units; at least 500 (422, errors_code: price_below_minimum otherwise). Above the compliance-review threshold the product goes back into manual price review.

Example request ​

json
{
  "price": 1000
}

Success response example ​

json
{
  "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. Media rows are reused through the product_media pivot (no file duplication).

  • Auth required: Yes
  • Roles: Product owner
  • Content type: application/json
  • Success status: 201 Created

Request body ​

FieldTypeRequiredNotes
priceintegerYesPrice in minor units for the new link.
titlestringNoOptional new link display title override. Min. 15 characters in title or public_description of the new link - the override or the inherited value - from 27 September 2026.
private_descriptionstringNoOptional creator-facing note override.
public_descriptionstringNoOptional buyer-facing description override. Min. 15 characters here or in title of the new link - the override or the inherited value - from 27 September 2026.
client_referencestring or nullNoYour own reference for the new link. Omitted: copied from the source product, so a sale through the price link still carries it in the webhook. null: none.
show_creator_name--Not a field of this call: the new link copies it from the source product, so both pages carry the same header. Change it with PATCH on the new product.

Example request ​

json
{
  "price": 1200,
  "title": "Campaign set - promo",
  "public_description": "Limited drop price"
}

Success response example ​

json
{
  "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.
  • From 27 September 2026: the new link needs at least 15 characters in its title or public_description - its own override or, where the call names none, the source product's value. Otherwise 422 with errors_code: product_text_too_short; a source product without any text needs a title or a description in the call.

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:

json
{
  "folder_id": 12
}

Unassign:

json
{
  "folder_id": null
}

Validation ​

  • if a folder id is provided, it must belong to the authenticated creator
  • null is 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, and thumbnail paths 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

Query parameters ​

ParameterTypeDefaultNotes
delete_mediabooleanfalse with an API key, true with a login tokenAlso delete the product's files from the vault (only files no other product uses). With an API key the files stay in the vault unless you send delete_media=1.

Side effects ​

  • deletes related media rows only when delete_media is on (with an API key: only with delete_media=1); 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 ​

json
{
  "success": true,
  "errors_message": null,
  "data": "deleted"
}

  1. upload media through Upload Sessions
  2. POST /api/products
  3. optional PATCH /api/products/{product} to update descriptions or toggles
  4. optional POST /api/products/{product}/collection
  5. frontend reads link from the product resource and exposes the sales link

Price rules ​

  • price is stored in minor units
  • minimum is 500 minor units (GET /api/app-data → products.min_price_cents); below it the call answers 422 with errors_code: price_below_minimum
  • prices above the compliance-review threshold (products.manual_price_review_threshold_cents, currently 15000 = 150.00) put the product into manual review: needs_price_review_approval: true and purchase_block_code: price_review_pending until Fangate approves
  • there is no upper limit, but PayPal is offered up to a price of 250.00 USD / 224.00 EUR and card payments up to 440.00 USD / 395.00 EUR / 200.00 GBP; above that buyers can only pay with crypto (see Payment limits above)
  • backend validation currently enforces integer pricing rather than decimal strings

POST /api/products/{product}/media ​

Attach existing library media to a product without re-uploading.

  • Auth required: Yes
  • Roles: Product owner
  • Content type: application/json
json
{
  "media_ids": [101, 102]
}

DELETE /api/products/{product}/media/ ​

Detach media from a product. The media item remains in the creator library.

  • Auth required: Yes
  • Roles: Product owner

Fangate backend developer documentation