Webhooks
The RefRef event catalog, how to receive and verify webhook events, read the event feed, and confirm Rewards.
RefRef tells your product about facts through events: a new participant, a bound referral, a recorded Event, a Reward that was earned or voided, a payout. You receive them in two ways:
- Webhooks. RefRef sends each event as a signed
POSTto every enabled endpoint of the Project that subscribes to its type. - The event feed.
GET /v1/webhook-eventslists the same events in commit order. The feed is the source of truth: if your endpoint missed deliveries, read the feed from your last cursor.
RefRef writes an event in the same database transaction as its fact. An event exists exactly when its fact was saved.
Event catalog
| Type | When RefRef sends it | data |
|---|---|---|
participant.created | A trusted call or a Widget token named a participant that the Project did not have, or a partner joined it | participant: { participantId, kind, externalId }. A partner has externalId: null. |
referral.created | Attribution bound a referral | referral: id, programId, referrer, referee, bindingEventId or bindingSubscriptionFactId, boundAt |
event.recorded | A signup or purchase got its attribution decision, or RefRef accepted a custom_ Event | event (type, participant, actor, source IDs, purchase amounts) and attribution (null for custom_) |
reward.earned | A participant earned a Reward | reward |
reward.voided | An operator voided a Reward | reward |
reward.fulfilled | A Reward was fulfilled: by your product, the Console, a webhook answer, or a RefRef delivery | reward |
reward.revoked | Your product took back a voided Reward that it granted | reward |
payout.paid | A payout paid cash Rewards of the Project | payout |
payout.failed | A payout failed before payment; its Rewards wait for the next payout | payout, with failureReason |
payout.returned | The provider returned a payout, before or after payment; the next payout pays its Rewards again | payout, with failureReason |
The API reference publishes a JSON Schema and a sample for each type.
The envelope
Every event has the same envelope:
{
"id": "wev_…",
"type": "reward.earned",
"apiVersion": "2026-10-04",
"occurredAt": "2026-10-04T12:00:00.000Z",
"projectId": "prj_…",
"environment": "live",
"data": { "reward": { … } }
}| Field | Meaning |
|---|---|
id | The event ID. Every delivery of one event has the same ID: deduplicate on it. |
type | One of the types in the catalog |
apiVersion | The version of the payload shape. An endpoint keeps the version that was current when you created it. |
occurredAt | When the fact was saved |
projectId | The Project of the fact |
environment | live or sandbox, the environment of the Project |
data | The object as the fact left it |
Inside one apiVersion, RefRef only adds fields and event types. Ignore fields and types that you do not know. A change that removes or renames a field is a new version.
The Reward object
reward.* events carry the Reward as GET /v1/rewards/{rewardId} returns it:
| Field | Meaning |
|---|---|
id, programId, ruleId | The Reward and the rule that earned it |
eventId | The Event that earned it |
beneficiaryRole | referrer or referee in a referral Program, subject in a loyalty Program |
beneficiary | { participantId, kind, externalId }: who gets the Reward. Use externalId to find the user in your product. |
benefitKind | What the Reward gives. Exactly one of money, discount, or custom is set for it. |
money | { amountMinor, netAmountMinor, currency, currencyExponent } |
discount | { rateBps } |
custom | { quantity, unitKey, unitName, unitNamePlural, validityDays }, for example 500 storage_mb |
fulfillmentStatus | pending, in_progress, fulfilled, failed, cancelled, revocation_pending, or revoked |
fulfillment, revocation | Who recorded the grant or the take-back, and when |
delivery | { method, status, reason, providerReference }. Your product grants a Reward whose method is operator. |
void | Set when an operator voided the Reward |
The payout object
payout.* events carry one disbursement, limited to the Rewards of the Project: { id, method, currency, amountMinor, rewards: [{ rewardId, beneficiary, amountMinor }], failureReason }. One transfer to a partner can pay Rewards of several Projects, so each Project gets an event with only its own lines. A line of a voided Reward that was paid before is negative.
Add an endpoint
In the Console, open Developers → Webhooks and add an endpoint. Choose its URL, its event types, and its fulfillment mode. The Console shows the signing secret once; copy it then. Every Workspace member sees the endpoints; only owners and admins change them.
The endpoint menu also has Edit, Send test event…, and Rotate secret. Each endpoint keeps a log of its delivery attempts, and an admin can replay a delivery from it.
With the API
An automation app or your backend can subscribe with a Workspace API key:
curl -X POST "$REFREF_API/v1/webhook-endpoints" \
-H "x-api-key: $REFREF_API_KEY" \
-H "content-type: application/json" \
-d '{
"projectId": "prj_…",
"url": "https://example.com/refref/webhook",
"eventTypes": ["reward.earned", "reward.voided"],
"fulfillmentMode": "explicit"
}'The answer has endpoint and its secret. The secret shows only in this answer.
| Route | Purpose |
|---|---|
POST /v1/webhook-endpoints | Subscribe a URL. eventTypes defaults to every type. |
GET /v1/webhook-endpoints?projectId=… | List the Project's endpoints, without secrets |
DELETE /v1/webhook-endpoints/{endpointId}?projectId=… | Unsubscribe. The events stay in the feed. |
The key acts only on Projects of its Workspace and its environment.
Receive and verify
RefRef signs each request with Standard Webhooks headers, so the Standard Webhooks libraries verify it.
| Header | Value |
|---|---|
webhook-id | The event ID |
webhook-timestamp | Unix seconds when RefRef signed the request |
webhook-signature | v1,<base64 HMAC-SHA256> of <webhook-id>.<webhook-timestamp>.<raw body>. Several are space-separated. |
The HMAC key is the base64-decoded text after whsec_ in the secret. After you rotate a secret, the old secret also signs for 24 hours, and the header has one signature for each secret. Accept the request when any one of them matches.
import { createHmac, timingSafeEqual } from "node:crypto";
/** Whether the Standard Webhooks headers sign `rawBody` with `secret`. */
export function verifyRefRefWebhook(
rawBody: string,
headers: Headers,
secret: string,
) {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const signature = headers.get("webhook-signature");
if (!id || !timestamp || !signature || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
return signature.split(" ").some((part) => {
const given = Buffer.from(part.slice("v1,".length), "base64");
return (
part.startsWith("v1,") &&
given.length === expected.length &&
timingSafeEqual(given, expected)
);
});
}Verify the raw body, before your framework parses it. A body that was parsed and serialized again can differ by one byte and fail the check. Refuse a timestamp more than five minutes from your clock.
Answer and retries
| Your answer | What RefRef does |
|---|---|
2xx | The delivery succeeded |
202 Accepted to reward.earned | The delivery succeeded; the Reward stays pending (see below) |
| Another status, or no answer in 10 seconds | RefRef tries again |
RefRef retries after 30 seconds, doubles the wait up to one hour, then tries each hour, for 80 attempts (about 72 hours). Then the delivery fails, and a Console admin can replay it. RefRef follows no redirect and sends only to https URLs on public addresses.
RefRef does not guarantee the order of deliveries. Each event carries the object as its fact left it, so compare occurredAt or read the object again when order matters. A delivery can also repeat: make your handler idempotent on the event ID, or on the Reward ID for reward.*.
Grant Rewards
Your product grants a Reward whose delivery.method is operator: account credit, a discount, or a custom quantity such as storage. The endpoint's fulfillment mode decides when RefRef records the grant:
on_delivery(the default). A2xxanswer toreward.earnedrecords the fulfillment. Grant the Reward before you answer. Answer202 Acceptedto a Reward that this endpoint does not grant, for example another unit; the Reward then stayspending.explicit. Your answer only acknowledges the delivery. When the grant is done, confirm it:
POST /v1/rewards/{rewardId}/fulfillment
x-api-key: <Workspace API key>
content-type: application/json
{ "projectId": "prj_…", "externalReference": "grant-123" }Use explicit when your handler queues the work, and for automation platforms, which answer before their later steps run. A repeat of the confirmation with the same facts returns the same record.
When an operator voids a Reward that you granted, you receive reward.voided. Take the grant back in your product, then record it with POST /v1/rewards/{rewardId}/revocation (body { projectId, externalReference }). A cash Reward is never revoked. RefRef then sends reward.revoked.
To find Rewards that a webhook did not reach, read GET /v1/rewards?deliveryMethod=operator&fulfillmentStatus=pending.
Read the event feed
GET /v1/webhook-events lists the Project's events in commit order:
curl "$REFREF_API/v1/webhook-events?projectId=prj_…&limit=100" \
-H "x-api-key: $REFREF_API_KEY"{ "events": [ … ], "next": "…" }Store next and pass it as after on the next read. An event never appears later behind a cursor that you already read. When there are no new events, next is the cursor that you sent. limit is 1 to 500; the default is 100.
Use the feed to catch up after downtime, to backfill a new system, or instead of webhooks when your product cannot receive requests.
Test an endpoint
Send test event… in the endpoint menu sends a signed sample of the type you choose, at once. Its ID starts with wev_test_, and RefRef does not store it, so it is not in the feed. Its objects do not exist: a call to confirm its Reward answers 404.
Automation platforms
Zapier, Make, and Activepieces connect with a webhook trigger and HTTP steps. See Automations for step-by-step recipes.