Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.nippy.la/llms.txt

Use this file to discover all available pages before exploring further.

Nippy sends signed webhooks to the webhookUrl configured in your campaign. They are the main channel for knowing what happened — always verify the signature before processing.

Envelope structure

All events share the same envelope:
{
  "id": "wh_uuid",
  "event": "spin.completed",
  "deliveryAttempt": 1,
  "traceId": "trace_uuid",
  "createdAt": "2026-04-30T22:40:34.147Z",
  "data": {
    "businessId": "bank-bbva-123",
    "userId": "user-123",
    "campaignId": "camp-abc",
    "spinId": "spin-xyz",
    "outcome": "won_points",
    "gift": {
      "giftId": "gift-001",
      "name": "500 points",
      "pointsValue": 500
    },
    "claimRequired": false,
    "spunAt": "2026-04-30T22:40:34.128Z"
  }
}

Headers you receive

HeaderContent
X-Nippy-EventEvent type (e.g. spin.completed)
X-Nippy-SignatureHMAC-SHA256 signature of the raw body

Verify the signature

Always verify the signature. If you don’t, anyone can send fake webhooks to your endpoint.
import { verifyWebhookSignature } from '@nippy/sdk'
import express from 'express'

const app = express()

app.post('/webhooks/nippy',
  express.raw({ type: 'application/json' }),  // raw body, not parsed
  (req, res) => {
    const isValid = verifyWebhookSignature({
      body: req.body.toString(),
      signature: req.headers['x-nippy-signature'] as string,
      secret: process.env.NIPPY_WEBHOOK_SECRET
    })

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

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

    switch (event.event) {
      case 'spin.completed':
        handleSpinCompleted(event.data)
        break
      case 'claim.completed':
        handleClaimCompleted(event.data)
        break
      case 'spin_auto_unlocked':
        handleAutoUnlocked(event.data)
        break
      case 'grant_points_awarded':
        handlePointsAwarded(event.data)
        break
    }

    res.status(200).send('ok')
  }
)
Use express.raw(), not express.json(). The signature is calculated over the raw body — if you parse it before verifying, verification will always fail.

Available events

spin.completed

Fires when a spin completes (manual or automatic).
{
  "event": "spin.completed",
  "data": {
    "businessId": "bank-123",
    "userId": "user-123",
    "campaignId": "camp-abc",
    "spinId": "spin-xyz",
    "outcome": "won_points",
    "gift": {
      "giftId": "gift-001",
      "name": "500 points",
      "pointsValue": 500
    },
    "claimRequired": false,
    "spunAt": "2026-04-30T22:40:34.128Z"
  }
}
What to do here: if outcome === 'won_points' and claimRequired === false, credit gift.pointsValue to the user.

claim.completed

Fires when the user claims a prize that required confirmation.
{
  "event": "claim.completed",
  "data": {
    "businessId": "bank-123",
    "userId": "user-123",
    "campaignId": "camp-abc",
    "spinId": "spin-xyz",
    "claimId": "claim-001",
    "gift": {
      "giftId": "gift-002",
      "name": "Sony Headphones",
      "type": "won_physical"
    },
    "claimedAt": "2026-04-30T22:45:00.000Z"
  }
}
What to do here: initiate the delivery process for the physical or digital prize.

spin_auto_unlocked

Fires when track() evaluated the rules and unlocked an automatic spin, just before spinning.
{
  "event": "spin_auto_unlocked",
  "data": {
    "userId": "user-123",
    "campaignId": "camp-abc",
    "eventType": "card.purchase.completed",
    "ruleId": "rule-001"
  }
}

grant_points_awarded

Fires when a rule with action: 'grant_points' was activated and points were credited directly.
{
  "event": "grant_points_awarded",
  "data": {
    "userId": "user-123",
    "campaignId": "camp-abc",
    "pointsValue": 200,
    "eventType": "card.purchase.completed"
  }
}
What to do here: credit data.pointsValue to the user in your system.

Retries

If your endpoint returns any status code other than 2xx, Nippy retries delivery automatically:
AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
After the fourth failed attempt, the webhook is marked as failed. You can check it in the webhook log.

Idempotency

The envelope id field is unique per delivery. If you receive the same id twice (network retry), process it only once by storing already-processed IDs in your database.