Agent skill

Rewardful Webhooks Skill

Receive and verify Rewardful webhooks. Use when setting up a Rewardful webhook endpoint, checking the X-Rewardful-Signature header (HMAC-SHA256 hex of the raw body), debugging 401 signature failures, or handling affiliate and referral events like affiliate.confirmed, referral.converted, commission.created and payout.paid.

Install this skill

npx skills add hookdeck/webhook-skills --skill rewardful-webhooks


Rewardful (affiliate and referral tracking for Stripe SaaS) sends a JSON POST to each endpoint you add on the dashboard's Webhooks page when affiliates, links, coupons, referrals, sales, commissions or payouts change. Each request carries an X-Rewardful-Signature header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with that endpoint's Signing Secret.

When to Use This Skill

  • How do I receive Rewardful webhooks?
  • How do I verify the X-Rewardful-Signature header?
  • How do I handle affiliate.confirmed, referral.converted, commission.created or payout.paid events?
  • Why does my Rewardful webhook signature verification fail (401)?
  • Why does Rewardful keep retrying my webhook?

Verification (core)

Header: X-Rewardful-Signature: <64 hex chars>, no prefix. Signed content: the raw body bytes only. No timestamp, so there is no replay window to check. Key: the endpoint's Signing Secret as UTF-8 bytes, used as-is (not base64- or hex-decoded).

const crypto = require('crypto');

function verifyRewardfulSignature(rawBody, signature, secret = process.env.REWARDFUL_WEBHOOK_SECRET) {
  if (!secret || !Buffer.isBuffer(rawBody)) return false;
  // Validate before decoding: Buffer.from(x, 'hex') silently drops bad characters.
  if (typeof signature !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex')); // equal length: 32 bytes
}
  • Sign the raw bytes (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()); parsing and re-serializing the JSON breaks the signature.
  • Missing, malformed or mismatched signature: 401. Bad JSON after a valid signature: 400.
  • Return 200 to acknowledge. Rewardful counts only 200 as success; any other status is retried with exponential backoff for 3 days, then dropped.
  • In Python use hmac.compare_digest on bytes (bytes.fromhex after a [0-9a-fA-F]{64} check).

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Dispatch on the body's event.type (<object>.<action>); no header names the event. The body is { object, event: { id, type, created_at, api_version }, request: { id } }. Dedupe on event.id.

EventSent When
affiliate.createdAn affiliate signed up or was created in the dashboard or API
affiliate.confirmedAn affiliate confirmed their email address
referral.leadA referral became a lead
referral.convertedA referral became a paying customer
sale.createdA sale was created
sale.refundedA sale was refunded
commission.createdA commission was created
commission.paidA commission was paid
payout.dueA payout is due
payout.paidA payout was paid
payout.failedA payout failed

Rewardful documents 31 types in total (affiliate_link.*, affiliate_coupon.*, sale.*, commission.*, payout.* and more); see references/overview.md.

Environment Variables

REWARDFUL_WEBHOOK_SECRET=your_endpoint_signing_secret   # Webhooks page in the Rewardful dashboard

Local Development

Rewardful can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

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

Use the printed URL as the endpoint URL in Rewardful.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →