Gareth Wilson Gareth Wilson

Guide to Rewardful Webhooks: Features and Best Practices

Published


Rewardful adds affiliate and referral programmes to SaaS products that bill through Stripe. It tracks which affiliate referred a visitor, links the resulting Stripe customer and sales to that referral, and calculates commissions and payouts. Webhooks let the rest of your stack follow along: welcome a newly confirmed affiliate, flag a converted referral in your CRM, or reconcile commission payouts in your accounting tool.

This guide covers Rewardful's 31 event types, the event and request IDs in each body, verifying X-Rewardful-Signature, and how strict Rewardful is about the response code.

What are Rewardful webhooks?

You add endpoints on the Webhooks page of the Rewardful dashboard and choose which event types each one receives. Every endpoint gets its own Signing Secret.

Deliveries are JSON POST requests with three parts: object, the record that changed, shaped like the matching REST API object; event, with the event's id, type, created_at and api_version; and request, whose id identifies this particular delivery attempt.

Rewardful signs the raw body with HMAC-SHA256 using the endpoint's secret and sends the hex digest in X-Rewardful-Signature.

Rewardful webhook features

FeatureDetails
ConfigurationDashboard Webhooks page, per endpoint
Signaturehex HMAC-SHA256
Signature headerx-rewardful-signature
Signed contentThe raw request body
KeyOne Signing Secret per endpoint
BodyJSON: object, event, request
Event routingBody field event.type
IDsevent.id per event, request.id per attempt
SuccessHTTP 200 only
RetriesExponential backoff for 3 days, then dropped

Events and payloads

GroupEvent typesCommon use
Affiliatesaffiliate.created, affiliate.confirmed, affiliate.updated, affiliate.deletedOnboard affiliates, sync to a CRM
Linksaffiliate_link.created, affiliate_link.updated, affiliate_link.deletedKeep share links current
Couponsaffiliate_coupon.created, affiliate_coupon.updated, affiliate_coupon.activated, affiliate_coupon.deactivated, affiliate_coupon.deletedSync promo codes to checkout
Referralsreferral.created, referral.lead, referral.converted, referral.deletedTrack funnel stages; converted means a paying customer
Salessale.created, sale.updated, sale.refunded, sale.deletedRevenue attribution
Commissionscommission.created, commission.updated, commission.paid, commission.voided, commission.deletedAffiliate earnings
Payoutspayout.created, payout.updated, payout.due, payout.paid, payout.deleted, payout.failedPayout operations and accounting

affiliate.created fires both when someone signs up and when you create an affiliate in the dashboard or API; affiliate.confirmed follows when they confirm their email. affiliate_coupon.activated means a coupon was re-activated after being deactivated.

Published payload example

Rewardful publishes one full example, affiliate.confirmed, available in the webhook samples library. Other events follow the same envelope with a different object.

{
  "object": {
    "id": "3433fffa-8255-4843-81b1-22aebc21eb34",
    "first_name": "James",
    "last_name": "Bond",
    "email": "james@mi6.co.uk",
    "created_at": "2019-06-24T00:43:36.097Z",
    "updated_at": "2019-06-27T18:38:41.684Z",
    "referrals": 42,
    "conversions": 3,
    "confirmed_at": "2019-06-24T00:43:36.095Z",
    "links": [
      {
        "id": "81bf1128-0ba1-4c1c-835c-286521f8791a",
        "url": "http://www.demo.com:8080/?via=james",
        "token": "james",
        "referrals": 42,
        "conversions": 3
      }
    ],
    "campaign": {
      "id": "82f26208-778e-4e98-a467-6ee264cf96d5",
      "name": "Friends of Acme",
      "created_at": "2019-06-24T00:43:35.985Z",
      "updated_at": "2019-06-24T00:43:35.985Z"
    },
    "paypal_email": null,
    "sign_in_count": 0,
    "stripe_account_id": null,
    "unconfirmed_email": null,
    "stripe_customer_id": null,
    "paypal_email_confirmed_at": null,
    "receive_new_commission_notifications": true
  },
  "event": {
    "id": "7fcdc81d-996a-4b7b-8faf-1db98b9d5507",
    "type": "affiliate.confirmed",
    "created_at": "2019-07-11T05:25:43.940Z",
    "api_version": "v1"
  },
  "request": {
    "id": "66a94f49-2b36-403c-83b8-91677cb1f460"
  }
}

Setting up Rewardful webhooks

Prerequisites

  • A Rewardful account with access to the Webhooks page of the dashboard
  • A publicly reachable HTTPS endpoint

Register Your Endpoint

  1. Open the Webhooks page in your Rewardful dashboard.
  2. Add an endpoint with your URL, for example https://example.com/webhooks/rewardful.
  3. Select its event types, such as affiliate.confirmed, referral.converted and commission.created.
  4. Save.

Get Your Signing Secret

The endpoint's Signing Secret is shown on the same page. Store it as:

WEBHOOK_SECRET=your_endpoint_signing_secret

Use its UTF-8 bytes as the HMAC key, without decoding. If you have several endpoints, each needs its own secret.

Test a delivery

Start a local receiver and forward requests with the Hookdeck CLI:

npx hookdeck-cli listen 3000 rewardful --path /webhooks/rewardful

Add the CLI's URL as an endpoint, then create and confirm a test affiliate to trigger affiliate.created and affiliate.confirmed.

Securing Rewardful webhooks

Compute HMAC-SHA256 of the raw body with the Signing Secret, hex-encode it, and compare it with X-Rewardful-Signature in constant time. Rewardful's Rails, PHP and Django samples read the raw body correctly but compare with == or !==. See Signed webhooks.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-rewardful-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler deduplicates on event.id (not request.id, which changes between attempts) and returns exactly 200.

const express = require('express');

function createRewardfulApp({ firstSeen, crm, finance }) {
  const app = express();
  app.post('/webhooks/rewardful', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { event, object } = parsed;
    if (!event || typeof event !== 'object' || !event.id || typeof event.type !== 'string') {
      return res.sendStatus(400);
    }
    try {
      if (!(await firstSeen(event.id))) return res.sendStatus(200);
      const [resource] = event.type.split('.');
      if (resource === 'affiliate' || resource === 'referral') {
        await crm.sync(event.type, object);
      } else if (resource === 'commission' || resource === 'payout' || resource === 'sale') {
        await finance.record(event.type, object, event.created_at);
      } else {
        console.log('Unhandled Rewardful event', event.type);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

There's no signed timestamp, so a captured request stays valid. event.id is what stops a replay or retry from recording a sale or commission twice.

On a Rewardful source, Hookdeck checks X-Rewardful-Signature as a hex HMAC-SHA256 of the raw body with your Signing Secret. It answers Rewardful with 200 either way, logs a mismatch as failed verification and doesn't forward that request. No timestamp tolerance is involved.

Make Rewardful webhooks production-ready. Hookdeck Event Gateway verifies every Rewardful event, sends affiliate updates to your CRM and commissions to finance, and keeps them replayable beyond Rewardful's 3-day retry window.

Rewardful webhook limitations and pain points

Only 200 counts

The Problem: Events are retried for days even though your endpoint accepted them.

Why It Happens: Rewardful treats any status other than 200 as a failure, including 201, 202 and 204.

Workarounds:

  • Return 200 explicitly from the webhook route.

How Hookdeck Can Help: Hookdeck answers Rewardful with 200 itself, whatever status your service returns.

A three-day limit

The Problem: Events from a long outage or a broken deploy are lost for good.

Why It Happens: Rewardful retries with exponential backoff for 3 days, then drops the event.

Workarounds:

  • Reconcile affiliates, referrals and commissions through the Rewardful API after incidents.

How Hookdeck Can Help: Hookdeck stores each event on the first attempt, so it can be replayed whenever your service is ready.

One example payload

The Problem: Code for sales, commissions and payouts has to be written without example bodies.

Why It Happens: Only affiliate.confirmed has a documented example; the docs say other objects match the REST API.

Workarounds:

  • Use the REST API reference for each object, and capture real events in a test account.

How Hookdeck Can Help: Hookdeck keeps the full body of every event it receives, so real payloads for each type are on hand.

Per-endpoint secrets

The Problem: Deliveries to one URL fail verification after adding a second endpoint.

Why It Happens: Every endpoint has its own Signing Secret, and one endpoint's secret doesn't verify another's deliveries.

Workarounds:

  • Give each endpoint its own path or service.

How Hookdeck Can Help: One Hookdeck source per endpoint keeps each secret paired with its traffic.

Best practices

Deduplicate on event.id

request.id identifies the attempt and changes on retry. event.id stays the same, so use it as your idempotency key, as our idempotency guide recommends.

Treat referral.converted as the conversion

It marks the move to a paying customer. Earlier stages (created, lead) are useful for funnel analytics but don't represent revenue.

Handle refunds and voids

sale.refunded and commission.voided reverse earlier events. Make sure your accounting applies them against the original records.

Acknowledge, then process

Return 200 after storing the event and do CRM and accounting updates in the background, as described in the asynchronous processing guide.

Conclusion

Rewardful's webhooks follow a clear model: an event block with a stable ID, the changed object, and a hex HMAC over the body. Return exactly 200, deduplicate on event.id rather than request.id, and plan for the 3-day retry limit and the lack of example bodies beyond affiliate.confirmed.

Hookdeck Event Gateway handles the response code for you and keeps every affiliate, sale and payout event replayable.

Get started with Hookdeck for free and handle Rewardful webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.