Webhooks allow your application to receive real-time notifications when events occur in your Paygentic account. When enabled, we’ll send HTTP POST requests to your configured endpoints whenever relevant events happen, such as customer creation, subscription changes, and more.

Quickstart Guide

Get up and running with webhooks in just a few minutes:
1

Enable Webhooks

Navigate to the Developer > Webhooks section in your Paygentic dashboard and toggle webhooks on.
2

Configure Your Endpoint

Once enabled, click “Manage Webhook Endpoints” to access the webhook management portal where you can:
  • Add your webhook endpoint URL (e.g., https://your-domain.com/webhooks/paygentic)
  • Select which event types to subscribe to
  • View your webhook signing secret
3

Test Your Endpoint

Use tools like Hookdeck or ngrok to test webhooks during development without deploying to production.
4

Verify and Process Events

import express from 'express';
import { Webhook } from 'svix';

const app = express();
const webhookSecret = process.env.WEBHOOK_SECRET; // Your signing secret from webhook portal

app.post('/webhooks/paygentic', 
  express.raw({ type: 'application/json' }), // Important: Use raw body
  async (req, res) => {
    const headers = {
      'svix-id': req.headers['svix-id'],
      'svix-timestamp': req.headers['svix-timestamp'],
      'svix-signature': req.headers['svix-signature']
    };

    try {
      const wh = new Webhook(webhookSecret);
      const payload = wh.verify(req.body, headers);
      
      // Process the webhook
      console.log('Webhook verified:', payload);
      
      // Handle different event types
      switch (payload.eventType) {
        case 'customer.created.v0':
          await handleCustomerCreated(payload.data);
          break;
        case 'subscription.created.v0':
          await handleSubscriptionCreated(payload.data);
          break;
        // Add more cases as needed
      }
      
      res.status(200).send('OK');
    } catch (err) {
      console.error('Webhook verification failed:', err);
      res.status(400).send('Webhook verification failed');
    }
  }
);

Setting Up Webhooks

Enabling Webhooks

  1. Access the Dashboard: Log in to your Paygentic account and navigate to Developer > Webhooks
  2. Enable Webhooks: Toggle the “Enable Webhooks” switch to activate webhook functionality
  3. Access Management Portal: Click “Manage Webhook Endpoints” to open the webhook management portal

Configuring Endpoints

In the webhook management portal, you can:
  • Add Endpoints: Specify the URL where you want to receive webhook events
  • Select Event Types: Choose which events you want to subscribe to
  • Set Filters: Configure advanced filtering rules if needed
  • View Logs: Monitor webhook deliveries and debug any issues
  • Retrieve Signing Secret: Access your webhook signing secret for verification
Remember to keep your webhook signing secret secure and never commit it to version control. Store it in environment variables or a secure secrets management system.

Best Practices

  • Use HTTPS endpoints only (HTTP is not supported for security reasons)
  • Implement webhook processing asynchronously to respond quickly
  • Store the svix-id to handle duplicate events (idempotency)
  • Disable CSRF protection for your webhook endpoint
  • Respond with a 2xx status code within 15 seconds

Security & Verification

Webhook security is critical to ensure that the events you receive are legitimate and haven’t been tampered with. Paygentic signs all webhooks with HMAC-SHA256.

Required Headers

Every webhook request includes three important headers:
  • svix-id: Unique identifier for the webhook message (use for idempotency)
  • svix-timestamp: Unix timestamp when the webhook was sent
  • svix-signature: Base64 encoded signature(s) for verification

Signature Verification

Always verify webhook signatures in production. This prevents attackers from sending fake events to your endpoint.
The recommended approach is to use Svix’s verification library, which we recommend for easy and secure webhook verification:
npm install svix

Verification Examples

import { Webhook } from 'svix';

function verifyWebhook(body, headers, secret) {
  const wh = new Webhook(secret);
  
  try {
    // Verify the webhook - throws on error
    const payload = wh.verify(body, headers);
    return { verified: true, payload };
  } catch (err) {
    return { verified: false, error: err.message };
  }
}

// Important: Use the raw request body
// Many frameworks parse JSON automatically - you need the raw string
app.use('/webhooks', express.raw({ type: 'application/json' }));

Replay Attack Protection

The timestamp in the svix-timestamp header protects against replay attacks. The Svix library automatically rejects webhooks with timestamps more than 5 minutes old (past or future). Ensure your server’s clock is synchronized using NTP.

Handling Webhooks

Response Requirements

  • Status Code: Return a 2xx status code (200-299) to indicate successful receipt
  • Timeout: Respond within 15 seconds or the delivery will be considered failed
  • Body: The response body is ignored - a simple “OK” or empty response is fine

Processing Best Practices

app.post('/webhooks/paygentic', async (req, res) => {
  // 1. Verify the webhook (as shown above)
  const payload = verifyWebhook(req.body, headers, secret);
  
  // 2. Respond immediately
  res.status(200).send('OK');
  
  // 3. Process asynchronously (don't block the response)
  setImmediate(async () => {
    try {
      await processWebhookAsync(payload);
    } catch (error) {
      console.error('Error processing webhook:', error);
      // Log to your error tracking service
    }
  });
});

Idempotency

Use the svix-id header to ensure you only process each event once:
const processedEvents = new Set(); // In production, use Redis or a database

async function processWebhook(payload, svixId) {
  if (processedEvents.has(svixId)) {
    console.log(`Event ${svixId} already processed, skipping`);
    return;
  }
  
  // Process the event
  await handleEvent(payload);
  
  // Mark as processed
  processedEvents.add(svixId);
}

CSRF Protection

Disable CSRF protection for webhook endpoints. Webhooks are verified using signatures, not CSRF tokens.
// Disable CSRF for webhook route
app.use('/webhooks', (req, res, next) => {
  req.csrfToken = () => ''; // Disable CSRF
  next();
});

Retry Policy & Failure Handling

Automatic Retry Schedule

If your endpoint fails to respond successfully, we will retry with exponential backoff:
AttemptDelay After Previous
1Immediately
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 hours

Failure Scenarios

A webhook delivery is considered failed if:
  • Your endpoint returns a non-2xx status code
  • Your endpoint doesn’t respond within 15 seconds
  • The endpoint is unreachable (connection error)
  • Your endpoint returns a 3xx redirect (not followed)

Endpoint Disabling

If an endpoint fails continuously for 5 days, it will be automatically disabled. You’ll receive an operational webhook notification when this happens. You can re-enable the endpoint from the webhook management portal.

Manual Recovery

You can manually retry failed webhooks through the webhook management portal:
  • Individual Retry: Retry a specific failed message
  • Bulk Recovery: Replay all failed messages from a specific date
  • View Logs: Inspect delivery attempts and error messages

Event Types Reference

Paygentic currently supports the following webhook event types:

Common Event Fields

All webhook events include these standard fields:
FieldTypeDescription
eventTypestringThe type of event (e.g., customer.created.v0)
eventIdstringUnique identifier for this specific event
timestampstringISO 8601 timestamp when the event occurred
dataobjectEvent-specific data payload

Code Examples

Complete Webhook Handler

Here’s a production-ready webhook handler example:
import express from 'express';
import { Webhook } from 'svix';
import Redis from 'ioredis';

const app = express();
const redis = new Redis(process.env.REDIS_URL);
const webhookSecret = process.env.PAYGENTIC_WEBHOOK_SECRET;

// Webhook handler with all best practices
app.post('/webhooks/paygentic',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const svixId = req.headers['svix-id'];
    const svixTimestamp = req.headers['svix-timestamp'];
    const svixSignature = req.headers['svix-signature'];

    // Check required headers
    if (!svixId || !svixTimestamp || !svixSignature) {
      return res.status(400).send('Missing required headers');
    }

    try {
      // Verify webhook signature
      const wh = new Webhook(webhookSecret);
      const payload = wh.verify(req.body, {
        'svix-id': svixId,
        'svix-timestamp': svixTimestamp,
        'svix-signature': svixSignature
      });

      // Check for duplicate processing (idempotency)
      const processed = await redis.get(`webhook:${svixId}`);
      if (processed) {
        console.log(`Webhook ${svixId} already processed`);
        return res.status(200).send('Already processed');
      }

      // Mark as processed (with 24 hour expiry)
      await redis.setex(`webhook:${svixId}`, 86400, 'processed');

      // Respond immediately
      res.status(200).send('OK');

      // Process asynchronously
      setImmediate(async () => {
        try {
          await processWebhookEvent(payload);
        } catch (error) {
          console.error('Error processing webhook:', error);
          // Send to error tracking service
          // Consider implementing a retry queue
        }
      });

    } catch (err) {
      console.error('Webhook verification failed:', err);
      return res.status(400).send('Invalid webhook');
    }
  }
);

async function processWebhookEvent(payload) {
  const { eventType, data } = payload;
  
  console.log(`Processing event: ${eventType}`);
  
  switch (eventType) {
    case 'customer.created.v0':
      await handleCustomerCreated(data);
      break;
      
    case 'customer.creation_failed.v0':
      await handleCustomerCreationFailed(data);
      break;
      
    case 'subscription.created.v0':
      await handleSubscriptionCreated(data);
      break;
      
    case 'subscription.cancelled.v0':
      await handleSubscriptionCancelled(data);
      break;
      
    case 'subscription.updated.v0':
      await handleSubscriptionUpdated(data);
      break;
      
    default:
      console.log(`Unhandled event type: ${eventType}`);
  }
}

// Event handlers
async function handleCustomerCreated(data) {
  console.log('New customer created:', data.customerId);
  // Your business logic here
}

async function handleSubscriptionCreated(data) {
  console.log('New subscription created:', data.subscriptionId);
  // Your business logic here
}

// ... implement other handlers

Testing Webhooks Locally

For local development, use a tunneling service to expose your local server:
# Using ngrok
ngrok http 3000

# Using Hookdeck CLI
hookdeck listen 3000

# Your webhook URL will be something like:
# https://abc123.ngrok.io/webhooks/paygentic
Then add this URL as your webhook endpoint in the webhook management portal for testing.

Additional Resources

Need Help?

If you encounter any issues with webhooks:
  1. Check the delivery logs in the webhook management portal for error messages
  2. Verify your endpoint is returning a 2xx status code
  3. Ensure you’re using the correct signing secret
  4. Confirm your server’s clock is synchronized (for timestamp validation)
  5. Contact support if you need additional assistance