> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nudj.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Implementation

> Sign verification, retry semantics, and endpoint examples for Nudj webhooks.

Nudj emits HTTP POST webhooks for domain events your downstream systems care about — reward earned, reward delivered, subscription updates, Shopify-driven achievement triggers, and more. This page covers the protocol — signatures, retries, and endpoint implementation. For the full list of emitted events, see the [events catalog](/enterprise/webhooks-events-catalog). To debug a delivery, see the [delivery logs](/enterprise/webhook-logs) page.

## Configuring a webhook

Webhook configurations are managed via the Admin API under `/api/v2/admin/webhooks/configs`. Each config specifies:

* **`url`** — your endpoint. Must be HTTPS in production.
* **`events`** — list of event types the config subscribes to. See [Webhook Events Catalog](/enterprise/webhooks-events-catalog).
* **`secret`** — used to compute the HMAC signature on each delivery.
* **`active`** — pause or resume deliveries without deleting the config.

Example config:

```json theme={null}
{
  "url": "https://your-api.example.com/webhooks/nudj",
  "events": ["reward.earned", "reward.delivered", "reward.delivery_failed"],
  "secret": "whsec_...",
  "active": true
}
```

## Request shape

Every delivery is an HTTP POST with a JSON body:

```json theme={null}
{
  "event": "reward.earned",
  "timestamp": "2026-04-17T14:30:00.000Z",
  "data": {
    "reward": { "id": "reward_abc123", "type": "asset", "value": 100, "name": "Weekly Bonus" },
    "user": { "id": "user_xyz789", "externalUserId": "customer_456" },
    "challenge": { "id": "challenge_def456", "name": "Weekly Shopping Challenge" }
  }
}
```

Headers:

* `Content-Type: application/json`
* `X-Nudj-Signature: sha256=<hex>` — HMAC-SHA256 of the raw body, signed with your configured `secret`.
* `X-Nudj-Event` — convenience header mirroring `event` in the payload.
* `X-Nudj-Delivery-Id` — unique per delivery attempt, stable across retries. Use this to idempotency-key your processing.

## Signature verification

Compute HMAC-SHA256 over the **raw** request body using your configured `secret`, then compare against the value after `sha256=` in the `X-Nudj-Signature` header. Use a timing-safe comparison.

<CodeGroup>
  ```javascript Node.js / Express theme={null}
  import express from "express";
  import crypto from "crypto";

  const app = express();

  // Raw body is required for signature verification
  app.post(
    "/webhooks/nudj",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const signature = (req.headers["x-nudj-signature"] || "").replace(
        "sha256=",
        ""
      );
      const expected = crypto
        .createHmac("sha256", process.env.NUDJ_WEBHOOK_SECRET)
        .update(req.body)
        .digest("hex");

      if (
        signature.length !== expected.length ||
        !crypto.timingSafeEqual(
          Buffer.from(signature, "hex"),
          Buffer.from(expected, "hex")
        )
      ) {
        return res.status(401).send("invalid signature");
      }

      const event = JSON.parse(req.body.toString("utf8"));
      // process event here, ideally async via a queue
      res.status(200).send("ok");
    }
  );
  ```

  ```python Python / Flask theme={null}
  from flask import Flask, request, abort
  import hmac, hashlib, os

  app = Flask(__name__)

  @app.post("/webhooks/nudj")
  def handle_nudj_webhook():
      raw = request.get_data()
      signature = request.headers.get("X-Nudj-Signature", "").removeprefix("sha256=")
      expected = hmac.new(
          os.environ["NUDJ_WEBHOOK_SECRET"].encode(),
          raw,
          hashlib.sha256,
      ).hexdigest()
      if not hmac.compare_digest(signature, expected):
          abort(401)
      # process JSON payload (request.get_json() is safe after signature passes)
      return ("ok", 200)
  ```
</CodeGroup>

<Warning>
  Verify the signature against the **raw** request body before parsing as JSON. Re-serialising the parsed body will produce a different byte sequence and the HMAC will never match.
</Warning>

## Response and retry semantics

* Respond with `2xx` within 30 seconds to acknowledge receipt.
* Any `5xx` response, connection failure, or timeout triggers a retry.
* Retries use exponential backoff.
* A delivery that fails every retry attempt is marked `failed` in the [delivery logs](/enterprise/webhook-logs).

### Idempotency

Process events idempotently. The `X-Nudj-Delivery-Id` header is stable across retries of the same delivery attempt, and the payload's `data.*.id` fields identify the underlying entity. Storing processed delivery IDs in your side lets you no-op duplicates safely.

## Local development

Use a tunnel such as [ngrok](https://ngrok.com) or [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to expose your local endpoint:

```bash theme={null}
ngrok http 3000
# paste the HTTPS forwarding URL into your webhook config's `url`
```

## Related pages

<CardGroup cols={2}>
  <Card title="Webhook events catalog" icon="list" href="/enterprise/webhooks-events-catalog">
    Reference of every event type Nudj emits, with example payloads.
  </Card>

  <Card title="Webhook delivery logs" icon="clock-rotate-left" href="/enterprise/webhook-logs">
    Inspect retry state, bodies, and failures via the Admin API.
  </Card>

  <Card title="Reward integration" icon="wallet" href="/enterprise/reward-integration">
    Higher-level integration patterns for getting rewards to downstream systems.
  </Card>
</CardGroup>

> Last reviewed: 2026-04
