Skip to main content

Overview

Protocol: HTTPS POST callbacks Pattern: Event-driven (Server → Client) Direction: Unidirectional (Server calls your endpoint) When to Use Webhooks:
  • ✅ Real-time event notifications
  • ✅ Order status updates
  • ✅ Payment confirmations
  • ✅ Pet adoption notifications
  • ✅ Inventory alerts
When NOT to Use Webhooks:
  • ❌ You need to request data (use REST instead)
  • ❌ Your endpoint is not publicly accessible (use polling instead)

How Webhooks Work

Event Occurs                    Your Server
    │                              │
    ▼                              │
Petstore API                      │
    │                              │
    ├────── HTTP POST ────────────>│
    │      (webhook payload)       │
    │                              │
    │                       ┌──────┴──────┐
    │                       │ Process     │
    │                       │ Event       │
    │                       └──────┬──────┘
    │                              │
    │<───── HTTP 200 OK ───────────┤
    │      (acknowledgment)         │
Key Characteristics:
  • Server initiates the request
  • Push-based (no polling needed)
  • Event-driven architecture
  • HMAC signature verification
  • Automatic retry on failure

Webhook Events

Pet Events

Pet Adopted

{
  "id": "019b4132-70aa-764f-b315-e2803d882a24",
  "eventType": "pet.adopted",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "pet": {
      "id": "pet_fYrZzCW9E1WIOyGw",
      "name": "Buddy",
      "species": "DOG",
      "status": "ADOPTED"
    },
    "adopter": {
      "id": "019b4137-6d8e-5c2b-e9f4-a3b5c8d7e2f1",
      "name": "John Doe",
      "email": "[email protected]"
    },
    "adoptionDate": "2025-01-06"
  }
}

Pet Status Changed

{
  "id": "019b4127-54d5-76d9-b626-0d4c7bfce5b6",
  "eventType": "pet.status_changed",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "petId": "pet_fYrZzCW9E1WIOyGw",
    "oldStatus": "AVAILABLE",
    "newStatus": "PENDING",
    "reason": "Adoption application received"
  }
}

Order Events

Order Created

{
  "id": "019b4135-89c2-724a-a825-c1d8a0e9f4c3",
  "eventType": "order.created",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "order": {
      "id": "019b4141-e5f3-a1b7-c4d8-9f6e2a5b8c1d",
      "status": "PENDING",
      "items": [
        {
          "productId": "019b4145-a6c2-b8d3-e7f4-5a9b1c2d3e4f",
          "quantity": 2,
          "price": 29.99
        }
      ],
      "total": 59.98,
      "currency": "USD"
    },
    "customer": {
      "id": "019b4137-6d8e-5c2b-e9f4-a3b5c8d7e2f1",
      "email": "[email protected]"
    }
  }
}

Order Status Changed

{
  "id": "019b4138-3d5f-8b1c-d9e7-f0a2b4c8e6d1",
  "eventType": "order.status_changed",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "orderId": "order_abc123",
    "oldStatus": "PENDING",
    "newStatus": "PROCESSING",
    "timestamp": "2025-01-06T12:00:00Z"
  }
}

Payment Events

Payment Succeeded

{
  "id": "019b4139-a7e4-6c3f-b2a1-e9f8d5c7b4a9",
  "eventType": "payment.succeeded",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "payment": {
      "id": "019b4142-c4d6-f8a9-b2e3-7d1c5f9a8e4b",
      "orderId": "order_abc123",
      "amount": 150.00,
      "currency": "USD",
      "status": "SUCCEEDED"
    },
    "paymentMethod": {
      "type": "CARD",
      "last4": "4242",
      "brand": "Visa"
    }
  }
}

Payment Failed

{
  "id": "019b4140-b2c8-9d4e-a3f6-c8e7b5d9a2f1",
  "eventType": "payment.failed",
  "timestamp": "2025-01-06T12:00:00Z",
  "data": {
    "payment": {
      "id": "019b4143-d8e4-a7f2-c5b9-6e4a8d3c7f1b",
      "orderId": "order_def456",
      "amount": 75.00,
      "currency": "USD",
      "status": "FAILED"
    },
    "error": {
      "code": "INSUFFICIENT_FUNDS",
      "message": "The card has insufficient funds."
    }
  }
}

Setting Up Webhooks

1. Register Your Webhook URL

curl -X POST https://api.petstoreapi.com/v1/webhooks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "url": "https://your-app.com/webhooks/petstore",
    "events": [
      "pet.adopted",
      "pet.status_changed",
      "order.created",
      "order.status_changed",
      "payment.succeeded",
      "payment.failed"
    ],
    "secret": "your_webhook_secret_here"
  }'
Response:
{
  "id": "019b4144-f1b7-e5c8-d4a6-3c7f9b2e8d5a",
  "url": "https://your-app.com/webhooks/petstore",
  "events": ["pet.adopted", "pet.status_changed", "..."],
  "secret": "whsec_xxx...",
  "status": "active",
  "createdAt": "2025-01-06T12:00:00Z"
}

2. Create Webhook Endpoint

const express = require('express');
const crypto = require('crypto');
const app = express();

// Your webhook secret (from registration)
const WEBHOOK_SECRET = 'whsec_xxx...';

// Raw body parser for signature verification
app.use('/webhooks/petstore', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const payload = req.body;

  // Verify signature
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  if (signature !== `sha256=${expectedSignature}`) {
    console.error('Invalid signature');
    return res.status(401).send('Invalid signature');
  }

  // Parse JSON after verification
  const event = JSON.parse(payload);

  console.log(`Received event: ${event.eventType}`);

  // Handle different event types
  switch (event.eventType) {
    case 'pet.adopted':
      handlePetAdopted(event.data);
      break;

    case 'order.created':
      handleOrderCreated(event.data);
      break;

    case 'payment.succeeded':
      handlePaymentSucceeded(event.data);
      break;

    case 'payment.failed':
      handlePaymentFailed(event.data);
      break;

    default:
      console.log(`Unhandled event type: ${event.eventType}`);
  }

  // Always return 200 OK
  res.sendStatus(200);
});

function handlePetAdopted(data) {
  const { pet, adopter } = data;
  console.log(`Pet ${pet.name} was adopted by ${adopter.name}`);
  // Update your database, send notification, etc.
}

function handleOrderCreated(data) {
  const { order, customer } = data;
  console.log(`New order ${order.id} from ${customer.email}`);
  // Process order
}

function handlePaymentSucceeded(data) {
  const { payment } = data;
  console.log(`Payment ${payment.id} succeeded`);
  // Update order status, send confirmation
}

function handlePaymentFailed(data) {
  const { payment, error } = data;
  console.log(`Payment ${payment.id} failed: ${error.message}`);
  // Notify customer, retry payment, etc.
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

3. Test Your Webhook

# Send test webhook
curl -X POST https://api.petstoreapi.com/v1/webhooks/wh_abc123/test \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "eventType": "pet.adopted",
    "data": {
      "pet": {
        "id": "019b4132-70aa-764f-b315-e2803d882a24",
        "name": "Test Pet"
      }
    }
  }'

Signature Verification

Why Verify Signatures?

  • ✅ Ensure webhook is from Petstore API
  • ✅ Detect tampering
  • ✅ Prevent fraud

Verification Process

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = b'whsec_xxx...'

@app.route('/webhooks/petstore', methods=['POST'])
def webhook():
    # Get signature from header
    signature = request.headers.get('X-Petstore-Signature')
    payload = request.data

    # Verify signature
    expected_signature = hmac.new(
        WEBHOOK_SECRET,
        payload,
        hashlib.sha256
    ).hexdigest()

    if signature != f'sha256={expected_signature}':
        return jsonify({'error': 'Invalid signature'}), 401

    # Process event
    event = request.get_json()
    handle_event(event)

    return jsonify({'status': 'ok'}), 200

Best Practices

1. Always Return 200 OK Quickly

app.post('/webhooks/petstore', (req, res) => {
  // Send response immediately
  res.sendStatus(200);

  // Process asynchronously
  processWebhookAsync(req.body);
});

2. Handle Duplicate Events

const processedEvents = new Set();

function processEvent(event) {
  // Check if already processed
  if (processedEvents.has(event.id)) {
    console.log(`Event ${event.id} already processed, skipping`);
    return;
  }

  // Mark as processed
  processedEvents.add(event.id);

  // Process event
  handleEvent(event);

  // Clean up old IDs (optional)
  if (processedEvents.size > 10000) {
    const oldestId = processedEvents.values().next().value;
    processedEvents.delete(oldestId);
  }
}

3. Retry Logic

The API will retry webhooks that fail:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
51 hour
After 5 failed attempts, the webhook is disabled.

4. Idempotency

Make your webhook handlers idempotent:
async function handlePaymentSucceeded(data) {
  const { payment } = data;

  // Check if already processed
  const existing = await db.payments.findUnique(payment.id);
  if (existing && existing.status === 'SUCCEEDED') {
    return; // Already processed
  }

  // Process payment
  await db.payments.update(payment.id, {
    status: 'SUCCEEDED',
    processedAt: new Date()
  });

  // Send notification (check if already sent)
  if (!existing?.notificationSent) {
    await sendNotification(payment);
    await db.payments.update(payment.id, {
      notificationSent: true
    });
  }
}

Security

1. Use HTTPS

Always use HTTPS for your webhook endpoint.

2. Verify Signatures

Never process webhook events without signature verification.

3. Validate Payloads

function validatePetAdoptedEvent(data) {
  if (!data.pet?.id) throw new Error('Missing pet.id');
  if (!data.adopter?.id) throw new Error('Missing adopter.id');
  if (!data.adoptionDate) throw new Error('Missing adoptionDate');
  // Add more validations...
}

4. Rate Limiting

const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 60000, // 1 minute
  max: 100, // 100 requests per minute
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/webhooks', webhookLimiter);

Managing Webhooks

List Webhooks

curl https://api.petstoreapi.com/v1/webhooks \
  -H "Authorization: Bearer YOUR_TOKEN"

Get Webhook Details

curl https://api.petstoreapi.com/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN"

Update Webhook

curl -X PATCH https://api.petstoreapi.com/v1/webhooks/wh_abc123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "events": [
      "pet.adopted",
      "order.created",
      "payment.succeeded"
    ]
  }'

Delete Webhook

curl -X DELETE https://api.petstoreapi.com/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN"

Troubleshooting

Not Receiving Webhooks

  1. Check webhook status is active
  2. Verify endpoint URL is publicly accessible
  3. Confirm HTTPS certificate is valid
  4. Check server logs for connection attempts
  5. Test with webhook test endpoint

Signature Verification Fails

  • Ensure you’re using the correct secret
  • Verify you’re comparing the entire signature
  • Check for encoding issues

High Failure Rate

  • Ensure endpoint responds quickly (< 5 seconds)
  • Always return 200 OK (even if processing fails later)
  • Implement retry logic in your application
  • Monitor endpoint uptime

Comparison with Alternatives

FeatureWebhooksPollingWebSocket
LatencyVery LowHighVery Low
Server LoadLowHighMedium
ComplexityLowLowHigh
Public EndpointRequiredNot RequiredRequired
Real-time

Interactive Documentation