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

# Webhooks

> Receive real-time event notifications

# Webhooks

Webhooks allow you to receive real-time notifications when events occur in PriveTag, such as bookings being confirmed or vouchers being used.

## Why Use Webhooks?

<CardGroup cols={2}>
  <Card title="Real-time Updates" icon="bolt">
    Get notified instantly when events happen, no polling required
  </Card>

  <Card title="Ground Truth Events" icon="check-double">
    Know when users actually visit venues (QR verification)
  </Card>

  <Card title="Booking Lifecycle" icon="arrows-rotate">
    Track booking from creation to completion
  </Card>

  <Card title="Analytics" icon="chart-line">
    Build custom analytics on user behavior
  </Card>
</CardGroup>

## Setting Up Webhooks

### Via Dashboard

1. Go to [privetag.com/developers/webhooks](https://privetag.com/developers/webhooks)
2. Click **Add Endpoint**
3. Enter your webhook URL
4. Select events to receive
5. Copy and save your **Webhook Secret**

### Via API Key Configuration

When creating or updating an API key, specify a webhook URL:

```json theme={null}
{
  "name": "Production Key",
  "webhook_url": "https://your-server.com/webhooks/privetag",
  "webhook_events": ["booking_confirmed", "voucher_delivered", "qr_verified"]
}
```

## Webhook Events

| Event               | Description                  | When Fired                           |
| ------------------- | ---------------------------- | ------------------------------------ |
| `booking_confirmed` | Booking successfully created | Immediately after `/execute_booking` |
| `voucher_delivered` | Voucher email sent to user   | Within 30 seconds of booking         |
| `qr_verified`       | User visited venue           | When QR is scanned at venue          |
| `booking_cancelled` | Booking was cancelled        | On cancellation                      |
| `booking_modified`  | Booking details changed      | On modification                      |

## Webhook Payload

All webhooks follow this structure:

```json theme={null}
{
  "id": "wh_evt_abc123",
  "event": "booking_confirmed",
  "created_at": "2025-12-10T14:30:15Z",
  "data": {
    // Event-specific data
  }
}
```

### booking\_confirmed

```json theme={null}
{
  "id": "wh_evt_abc123",
  "event": "booking_confirmed",
  "created_at": "2025-12-10T14:30:15Z",
  "data": {
    "booking_id": "bkg_h7j9k2m4",
    "activity_id": "act_abc123",
    "activity_title": "Safari World Bangkok",
    "user_email": "guest@example.com",
    "user_name": "John Doe",
    "booking_date": "2025-12-15",
    "num_guests": 2,
    "total_price": 3000,
    "currency": "THB",
    "context_log_id": "ctx_xyz789"
  }
}
```

### voucher\_delivered

```json theme={null}
{
  "id": "wh_evt_def456",
  "event": "voucher_delivered",
  "created_at": "2025-12-10T14:30:45Z",
  "data": {
    "booking_id": "bkg_h7j9k2m4",
    "voucher_code": "PVT-2025-ABC123",
    "delivered_to": "guest@example.com",
    "delivered_at": "2025-12-10T14:30:43Z"
  }
}
```

### qr\_verified (Ground Truth)

<Info>
  This is the **Ground Truth** event - it tells you when a user actually visited the venue, not just when they booked.
</Info>

```json theme={null}
{
  "id": "wh_evt_ghi789",
  "event": "qr_verified",
  "created_at": "2025-12-15T10:30:00Z",
  "data": {
    "booking_id": "bkg_h7j9k2m4",
    "activity_id": "act_abc123",
    "context_log_id": "ctx_xyz789",
    "verified_at": "2025-12-15T10:30:00Z",
    "venue_name": "Safari World Bangkok",
    "verification_method": "qr_scan"
  }
}
```

### booking\_cancelled

```json theme={null}
{
  "id": "wh_evt_jkl012",
  "event": "booking_cancelled",
  "created_at": "2025-12-12T09:15:00Z",
  "data": {
    "booking_id": "bkg_h7j9k2m4",
    "cancelled_at": "2025-12-12T09:15:00Z",
    "reason": "user_requested",
    "refund_status": "processed"
  }
}
```

## Verifying Webhooks

All webhook requests include a signature header for verification:

```
X-Privetag-Signature: sha256=abc123...
```

### Verification Example

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(payload, signature, secret):
      expected = hmac.new(
          secret.encode(),
          payload.encode(),
          hashlib.sha256
      ).hexdigest()

      return hmac.compare_digest(f"sha256={expected}", signature)

  # In your webhook handler
  @app.route('/webhooks/privetag', methods=['POST'])
  def handle_webhook():
      payload = request.get_data(as_text=True)
      signature = request.headers.get('X-Privetag-Signature')

      if not verify_webhook(payload, signature, WEBHOOK_SECRET):
          return 'Invalid signature', 401

      event = request.json
      # Process event...
      return 'OK', 200
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhook(payload, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

    return signature === `sha256=${expected}`;
  }

  // Express handler
  app.post('/webhooks/privetag', express.raw({ type: 'application/json' }), (req, res) => {
    const payload = req.body.toString();
    const signature = req.headers['x-privetag-signature'];

    if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    // Process event...
    res.sendStatus(200);
  });
  ```
</CodeGroup>

<Warning>
  **Always verify signatures** to ensure webhooks are from PriveTag. Never process unverified webhooks.
</Warning>

## Responding to Webhooks

### Success Response

Return a 2xx status code to acknowledge receipt:

```
HTTP/1.1 200 OK
```

### Retry Logic

If your endpoint returns a non-2xx status or times out, we'll retry:

| Retry | Delay      |
| ----- | ---------- |
| 1st   | 5 seconds  |
| 2nd   | 30 seconds |
| 3rd   | 2 minutes  |
| 4th   | 10 minutes |
| 5th   | 1 hour     |

After 5 failed attempts, the webhook is marked as failed.

## Best Practices

<AccordionGroup>
  <Accordion title="Respond quickly">
    Return 200 immediately and process asynchronously:

    ```python theme={null}
    @app.route('/webhooks/privetag', methods=['POST'])
    def handle_webhook():
        event = request.json

        # Queue for async processing
        queue.enqueue(process_webhook, event)

        # Return immediately
        return 'OK', 200
    ```
  </Accordion>

  <Accordion title="Handle duplicates">
    Use `id` field for idempotency:

    ```python theme={null}
    def process_webhook(event):
        event_id = event['id']

        # Check if already processed
        if redis.exists(f"webhook:{event_id}"):
            return

        # Process event
        handle_event(event)

        # Mark as processed (with TTL)
        redis.setex(f"webhook:{event_id}", 86400, '1')
    ```
  </Accordion>

  <Accordion title="Use HTTPS">
    Webhook URLs must use HTTPS in production. We verify SSL certificates.
  </Accordion>

  <Accordion title="Log everything">
    Log all webhook events for debugging:

    ```python theme={null}
    import logging

    logger = logging.getLogger('webhooks')

    def handle_webhook():
        event = request.json
        logger.info(f"Received webhook: {event['event']} - {event['id']}")
        # Process...
    ```
  </Accordion>
</AccordionGroup>

## Testing Webhooks

### Test Endpoint

Send test webhooks from the dashboard or via API:

```bash theme={null}
curl -X POST https://api.privetag.com/api/webhooks/test \
  -H "x-api-key: pk_a1b2c3..." \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "booking_confirmed",
    "webhook_url": "https://your-server.com/webhooks/privetag"
  }'
```

### Local Development

Use tools like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.github.io/www/) to test webhooks locally:

```bash theme={null}
# Start ngrok
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/privetag
```

## Webhook Logs

View webhook delivery logs in the dashboard:

* **Delivery status**: Success, failed, retrying
* **Response code**: HTTP status returned
* **Response time**: Latency of your endpoint
* **Payload**: Full request sent

## Event Filtering

Configure which events to receive:

```json theme={null}
{
  "webhook_url": "https://your-server.com/webhooks/privetag",
  "webhook_events": [
    "qr_verified"
  ]
}
```

This is useful for:

* Only receiving Ground Truth events
* Separating booking events from verification events
* Reducing webhook volume

## Next Steps

<CardGroup cols={2}>
  <Card title="API Keys" icon="key" href="/authentication/api-keys">
    Configure webhook URL in your API key
  </Card>

  <Card title="Execute Booking" icon="ticket" href="/api-reference/b2a/execute-booking">
    Book activities and receive webhook events
  </Card>
</CardGroup>
