Skip to content
Proofline

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 2xx quickly 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"
  }
}
  • id is 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.
  • appId is the app the event belongs to, or null for 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.

  • amountMinor on 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.locked repeats 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.reversed takes money back: amountMinor is never positive (negative, or zero when nothing was still owed) and reason says 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, and reversalOf is 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, now status void.
  • payout.paid is org-wide: appId is null and data.appIds lists the apps whose commissions the payout paid. Endpoints for all apps receive it, and so do endpoints limited to one of those apps.