Rewardly Docs
Guides

Webhooks

Subscribe to events, verify signatures, and test your handler.

Webhooks push events to your server as rewards move through their lifecycle — no polling required.

Subscribe

curl -X POST https://paybilt-rewards-api.fly.dev/v1/webhook-endpoints \
  -H "Authorization: Bearer $REWARDLY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.yourapp.com/rewardly-webhook", "events": ["reward.redeemed", "card.issued"]}'

An empty events array subscribes to everything. The response includes the endpoint's signing secret (whsec_...) — store it now, it's shown once. Rotate it later with POST /v1/webhook-endpoints/:id/rotate-secret.

Event catalog

EventFires when
reward.createdA reward is accepted and funds are held
reward.sentThe recipient email is queued
reward.openedThe recipient visits their redemption link
reward.partially_redeemedA choice reward has some value used
reward.redeemedThe full value has been turned into cards
reward.expiredA reward hit its expiry; remaining value returned
reward.canceledYou canceled a reward
card.issuedA gift card was successfully issued
card.failedA card could not be issued (value re-credited)
wallet.creditedA top-up settled

Payload shape

{
  "id": "evt_a1b2c3d4e5f6",
  "type": "reward.redeemed",
  "createdAt": "2026-08-06T12:00:00.000Z",
  "data": { "id": "rw_...", "status": "REDEEMED", "valueCents": 5000 }
}

Verify signatures

Every delivery is signed. Compute an HMAC-SHA256 of "{X-Webhook-Timestamp}.{raw request body}" with your endpoint secret and compare it to the X-Webhook-Signature header (v1=<hex>):

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req: { headers: Record<string, string>; rawBody: string }) {
  const timestamp = req.headers["x-webhook-timestamp"];
  const signature = req.headers["x-webhook-signature"]; // "v1=abc123..."

  // reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = `v1=${createHmac("sha256", process.env.WEBHOOK_SECRET!)
    .update(`${timestamp}.${req.rawBody}`)
    .digest("hex")}`;

  return (
    expected.length === signature.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  );
}

Use the raw request body — parsing and re-serializing JSON will change the bytes and break the signature.

Test your handler

Queue a signed ping event any time:

curl -X POST https://paybilt-rewards-api.fly.dev/v1/webhook-endpoints/we_123/test \
  -H "Authorization: Bearer $REWARDLY_KEY"

Then check the result with GET /v1/webhook-endpoints/:id/deliveries, which shows status, attempt count, and the HTTP status your server returned.

Delivery behavior

  • Respond with any 2xx quickly (do the work async) — anything else counts as a failure and is retried with backoff.
  • Deliveries can arrive out of order; use createdAt or reward status rather than arrival order.
  • Handlers should be idempotent: use the event id to dedupe.
  • Pause an endpoint without deleting it via PATCH /v1/webhook-endpoints/:id with {"active": false}.

On this page