Skip to content
API · Webhooks

Webhooks signed with HMAC

Receive store events in your external system in real time.

What they are

A webhook is an HTTPS endpoint of yours that the API POSTs to when an event occurs in your store (an order completed, a recharge approved, a customer registered). Removes the need to poll the API.

Register an endpoint

POST /api/v1/external/webhooks · scope webhooks:manage

Body

FieldTypeDescription
urlstringHTTPS URL of your endpoint. Required.
descriptionstringInternal label (max 200). Optional.
eventsarrayList of subscribed events. At least one.

Request

curl -X POST https://api.nuvlyx.com/api/v1/external/webhooks \
  -H "Authorization: Bearer nvl_live_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.my-erp.com/hooks/nuvlyx",
    "description": "Production ERP",
    "events": ["order.completed", "recharge.approved"]
  }'

Response 201

{
  "id": "wep_…",
  "url": "https://api.my-erp.com/hooks/nuvlyx",
  "events": ["order.completed", "recharge.approved"],
  "status": "ACTIVE",
  "secret": "a8f3c8e1b7d4...64-hex-chars...",
  "createdAt": "2026-05-18T..."
}

The secret is returned ONLY ONCE. Store it in a safe place — it's used to verify the signature of every event you receive.

Event catalog

EventWhen it fires
order.createdOrder created in PENDING or PROCESSING status.
order.completedOrder closed with all items DELIVERED.
order.items_fulfilledBackfill of PENDING items after stock upload.
recharge.approvedWallet recharge approved.
recharge.rejectedRecharge rejected by OWNER or the system.
customer.registeredNew customer created in the store.
stock.lowA variation crosses the low-stock threshold.
stock.availableA previously-zero variation received stock.

Payload shape

{
  "id": "evt_a4f2c8e1b3d7...",
  "type": "order.completed",
  "created_at": "2026-05-18T15:42:00Z",
  "livemode": true,
  "data": {
    // event-specific payload
  }
}

Verify the signature

Every POST includes an X-Nuvlyx-Signature header (Stripe-style):

X-Nuvlyx-Signature: t=1747754520,v1=<hmac_sha256_hex>

To verify: concatenate {t}.{rawBody}, compute HMAC-SHA256 with your secret and compare against v1 with a constant-time comparison.

Node.js

import crypto from 'node:crypto';

export function verifyNuvlyxSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}

Python

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=') for p in header.split(','))
    expected = hmac.new(
        secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])

Retries and suspension

  • Your endpoint must respond 2xx within 8 seconds.
  • On timeout, network error, or 5xx: up to 3 automatic retries (RabbitMQ backoff).
  • After 5 consecutive failures, the endpoint is automatically marked SUSPENDED and stops receiving events. Re-create it to reactivate.
  • The first 2xx delivery resets the failure counter to zero.

Deduplication

The X-Nuvlyx-Event-Id header carries the same id as the payload. Persist it and reject duplicates — events can be delivered more than once on retries.

Audit deliveries

GET /api/v1/external/webhooks/:id/deliveries returns the last 50 attempts with status, latency and error message.

Delete an endpoint

DELETE /api/v1/external/webhooks/:id