useLoyalty
Server SDK

Webhooks

Receive real-time events from your loyalty program

Webhooks push events to your server the moment they happen — no polling required. Register an HTTPS endpoint and choose which event types to receive.

Webhook Events

EventFired when
member.createdA new member is created
member.updatedA member's profile is updated
points.awardedPoints are awarded to a member
points.deductedPoints are deducted from a member
badge.awardedA badge is earned
quest.completedA quest is completed
referral.convertedA referral is converted
promo.redeemedA promo code is redeemed
reward.redeemedA reward is redeemed

List Webhooks

GET /api/v1/webhooks

SDK

const webhooks = await useLoyalty.webhooks.list();

Register a Webhook

POST /api/v1/webhooks

Request Body

{
  "url": "https://yourapp.com/webhooks/useloyalty",
  "events": ["points.awarded", "quest.completed", "reward.redeemed"]
}

SDK

const webhook = await useLoyalty.webhooks.create({
  url: 'https://yourapp.com/webhooks/useloyalty',
  events: ['points.awarded', 'quest.completed', 'reward.redeemed'],
});

Response

{
  "id": "wh_abc123",
  "url": "https://yourapp.com/webhooks/useloyalty",
  "events": ["points.awarded", "quest.completed", "reward.redeemed"],
  "isActive": true,
  "createdAt": "2024-01-20T10:00:00Z"
}

Delete a Webhook

DELETE /api/v1/webhooks/:webhookId

SDK

await useLoyalty.webhooks.delete('wh_abc123');

Test a Webhook

Send a test payload to your endpoint to verify it's receiving correctly.

POST /api/v1/webhooks/:webhookId/test

SDK

const { success } = await useLoyalty.webhooks.test('wh_abc123');

Verifying Webhook Signatures

Every webhook request includes an X-UseLoyalty-Signature header. Always verify it before processing — it proves the request came from useLoyalty, not a third party.

import { verifyWebhookSignature } from '@useloyalty/sdk';

// Express — use raw body middleware BEFORE json parser for this route
app.post(
  '/webhooks/useloyalty',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const isValid = verifyWebhookSignature(
      req.body.toString(),           // raw string body
      req.headers['x-useloyalty-signature'] as string,
      process.env.USELOYALTY_WEBHOOK_SECRET!
    );

    if (!isValid) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body.toString());
    handleEvent(event);

    res.send('ok');
  }
);

Pass the raw request body string to verifyWebhookSignature — do not parse it as JSON first. Parsing and re-serializing changes whitespace and key order, which breaks the signature check.

Next.js App Router

// app/api/webhooks/useloyalty/route.ts
import { verifyWebhookSignature } from '@useloyalty/sdk';

export async function POST(req: Request) {
  const body = await req.text(); // raw string
  const signature = req.headers.get('x-useloyalty-signature') ?? '';

  const isValid = verifyWebhookSignature(
    body,
    signature,
    process.env.USELOYALTY_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return new Response('Unauthorized', { status: 401 });
  }

  const event = JSON.parse(body);
  await handleUseLoyaltyEvent(event);

  return new Response('ok');
}

Webhook Payload Shape

{
  "id": "evt_xyz789",
  "type": "points.awarded",
  "timestamp": "2024-01-20T10:30:00Z",
  "signature": "sha256=abc123...",
  "data": {
    "memberId": "clx123abc",
    "externalId": "user_123",
    "pointsAwarded": 100,
    "newBalance": 1600,
    "transactionId": "txn_abc"
  }
}

Handling Events

async function handleUseLoyaltyEvent(event: WebhookEvent) {
  switch (event.type) {
    case 'points.awarded':
      await notifyUser(event.data.externalId, `You earned ${event.data.pointsAwarded} points!`);
      break;

    case 'quest.completed':
      await notifyUser(event.data.externalId, `Quest complete! You earned ${event.data.pointsAwarded} points.`);
      break;

    case 'badge.awarded':
      await notifyUser(event.data.externalId, `New badge: ${event.data.badgeName}!`);
      break;

    case 'reward.redeemed':
      if (event.data.couponCode) {
        await sendCouponEmail(event.data.externalId, event.data.couponCode);
      }
      break;
  }
}

Retry Behavior

useLoyalty retries failed webhook deliveries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

A delivery is considered failed if your endpoint returns a non-2xx status or doesn't respond within 10 seconds. After 5 failed attempts the webhook is marked as failed and no further retries occur.

Return a 200 response immediately and process the event asynchronously (e.g. push to a queue). Long-running handlers increase the chance of timeouts and duplicate deliveries.

On this page