Webhooks

Webhooks let your application receive real-time HTTP callbacks whenever important events occur in your Ploints account. Instead of polling the API, you register a URL and Ploints pushes events to you.

Setting Up Webhooks

You can register a webhook endpoint via the API or from the Ploints dashboard under Integrations → Webhooks.

Via API

curl -X POST https://ploints.space/api/v1/webhooks \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/webhooks/ploints",
    "events": ["points.earned", "customer.created"]
  }'

The response includes a secret field. Store this securely -- you will need it to verify webhook signatures.

{
  "success": true,
  "data": {
    "id": "wh_abc123",
    "url": "https://yoursite.com/webhooks/ploints",
    "events": ["points.earned", "customer.created"],
    "secret": "whsec_k8x2..."
  }
}

Available Events

Subscribe to any combination of the following event types:

EventDescription
points.earnedA customer was awarded points
points.redeemedA customer redeemed points
customer.createdA new customer was enrolled
customer.tier_changedA customer moved to a new loyalty tier
badge.issuedA badge was issued to a customer
reward.redeemedA customer redeemed a reward
campaign.conversionA campaign conversion was tracked (e.g. referral, influencer link)

Payload Format

Webhook payloads are sent as POST requests with a JSON body:

{
  "id": "evt_a1b2c3",
  "type": "points.earned",
  "timestamp": "2025-06-15T10:30:00Z",
  "data": {
    "customerEmail": "jane@example.com",
    "amount": 50,
    "description": "Purchase #1234",
    "newBalance": 350
  }
}

Signature Verification

Every webhook request includes a X-Ploints-Signature header containing an HMAC-SHA256 signature of the raw request body. Always verify this signature to ensure the request genuinely came from Ploints.

How it works

  1. Read the raw request body as a string (do not parse JSON first).
  2. Compute an HMAC-SHA256 hash of the raw body using your webhook secret as the key.
  3. Compare the computed hash to the value in the X-Ploints-Signature header using a constant-time comparison.
  4. If they match, the request is authentic. If not, reject it with a 401.

Node.js Example

import crypto from "node:crypto";

function verifyWebhookSignature(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Usage in an Express handler
app.post("/webhooks/ploints", (req, res) => {
  const signature = req.headers["x-ploints-signature"] as string;
  const rawBody = req.body; // raw string (use express.raw())

  if (!verifyWebhookSignature(rawBody, signature, process.env.PLOINTS_WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(rawBody);

  switch (event.type) {
    case "points.earned":
      // Handle points earned
      break;
    case "customer.created":
      // Handle new customer
      break;
    // ... handle other events
  }

  res.status(200).json({ received: true });
});

Retry Policy

If your endpoint returns a non-2xx status code or times out (after 10 seconds), Ploints will retry the delivery with exponential backoff:

  • 1st retry: 1 minute after the initial attempt
  • 2nd retry: 5 minutes
  • 3rd retry: 30 minutes
  • 4th retry: 2 hours
  • 5th retry: 12 hours (final attempt)

After 5 failed retries the event is marked as failed. You can view and manually resend failed deliveries from the dashboard under Integrations → Webhooks → Delivery Log.

Best Practices

  • Always verify signatures -- never trust unverified payloads.
  • Respond quickly -- return a 200 immediately and process asynchronously if your handler is slow.
  • Handle duplicates -- use the id field to deduplicate in case of retries.
  • Use HTTPS -- webhook URLs must use HTTPS for security.

Want to manage webhooks programmatically? See the API Reference or use the TypeScript SDK.