Skip to Content
FanConvert Docs
REST API

Webhooks

Receive sales, emergencies and AI pauses live — events, HMAC signature, deliveries.

2 min readOpen in ChatGPTOpen in Claude

Receive events without polling the API: POST /public/v1/webhooks with a public https:// address (port 443 or 8443) and the events you want. The response gives the signing secret whsec_… only once.

Permissions: webhooks:write (and webhooks:read to list and read the log), plus the read permission of each event.

EventWhenRead permission required
sale.createda sale (MYM, Telegram, OnlyFans, Fanvue)sales:read (+ telegram:read for Telegram)
emergency.createda new emergencynotifications:read
ai.pausedthe AI paused on a fan, or turned off for a creatorchatting:read
pingPOST /public/v1/webhooks/{id}/ping, to test—

An event only carries identifiers and amounts, never a fan's name or a message: read the details with your key.

{
  "id": "evt_9b1c…",
  "type": "sale.created",
  "apiVersion": "v1-beta",
  "createdAt": "2026-11-07T12:30:00Z",
  "data": {"platform": "mym", "creatorId": "emma-demo", "saleId": "s_…",
           "conversationId": "123", "amountMinor": 1500, "currency": "EUR", "at": "…"}
}

Verify the signature

Header FanConvert-Signature: t=<seconds>,v1=<hex>: compute HMAC-SHA256 of "<t>.<raw body>" with your secret, compare in constant time, reject a t older than 5 minutes.

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes) -> bool:
    fields = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(fields["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{fields['t']}.".encode() + body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, fields["v1"])

Delivery

  • Answer 2xx within 10 s. Otherwise, retries after 1 min, 5 min, 30 min, 2 h and 6 h (six attempts), then failed.
  • Redirects are never followed.
  • The FanConvert-Event-Id header lets you ignore a duplicate.
  • The log: GET /public/v1/webhooks/{id}/deliveries.
  • A webhook lives with its key: key revoked or expired, it is disabled. DELETE /public/v1/webhooks/{id} disables it (the log stays).
  • Events are detected about every 30 s; on creation, nothing from the past is replayed.

Reference: Webhooks.

Last updated on

On this page