Webhooks
Receive tip.created events and verify the timestamped HMAC-SHA256 signature over the raw request body.
Receive events
The app needs an HTTPS Webhook URL and the grant needs events.subscribe. When the creator receives a completed tip, Furipay POSTs this payload:
{
"event": "tip.created",
"name": "Ploy",
"amount": 50,
"message": "Keep going!",
"createdAt": "2026-08-14T09:12:00.000Z"
}
Verify the signature
Every request carries X-Furipay-Signature in the form t=UNIX_SECONDS,v1=HEX_SIGNATURE. The signature is HMAC-SHA256 of t.rawBody, keyed by the webhook secret.
Capture the unchanged raw body before parsing JSON. Do not re-stringify the parsed object for verification:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyFuripayWebhook(
rawBody: Buffer,
header: string,
webhookSecret: string,
): boolean {
const fields = Object.fromEntries(
header.split(",").map((part) => part.split("=", 2)),
);
const timestamp = Number(fields.t);
const provided = Buffer.from(fields.v1 ?? "", "hex");
if (
!Number.isInteger(timestamp) ||
Math.abs(Date.now() / 1000 - timestamp) > 300 ||
provided.length !== 32
) {
return false;
}
const expected = createHmac("sha256", webhookSecret)
.update(String(timestamp) + ".")
.update(rawBody)
.digest();
return timingSafeEqual(expected, provided);
}
Rejecting timestamps older than five minutes reduces replay risk. Then return HTTP 2xx quickly and perform heavier work in your own queue.
Retries and testing
- Furipay gives each request ten seconds.
- Network errors, timeouts, and non-2xx responses are attempted up to four times, waiting 0, 5, 60, and 600 seconds before each attempt.
- The Developer Portal's test event makes one attempt and records the result in the Delivery log.
- Make the endpoint safe to process more than once because a retry can repeat a request that already completed.