Webhooks Overview

Webhooks are a way for your app to receive live notifications of activity on your user’s accounts.

Currently we support 5 different event types:

  • Message Received
  • New Follower
  • New Subscriber
  • Purchase Received
  • Tip Received

Here’s how to use them:

  1. Setup an endpoint on your backend to receive webhooks, this should be able to receive POST requests from our API
  2. Navigate to the Fanvue Developer Area, select your app and go to the Webhooks tab Webhooks UI
  3. Enter the desired URL Webhooks URL
  4. Press Save and then enable the webhook by checking the box next to it

How deliveries work

  • Each webhook is delivered as an HTTP POST with Content-Type: application/json.
  • The request body contains an event-specific JSON payload.
  • Your endpoint should return a 2xx response as soon as you successfully receive and persist the event.

Verifying webhook signatures

Every webhook request includes an X-Fanvue-Signature header. You should verify this signature on every request to ensure it came from Fanvue and was not tampered with.

Signature format

The header value has the format:

t=<timestamp>,v0=<signature>
  • t is a Unix timestamp (seconds since epoch) indicating when the request was signed.
  • v0 is the HMAC-SHA256 signature encoded as a hexadecimal string.

Finding your signing secret

Your webhook signing secret is available in the Fanvue Developer Area:

  1. Navigate to your app’s Webhooks tab
  2. Click “View Signing Secret”

Keep this secret secure and never expose it in client-side code.

Verification steps

  1. Extract the timestamp (t) and signature (v0) from the header
  2. Construct the signed payload by concatenating the timestamp, a period, and the raw request body: {timestamp}.{body}
  3. Compute the expected signature using HMAC-SHA256 with your signing secret
  4. Compare the expected signature with the received signature using a timing-safe comparison
  5. Optionally, check that the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks

Verification example (Node.js)

1import crypto from "crypto";
2
3const SIGNING_SECRET = process.env.FANVUE_WEBHOOK_SECRET!;
4const TOLERANCE_SECONDS = 300; // 5 minutes
5
6export function verifyWebhookSignature(
7 payload: string,
8 signatureHeader: string
9): boolean {
10 // Parse header: "t=1234567890,v0=abc123..."
11 const parts = signatureHeader.split(",");
12 let timestamp: string | undefined;
13 let signature: string | undefined;
14
15 for (const part of parts) {
16 const [key, value] = part.split("=");
17 if (key === "t") timestamp = value;
18 if (key === "v0") signature = value;
19 }
20
21 if (!timestamp || !signature) {
22 return false;
23 }
24
25 // Check timestamp tolerance (optional but recommended)
26 const currentTime = Math.floor(Date.now() / 1000);
27 if (Math.abs(currentTime - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) {
28 return false;
29 }
30
31 // Compute expected signature
32 const signedPayload = `${timestamp}.${payload}`;
33 const expectedSignature = crypto
34 .createHmac("sha256", SIGNING_SECRET)
35 .update(signedPayload)
36 .digest("hex");
37
38 // Timing-safe comparison
39 return crypto.timingSafeEqual(
40 Buffer.from(signature),
41 Buffer.from(expectedSignature)
42 );
43}

Testing locally

  • Expose your local server with a tunneling tool (for example, ngrok) and copy the public HTTPS URL.
  • Set that URL in the Webhooks UI for the event type you want to receive.
  • Trigger the event in a test environment and inspect the request reaching your server.

Example endpoint (Node.js / Express)

1import crypto from "crypto";
2import express from "express";
3
4const app = express();
5
6const SIGNING_SECRET = process.env.FANVUE_WEBHOOK_SECRET!;
7const TOLERANCE_SECONDS = 300; // 5 minutes
8
9function verifyWebhookSignature(payload: string, signatureHeader: string): boolean {
10 const parts = signatureHeader.split(",");
11 let timestamp: string | undefined;
12 let signature: string | undefined;
13
14 for (const part of parts) {
15 const [key, value] = part.split("=");
16 if (key === "t") timestamp = value;
17 if (key === "v0") signature = value;
18 }
19
20 if (!timestamp || !signature) return false;
21
22 const currentTime = Math.floor(Date.now() / 1000);
23 if (Math.abs(currentTime - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) {
24 return false;
25 }
26
27 const signedPayload = `${timestamp}.${payload}`;
28 const expectedSignature = crypto
29 .createHmac("sha256", SIGNING_SECRET)
30 .update(signedPayload)
31 .digest("hex");
32
33 return crypto.timingSafeEqual(
34 Buffer.from(signature),
35 Buffer.from(expectedSignature)
36 );
37}
38
39// Use raw body for signature verification
40app.use(
41 express.json({
42 verify: (req: any, _res, buf) => {
43 req.rawBody = buf.toString();
44 },
45 })
46);
47
48app.post("/webhooks/fanvue", (req: any, res) => {
49 const signatureHeader = req.headers["x-fanvue-signature"];
50
51 if (!signatureHeader || !verifyWebhookSignature(req.rawBody, signatureHeader)) {
52 res.status(401).json({ error: "Invalid signature" });
53 return;
54 }
55
56 const event = req.body;
57 // Process the event...
58
59 res.sendStatus(200);
60});
61
62app.listen(3000);