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

# Webhooks & Events

> Receive real-time notifications and monitor platform events

# Webhooks & Events

**Time**: \~10 minutes | **Difficulty**: Beginner

Use the TagadaPay Node SDK to register HTTPS endpoints for real-time notifications, verify deliveries cryptographically, and query the event log for debugging and analytics.

***

## What webhooks are (and why use them)

**Webhooks** are HTTP callbacks TagadaPay sends to your server when something happens on the platform — for example, a successful payment or a subscription change. Your endpoint receives a JSON payload so you can update your database, trigger fulfillment, notify internal tools, or sync with Zapier/n8n without polling the API.

<Info>
  Webhooks run **asynchronously** after the triggering action. Design your handler to respond quickly (e.g. validate the signature, enqueue work, return `2xx`). Heavy work should happen in a background job.
</Info>

***

## Create a webhook

Register a URL and the event types you care about. The API returns the endpoint id, URL, **signing secret**, subscribed types, and whether the endpoint is enabled.

```ts theme={null}
import Tagada from '@tagadapay/node-sdk';

const tagada = new Tagada('your-api-key');

const webhook = await tagada.webhooks.create({
  storeId: 'store_...',
  url: 'https://api.example.com/webhooks/tagada',
  eventTypes: ['order/paid', 'subscription/created', 'subscription/canceled'],
  description: 'Production notifications',
});

// { id, url, secret, eventTypes, enabled, storeId, createdAt, ... }
console.log(webhook.id, webhook.secret);
```

<Tip>
  Store `webhook.secret` securely (environment variable or secrets manager). It is returned when you create the endpoint and is required to verify that payloads really came from TagadaPay.
</Tip>

***

## The signing secret

Each webhook endpoint has a unique **secret**. TagadaPay uses it to compute an **HMAC-SHA256** signature over the **raw JSON body** of each delivery. Your server must verify that signature before trusting the payload.

<Note>
  Event type strings in `eventTypes` must **exactly match** the names listed below (slash format like `order/paid`). The SDK validates event types at build time via TypeScript and at runtime — passing an invalid type will throw an error immediately.
</Note>

***

## Payload envelope

Every delivery is a JSON object with the same top-level shape:

```json theme={null}
{
  "id": "8f1c04b1-3a2e-4a6a-9a63-2b0c5f7a3f7b",
  "type": "order/paid",
  "createdAt": "2026-08-27T14:22:41.812Z",
  "data": {
    "orderId": "order_5f8ec3a7b2d1",
    "storeId": "store_1a2b3c4d5e6f",
    "totalAmount": 4990,
    "currency": "EUR",
    "customerEmail": "buyer@example.com"
  },
  "metadata": {
    "source": "tagadapay",
    "version": "1.0",
    "accountId": "acc_9e8d7c6b5a4f",
    "storeId": "store_1a2b3c4d5e6f"
  }
}
```

<Warning>
  The top-level **`id` is the event's own UUID** — not the id of the order, subscription or payment it describes. The resource's id lives inside `data` (`data.orderId`, `data.subscriptionId`, `data.paymentId`, etc., depending on the event). Reading `payload.id` as an order id will return the event UUID and fail every downstream lookup.
</Warning>

Use `id` as the deduplication key (see [Delivery contract](#delivery-contract) below). Use `type` to route the handler. The exact contents of `data` depend on the event type — inspect a real delivery in the [Delivery history](#delivery-history) view of the CRM to see what your subscriptions actually receive.

***

## Delivery contract

TagadaPay `POST`s each event to your endpoint with a strict, bounded delivery contract. Design your handler to fit inside it.

### Timeout

**10 seconds per attempt.** If your endpoint has not returned a response within 10 seconds, TagadaPay aborts the connection and treats the attempt as failed. The abort does **not** cancel any work already running on your side, so an aborted attempt that keeps processing can end up finishing twice — one on the aborted call, one on the retry. Handlers must be idempotent (see below).

### Retry schedule

Up to **6 attempts** per event over roughly **24 hours**. The first attempt fires immediately when the event is emitted; later attempts use smart backoff based on the failure type (timeouts and 5xx retry quickly; HTTP 409 / Woo sync lag waits longer).

| Attempt | Typical delay after previous failure |
| ------- | ------------------------------------ |
| 1       | Immediate                            |
| 2       | 0–4 seconds                          |
| 3       | \~1 minute                           |
| 4       | \~10 minutes                         |
| 5       | \~1 hour                             |
| 6       | \~6 hours                            |

Permanent client errors (`401`, `404`, `400`, `403`) stop retrying immediately — they will not consume later attempts. After attempt 6 fails, the delivery moves to a dead-letter state. You can replay it manually from the CRM once the endpoint is fixed.

### Ack fast, process async

Return a 2xx (typically `200`) **as soon as possible after verifying the signature** — ideally within 2 seconds. Do the actual work (fulfilment, database writes, calls to Shopify, e-mails) in a background job. If your handler takes longer than 10 seconds because it processes synchronously, every attempt for that event will time out, be retried, and you will run the same work up to three times in parallel.

<Tip>
  The right shape for a webhook handler:\
    1. Verify the signature.\
    2. Enqueue a background job with the event.\
    3. Respond `200 OK`.\
  Everything else — database writes, external API calls, e-mails — happens in the worker.
</Tip>

### Idempotency (dedupe by `event.id`)

The same event can reach you more than once — an aborted attempt that kept processing, an attempt-2 landing while attempt-1 completes late, or a manual replay from the CRM. Use the top-level `id` as your deduplication key: record each `id` you have processed and short-circuit if you see it again. Two deliveries with the same `id` describe the same event and must produce the same side effect only once.

### Auto-disable of dead endpoints

An endpoint that goes **3 continuous days without a single successful delivery** is disabled automatically, so a URL that has stopped answering does not silently consume your allowance. A badge shows the auto-disabled state in the CRM, and one click re-enables it once you have fixed the endpoint.

***

## List webhooks

```ts theme={null}
const endpoints = await tagada.webhooks.list('store_...');
// Webhook[]
```

***

## Delete a webhook

```ts theme={null}
const { success, id } = await tagada.webhooks.del('wh_...');
```

***

## Available event types

All webhook event types use the **slash format** (`category/event`). These are the only valid values accepted by the `eventTypes` field.

### Order events

| Event type               | Description                                                           |
| ------------------------ | --------------------------------------------------------------------- |
| `order/paid`             | Order successfully paid — trigger fulfillment or CRM updates          |
| `order/created`          | Order record created                                                  |
| `order/refunded`         | Order refunded                                                        |
| `order/failed`           | Order payment failed                                                  |
| `order/upsellStarted`    | Post-purchase upsell flow initiated                                   |
| `order/paymentInitiated` | Payment process started for an order                                  |
| `order/syncFailed`       | Storefront sync to Shopify/Woo failed (paid order may be unfulfilled) |

### Checkout events

| Event type                | Description                              |
| ------------------------- | ---------------------------------------- |
| `checkout/initiated`      | Checkout session started                 |
| `checkout/emailValidated` | Customer email validated during checkout |

### Payment events

| Event type           | Description                                  |
| -------------------- | -------------------------------------------- |
| `payment/created`    | Payment record created                       |
| `payment/succeeded`  | Payment completed successfully               |
| `payment/failed`     | Payment attempt failed                       |
| `payment/refunded`   | Payment refunded                             |
| `payment/authorized` | Payment authorized (not yet captured)        |
| `payment/rejected`   | Payment rejected by processor or fraud rules |

### Subscription events

| Event type                         | Description                              |
| ---------------------------------- | ---------------------------------------- |
| `subscription/created`             | New subscription started                 |
| `subscription/canceled`            | Subscription canceled                    |
| `subscription/paused`              | Subscription paused                      |
| `subscription/resumed`             | Subscription resumed                     |
| `subscription/pastDue`             | Subscription payment past due            |
| `subscription/rebillUpcoming`      | Upcoming rebill notification             |
| `subscription/rebillSucceeded`     | Rebill charge succeeded                  |
| `subscription/rebillDeclined`      | Rebill charge declined                   |
| `subscription/cancelScheduled`     | Cancellation scheduled for end of period |
| `subscription/rebillCaptureFailed` | Rebill capture failed                    |

### Funnel events

| Event type                | Description                   |
| ------------------------- | ----------------------------- |
| `funnel/sessionStarted`   | Funnel session started        |
| `funnel/sessionCompleted` | Funnel session completed      |
| `funnel/stepEntered`      | Visitor entered a funnel step |
| `funnel/stepViewed`       | Funnel step viewed            |
| `funnel/stepExited`       | Visitor exited a funnel step  |
| `funnel/converted`        | Funnel conversion recorded    |
| `funnel/customEvent`      | Custom funnel event fired     |
| `funnel/sessionAbandoned` | Funnel session abandoned      |

### Club events

| Event type                   | Description                 |
| ---------------------------- | --------------------------- |
| `club/membershipActivated`   | Club membership activated   |
| `club/membershipDeactivated` | Club membership deactivated |

### Security events

| Event type               | Description             |
| ------------------------ | ----------------------- |
| `security/abuseDetected` | Abuse or fraud detected |

<Warning>
  The SDK and API **reject** any event type not in this list. If you pass an invalid type like `order.paid` (dot format) or `s_order_paid` (internal format), you will receive a validation error.
</Warning>

***

## Events API (query & analytics)

Use the **Events** resource to audit activity, build dashboards, or debug webhook payloads.

### Recent events

```ts theme={null}
const recent = await tagada.events.recent(25);
// AppEvent[] — includes eventType, createdAt, payload, storeId, ...
```

### Statistics

```ts theme={null}
const stats = await tagada.events.statistics({ storeId: 'store_...' });
// { totalEvents, eventsByType, processedCount, unprocessedCount }
```

### List with filters and pagination

```ts theme={null}
const { data, total, page, pageSize, hasMore } = await tagada.events.list({
  pagination: { page: 1, pageSize: 50 },
  sortBy: { field: 'appEvents.createdAt', direction: 'desc' },
  filters: {
    'stores.id': ['store_...'],
    // Event types here are often internal analytics ids (e.g. s_order_paid) — see Note below
    'appEvents.eventType': ['s_order_paid'],
  },
});
```

Optional filters include date ranges on `appEvents.createdAt` / `appEvents.processedAt`, `customer.email`, `appEvents.draft`, and free-text `search`.

<Note>
  **Webhook subscriptions** use slash-format names like `order/paid` and `subscription/created`. The **Events list** API may return internal `eventType` strings (for example `s_order_paid`) — these are for internal analytics and should not be used when creating webhooks.
</Note>

***

## Webhook signature verification (HMAC-SHA256)

TagadaPay signs the **exact JSON string** sent as the request body.

1. Read the raw body as a **string** (do not parse JSON before verifying).
2. Compute `HMAC-SHA256(secret, rawBody)` and hex-encode the digest.
3. Compare to the `X-TagadaPay-Signature` header value after the `sha256=` prefix.

```ts theme={null}
import crypto from 'crypto';

function verifyTagadaWebhook(rawBody: string, secret: string, signatureHeader: string | null) {
  if (!signatureHeader?.startsWith('sha256=')) return false;
  const expected = signatureHeader.slice('sha256='.length);
  const hmac = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(hmac, 'hex'));
  } catch {
    return false;
  }
}
```

Every delivery also carries the following headers:

| Header                         | Value                                                                |
| ------------------------------ | -------------------------------------------------------------------- |
| `User-Agent`                   | `TagadaPay-Webhooks/1.0`                                             |
| `X-TagadaPay-Timestamp`        | Delivery time as a Unix timestamp in **milliseconds** (`Date.now()`) |
| `X-TagadaPay-Event-Type`       | The event type, e.g. `order/paid`                                    |
| `X-TagadaPay-Event-Id`         | The event UUID — same value as the top-level `id` in the JSON body   |
| `X-TagadaPay-Delivery-Attempt` | `1` through `6` — which attempt this delivery is                     |

You can use `X-TagadaPay-Timestamp` for optional replay protection and `X-TagadaPay-Delivery-Attempt` to log or alert on retries.

***

## Delivery history

Every delivery attempt is recorded — the exact payload sent, the headers, the HTTP status returned, the response body, the outcome — and stays available for audit and debugging.

Open it in the CRM under **Integrations → Webhooks → Deliveries** (per endpoint). Each row is one attempt: successful attempts appear with the response code, failed ones with the error, and a timed-out attempt shows the `No response` badge.

<Info>
  When something looks wrong — an event you expected did not fire, a `4xx` your endpoint should have accepted — the Deliveries view is the source of truth. It shows exactly what left our side and what your endpoint answered, byte for byte.
</Info>

***

## SDK methods reference

| Resource   | Method                | Description                                                                     |
| ---------- | --------------------- | ------------------------------------------------------------------------------- |
| `webhooks` | `create(params)`      | Create endpoint; params: `storeId`, `url`, `eventTypes`, optional `description` |
| `webhooks` | `list(storeId)`       | List all webhooks for a store                                                   |
| `webhooks` | `del(id)`             | Delete webhook; returns `{ success, id }`                                       |
| `events`   | `recent(limit?)`      | Most recent events                                                              |
| `events`   | `statistics(params?)` | Aggregates; optional `storeId`, `startDate`, `endDate`                          |
| `events`   | `list(params?)`       | Filtered list with `filters`, `pagination`, `sortBy`, `search`                  |
| `events`   | `retrieve(eventId)`   | Single event by id                                                              |

***

## Next steps

<CardGroup cols={2}>
  <Card title="Node SDK Quick Start" icon="terminal" href="/developer-tools/node-sdk/quick-start">
    Install the SDK, authenticate, and explore core resources
  </Card>

  <Card title="Sandbox Testing" icon="flask-vial" href="/developer-tools/node-sdk/sandbox-testing">
    Exercise payments and webhook-style flows without live processors
  </Card>

  <Card title="Merchant Quick Start" icon="rocket" href="/developer-tools/node-sdk/merchant-quickstart">
    End-to-end store, funnel, and checkout setup
  </Card>

  <Card title="Subscriptions" icon="repeat" href="/developer-tools/node-sdk/subscriptions">
    Recurring billing and subscription lifecycle
  </Card>
</CardGroup>
