Reference
Webhooks
Add an endpoint in Settings → Webhooks, choose the events, and Proofline will POST JSON to it.
Events
| Event | When |
|---|---|
attribution.decided |
A user’s attribution was decided or changed |
commission.created |
A commission was earned on a paid period, or an adjustment was added |
commission.locked |
The hold passed; the commission locked |
commission.reversed |
A reversal of a commission: a refund, or a void by you or after a confirmed fraud flag |
payout.paid |
One creator’s payout was marked paid: one event per payout, so a batch sends one per creator |
fraud.flagged |
A fraud rule raised a flag |
Delivery
- Delivery is at least once: a retry can deliver the same event again, so de-duplicate by
id. - Respond with any
2xxquickly and do the work afterwards. Anything else is retried with exponential backoff, then marked dead and shown in the dashboard. - Events can arrive out of order. Use the timestamps in the payload, not arrival order.
Verifying signatures
Every request carries:
Proofline-Signature: t=1729500000,v1=5f0c…e9
v1 is the hex HMAC-SHA256 of "{t}.{raw body}" with your endpoint’s signing secret. Verify it
against the raw body, compare in constant time, and reject old timestamps.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyProofline(
secret: string,
rawBody: string,
header: string,
toleranceSec = 300,
) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=') as [string, string]),
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
Payload shape
{
"id": "commission.locked:6f1c2b9e-…",
"type": "commission.locked",
"createdAt": "2026-11-20T03:00:00.000Z",
"appId": "0b7e41d2-…",
"data": {
"commissionId": "6f1c2b9e-…",
"kind": "earning",
"creatorId": "…",
"programId": "…",
"storeTransactionId": "…",
"attributionId": "…",
"amountMinor": 539,
"currency": "USD",
"status": "locked",
"holdUntil": "2026-11-20T03:00:00.000Z",
"reversalOf": null,
"reason": "$8.99 × 60% (period 1 of 3) = $5.39 → 30-day hold until 2026-11-20 → approved"
}
}
idis unique per event and the same on every retry, so de-duplicate on it. Treat it as an opaque string: don’t parse it or expect a prefix.appIdis the app the event belongs to, ornullfor an org-wide event (payout.paid).- No event carries the receipt. Fetch it with
GET /api/v1/receipts/{commissionId}.
Event data
| Event | data fields |
|---|---|
attribution.decided |
attributionId, endUserId, programId, creatorId, tier, status, contested, autopay, rulesVersion, supersedesId |
commission.* |
commissionId, kind (earning, reversal, adjustment), creatorId, programId, storeTransactionId, attributionId, amountMinor, currency, status, holdUntil, reversalOf, reason |
payout.paid |
payoutId, batchId, creatorId, amountMinor, currency, method, paidAt, stripeTransferId, appIds |
fraud.flagged |
flagId, rule, severity, subjectType, subjectId, holdCommissions |
Fields that don’t apply are null, for example storeTransactionId on a manual adjustment.
amountMinoron commission events is the signed change to the creator’s balance, in minor units: an earning is positive, a reversal negative, and an adjustment either.commission.lockedrepeats a commission already counted, with the amount it locked at. Rely on that amount (or the API) for a commission’s final figure: one that hasn’t locked can still be re-priced during its hold.commission.reversedtakes money back:amountMinoris never positive (negative, or zero when nothing was still owed) andreasonsays why. It covers a refund, a void after a confirmed fraud flag and a void by you in review. After a refund or a confirmed fraud flag it usually describes a new reversal or adjustment row, andreversalOfis the earning it offsets. When a commission that hadn’t locked is voided instead (by you in review, or by Proofline, for example for a self-referral), it describes the voided commission itself, nowstatusvoid.payout.paidis org-wide:appIdisnullanddata.appIdslists the apps whose commissions the payout paid. Endpoints for all apps receive it, and so do endpoints limited to one of those apps.