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?
Real-time Updates
Get notified instantly when events happen, no polling required
Ground Truth Events
Know when users actually visit venues (QR verification)
Booking Lifecycle
Track booking from creation to completion
Analytics
Build custom analytics on user behavior
Setting Up Webhooks
Via Dashboard
- Go to privetag.com/developers/webhooks
- Click Add Endpoint
- Enter your webhook URL
- Select events to receive
- Copy and save your Webhook Secret
Via API Key Configuration
When creating or updating an API key, specify a webhook URL:
{
"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:
{
"id": "wh_evt_abc123",
"event": "booking_confirmed",
"created_at": "2025-12-10T14:30:15Z",
"data": {
// Event-specific data
}
}
booking_confirmed
{
"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
{
"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)
This is the Ground Truth event - it tells you when a user actually visited the venue, not just when they booked.
{
"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
{
"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
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
Always verify signatures to ensure webhooks are from PriveTag. Never process unverified webhooks.
Responding to Webhooks
Success Response
Return a 2xx status code to acknowledge receipt:
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
Return 200 immediately and process asynchronously:@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
Use id field for idempotency: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')
Webhook URLs must use HTTPS in production. We verify SSL certificates.
Log all webhook events for debugging:import logging
logger = logging.getLogger('webhooks')
def handle_webhook():
event = request.json
logger.info(f"Received webhook: {event['event']} - {event['id']}")
# Process...
Testing Webhooks
Test Endpoint
Send test webhooks from the dashboard or via API:
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 or localtunnel to test webhooks locally:
# 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:
{
"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