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

# Webhooks

> Receive real-time notifications when events occur in your Paygentic account

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:

<Steps>
  <Step title="Enable Webhooks">
    Navigate to the [Developer > Webhooks](https://platform.paygentic.io/merchant/developer/webhooks) section in your Paygentic dashboard and toggle webhooks on.
  </Step>

  <Step title="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
  </Step>

  <Step title="Test Your Endpoint">
    <Tip>
      Use tools like [Hookdeck](https://hookdeck.com) or [ngrok](https://ngrok.com) to test webhooks during development without deploying to production.
    </Tip>
  </Step>

  <Step title="Verify and Process Events">
    <CodeGroup>
      ```javascript Node.js theme={null}
      import express from 'express';
      import { Webhook } from 'svix';

      const app = express();
      const webhookSecret = process.env.PAYGENTIC_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;
              case 'subscription.activated.v0':
                await handleSubscriptionActivated(payload.data);
                break;
              case 'payment.completed.v0':
                await handlePaymentCompleted(payload.data);
                break;
              case 'payment.failed.v0':
                await handlePaymentFailed(payload.data);
                break;
              case 'payment.expired.v0':
                await handlePaymentExpired(payload.data);
                break;
              case 'source_event.pending.v0':
                await handleSourceEventPending(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');
          }
        }
      );
      ```

      ```python Python theme={null}
      from flask import Flask, request
      from svix.webhooks import Webhook
      import os

      app = Flask(__name__)
      webhook_secret = os.environ.get('PAYGENTIC_WEBHOOK_SECRET')  # Your signing secret from webhook portal

      @app.route('/webhooks/paygentic', methods=['POST'])
      def handle_webhook():
          headers = {
              'svix-id': request.headers.get('svix-id'),
              'svix-timestamp': request.headers.get('svix-timestamp'),
              'svix-signature': request.headers.get('svix-signature')
          }
          
          try:
              wh = Webhook(webhook_secret)
              payload = wh.verify(request.data, headers)
              
              # Process the webhook
              print(f"Webhook verified: {payload}")
              
              # Handle different event types
              event_type = payload.get('eventType')
              if event_type == 'customer.created.v0':
                  handle_customer_created(payload['data'])
              elif event_type == 'subscription.created.v0':
                  handle_subscription_created(payload['data'])
              elif event_type == 'subscription.activated.v0':
                  handle_subscription_activated(payload['data'])
              elif event_type == 'payment.completed.v0':
                  handle_payment_completed(payload['data'])
              elif event_type == 'payment.failed.v0':
                  handle_payment_failed(payload['data'])
              elif event_type == 'payment.expired.v0':
                  handle_payment_expired(payload['data'])
              elif event_type == 'source_event.pending.v0':
                  handle_source_event_pending(payload['data'])
              # Add more conditions as needed
              
              return 'OK', 200
          except Exception as e:
              print(f"Webhook verification failed: {e}")
              return 'Webhook verification failed', 400
      ```
    </CodeGroup>
  </Step>
</Steps>

## Setting up webhooks

### Enabling webhooks

1. **Access the Dashboard**: Log in to your Paygentic account and navigate to [Developer > Webhooks](https://platform.paygentic.io/merchant/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

<Warning>
  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.
</Warning>

### 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 and verification

Webhook security is critical to ensure that the events you receive are legitimate and have not 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

<Warning>
  Always verify webhook signatures in production. This prevents attackers from sending fake events to your endpoint.
</Warning>

The recommended approach is to use Svix's verification library, which we recommend for easy and secure webhook verification:

<CodeGroup>
  ```bash npm theme={null}
  npm install svix
  ```

  ```bash pip theme={null}
  pip install svix
  ```

  ```bash go theme={null}
  go get github.com/svix/svix-webhooks/go
  ```

  ```bash gem theme={null}
  gem install svix
  ```
</CodeGroup>

### Verification examples

<CodeGroup>
  ```javascript Node.js theme={null}
  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' }));
  ```

  ```python Python theme={null}
  from svix.webhooks import Webhook

  def verify_webhook(body, headers, secret):
      wh = Webhook(secret)
      
      try:
          # Verify the webhook - throws on error
          payload = wh.verify(body, headers)
          return {"verified": True, "payload": payload}
      except Exception as e:
          return {"verified": False, "error": str(e)}

  # Important: Use the raw request body
  # In Flask: request.data
  # In Django: request.body
  # In FastAPI: await request.body()
  ```
</CodeGroup>

### 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

```javascript theme={null}
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 (do not 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:

```javascript theme={null}
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

<Warning>
  Disable CSRF protection for webhook endpoints. Webhooks are verified using signatures, not CSRF tokens.
</Warning>

<CodeGroup>
  ```javascript Express.js theme={null}
  // Disable CSRF for webhook route
  app.use('/webhooks', (req, res, next) => {
    req.csrfToken = () => ''; // Disable CSRF
    next();
  });
  ```

  ```python Django theme={null}
  from django.views.decorators.csrf import csrf_exempt

  @csrf_exempt
  def webhook_handler(request):
      # Handle webhook
      pass
  ```
</CodeGroup>

## Retry policy and failure handling

### Automatic retry schedule

If your endpoint fails to respond successfully, we will retry with exponential backoff:

| Attempt | Delay After Previous |
| ------- | -------------------- |
| 1       | Immediately          |
| 2       | 5 seconds            |
| 3       | 5 minutes            |
| 4       | 30 minutes           |
| 5       | 2 hours              |
| 6       | 5 hours              |
| 7       | 10 hours             |
| 8       | 10 hours             |

### Failure scenarios

A webhook delivery is considered failed if:

* Your endpoint returns a non-2xx status code
* Your endpoint does not 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:

<AccordionGroup>
  <Accordion title="Customer Events">
    <AccordionGroup>
      <Accordion title="customer.created.v0">
        Triggered when a new customer is successfully created.

        ```json theme={null}
        {
          "eventType": "customer.created.v0",
          "eventId": "evt_Wqb1k73rXprtTm7Qdlr38G",
          "timestamp": "2024-01-15T10:30:00.000Z",
          "data": {
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "createdAt": "2024-01-15T10:30:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="customer.creation_failed.v0">
        Triggered when customer creation fails.

        ```json theme={null}
        {
          "eventType": "customer.creation_failed.v0",
          "eventId": "evt_Xrc2l84sYqstUn8Reis49H",
          "timestamp": "2024-01-15T10:31:00.000Z",
          "data": {
            "merchantId": "org_456def",
            "error": {
              "code": "validation_failed",
              "message": "Required field 'email' is missing"
            },
            "attemptedAt": "2024-01-15T10:31:00.000Z"
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Subscription Events">
    <AccordionGroup>
      <Accordion title="subscription.created.v0">
        Triggered when a new subscription is created.

        ```json theme={null}
        {
          "eventType": "subscription.created.v0",
          "eventId": "evt_Zsd3m95tZrtuVo9Sfjt51I",
          "timestamp": "2024-01-15T11:00:00.000Z",
          "data": {
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "planId": "plan_456def",
            "planName": "Professional Plan",
            "merchantId": "org_456def",
            "status": "active",
            "startedAt": "2024-01-15T11:00:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="subscription.activated.v0">
        Triggered when a subscription becomes active and ready for use.

        This event fires when a subscription transitions to the `active` state, which happens either:

        * **Immediately upon creation** for subscriptions without upfront costs
        * **After successful payment** of upfront costs for subscriptions requiring wallet prefunding

        **Key Distinction:** While `subscription.created.v0` fires when a subscription is first created (which may be in `pending_payment` state), `subscription.activated.v0` fires when the subscription is ready for use. Use this event to provision access or activate features for customers.

        ```json theme={null}
        {
          "eventType": "subscription.activated.v0",
          "eventId": "evt_Act1v8d9Bvz0bu5Ylq17O",
          "timestamp": "2024-01-15T11:00:00.000Z",
          "data": {
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "planId": "plan_456def",
            "planName": "Professional Plan",
            "activatedAt": "2024-01-15T11:00:00.000Z",
            "paymentMethod": "wallet_prefund"
          }
        }
        ```

        <Tip>
          **Best Practice:** Listen to `subscription.activated.v0` for production-readiness signals and service provisioning, rather than `subscription.created.v0`, to ensure the subscription has been fully funded and is ready for use.
        </Tip>
      </Accordion>

      <Accordion title="subscription.cancelled.v0">
        Triggered when a subscription is cancelled.

        ```json theme={null}
        {
          "eventType": "subscription.cancelled.v0",
          "eventId": "evt_Ate4n06uAsuVwp0Tgku62J",
          "timestamp": "2024-01-15T12:00:00.000Z",
          "data": {
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "cancelledAt": "2024-01-15T12:00:00.000Z",
            "reason": "customer_request"
          }
        }
        ```
      </Accordion>

      <Accordion title="subscription.updated.v0">
        Triggered when a subscription is updated (e.g., plan change, quantity adjustment).

        ```json theme={null}
        {
          "eventType": "subscription.updated.v0",
          "eventId": "evt_Buf5o17vBtvWxq1Uhlv73K",
          "timestamp": "2024-01-15T13:00:00.000Z",
          "data": {
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "updates": {
              "status": "paused",
              "planId": "plan_789ghi"
            },
            "updatedAt": "2024-01-15T13:00:00.000Z"
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Payment Events">
    <AccordionGroup>
      <Accordion title="payment.completed.v0">
        Triggered when a payment is successfully completed.

        ```json theme={null}
        {
          "eventType": "payment.completed.v0",
          "eventId": "evt_Xrb2l84sYqsuUn8Rem5s49H",
          "timestamp": "2026-03-03T12:00:00.000Z",
          "data": {
            "paymentId": "pay_a1b2c3d4e5f6g7h8i9j0",
            "merchantId": "org_f7g8h9i0j1k2l3m4",
            "customerId": "cus_p1q2r3s4t5u6v7w8",
            "amount": "49.99",
            "currency": "USD",
            "reference": "ORD-12345",
            "completedAt": "2026-03-03T12:00:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="payment.failed.v0">
        Triggered when a payment attempt fails. Includes error details from the payment processor.

        ```json theme={null}
        {
          "eventType": "payment.failed.v0",
          "eventId": "evt_Abc2k93rXprtTm8Qdlr49H",
          "timestamp": "2026-03-03T12:00:00.000Z",
          "data": {
            "paymentId": "pay_a1b2c3d4e5f6g7h8i9j0",
            "merchantId": "org_f7g8h9i0j1k2l3m4",
            "customerId": "cus_p1q2r3s4t5u6v7w8",
            "amount": "49.99",
            "currency": "USD",
            "reference": "ORD-12345",
            "error": {
              "code": "card_declined",
              "message": "Your card was declined"
            },
            "failedAt": "2026-03-03T12:00:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="payment.expired.v0">
        Triggered when a payment session expires before the customer completes it.

        ```json theme={null}
        {
          "eventType": "payment.expired.v0",
          "eventId": "evt_Def3m84tZrstVn9Sem6t50I",
          "timestamp": "2026-03-03T12:00:00.000Z",
          "data": {
            "paymentId": "pay_a1b2c3d4e5f6g7h8i9j0",
            "merchantId": "org_f7g8h9i0j1k2l3m4",
            "amount": "49.99",
            "currency": "USD",
            "reference": "ORD-12345",
            "expiredAt": "2026-03-03T12:00:00.000Z"
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Invoice Events">
    <AccordionGroup>
      <Accordion title="invoice.issued.v0">
        Triggered when an invoice is issued and a payment session is opened.

        ```json theme={null}
        {
          "eventType": "invoice.issued.v0",
          "eventId": "evt_Zrf5n95uYqstWp9Sgn7v60J",
          "timestamp": "2026-03-03T12:00:00.000Z",
          "data": {
            "invoiceId": "inv_a1b2c3d4e5f6g7h8",
            "subscriptionId": "sub_z9a0b1c2d3e4f5g6",
            "customerId": "cus_p1q2r3s4t5u6v7w8",
            "merchantId": "org_f7g8h9i0j1k2l3m4",
            "status": "ISSUED",
            "invoiceNumber": "INV-2026-000001",
            "periodStart": "2026-03-01T00:00:00.000Z",
            "periodEnd": "2026-04-01T00:00:00.000Z",
            "subtotal": "49.99",
            "totalTax": "0.00",
            "grandTotal": "49.99",
            "issuedAt": "2026-03-03T12:00:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="invoice.paid.v0">
        Triggered when an invoice transitions to `PAID` — covers subscription renewals, grant purchases (the `grantId` is included in the payload), out-of-band payments, and zero-amount invoices.

        ```json theme={null}
        {
          "eventType": "invoice.paid.v0",
          "eventId": "evt_Vsk6q06vZrtsXq0Tho8w71K",
          "timestamp": "2026-03-03T12:05:00.000Z",
          "data": {
            "invoiceId": "inv_a1b2c3d4e5f6g7h8",
            "subscriptionId": "sub_z9a0b1c2d3e4f5g6",
            "customerId": "cus_p1q2r3s4t5u6v7w8",
            "merchantId": "org_f7g8h9i0j1k2l3m4",
            "status": "PAID",
            "invoiceNumber": "INV-2026-000001",
            "periodStart": "2026-03-01T00:00:00.000Z",
            "periodEnd": "2026-04-01T00:00:00.000Z",
            "subtotal": "49.99",
            "totalTax": "0.00",
            "grandTotal": "49.99",
            "paidAt": "2026-03-03T12:05:00.000Z",
            "currency": "USD",
            "paymentIntentId": "pi_3OabcDEF456ghi",
            "grantId": "grt_v1w2x3y4z5"
          }
        }
        ```

        `grantId` is populated only when the invoice was created via `POST /v1/entitlements/{id}/grants/purchase`; otherwise it is omitted. `paymentIntentId` is present for Stripe-routed payments and omitted for out-of-band and zero-amount invoices.

        <Note>
          `grandTotal` is the authoritative amount paid. `subtotal` and `totalTax` are each rounded to 2 decimal places independently, so re-adding them can land a cent off `grandTotal` — use `grandTotal` directly rather than re-summing the parts.
        </Note>

        <Note>
          This event is delivered **at least once**. On rare retries you may receive the same `invoice.paid.v0` more than once for a given invoice — dedupe on `invoiceId` (or the Svix message idempotency key) before acting on it.
        </Note>
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Source Events">
    <AccordionGroup>
      <Accordion title="source.activated.v0">
        Triggered when a usage source is successfully activated and connected.

        ```json theme={null}
        {
          "eventType": "source.activated.v0",
          "eventId": "evt_Cuf6p28wCuwXyr2Vim84L",
          "timestamp": "2024-01-15T14:00:00.000Z",
          "data": {
            "sourceId": "src_abc123",
            "sourceType": "stripe_revenue",
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "provider": "stripe",
            "activatedAt": "2024-01-15T14:00:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="source.activation_failed.v0">
        Triggered when a usage source fails to activate due to configuration or authentication issues.

        ```json theme={null}
        {
          "eventType": "source.activation_failed.v0",
          "eventId": "evt_Dvg7q39xDvxYzs3Wjo95M",
          "timestamp": "2024-01-15T14:05:00.000Z",
          "data": {
            "sourceId": "src_abc123",
            "sourceType": "stripe_revenue",
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "error": {
              "code": "authentication_failed",
              "message": "Invalid API key provided"
            },
            "attemptedAt": "2024-01-15T14:05:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="source.disconnected.v0">
        Triggered when a usage source is disconnected or deactivated.

        ```json theme={null}
        {
          "eventType": "source.disconnected.v0",
          "eventId": "evt_Ewh8r40yEwyZat4Xkp06N",
          "timestamp": "2024-01-15T15:00:00.000Z",
          "data": {
            "sourceId": "src_abc123",
            "sourceType": "stripe_revenue",
            "subscriptionId": "sub_789ghi",
            "customerId": "cust_123abc",
            "merchantId": "org_456def",
            "reason": "user_initiated",
            "disconnectedAt": "2024-01-15T15:00:00.000Z"
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>

  <Accordion title="Source Event Processing">
    <AccordionGroup>
      <Accordion title="source_event.pending.v0">
        Triggered when a source event enters pending state and requires manual processing.

        ```json theme={null}
        {
          "eventType": "source_event.pending.v0",
          "eventId": "evt_SrcEvt1k73rXprtTm7Qdlr38G",
          "timestamp": "2024-01-15T10:30:00.000Z",
          "data": {
            "sourceEventId": "sev_123abc",
            "sourceId": "src_456def",
            "sourceType": "stripe_revenue",
            "planId": "plan_789ghi",
            "subscriptionId": "sub_abc123",
            "merchantId": "org_def456",
            "externalEventId": "inv_stripe_xyz789",
            "status": "pending",
            "createdAt": "2024-01-15T10:30:00.000Z",
            "updatedAt": "2024-01-15T10:30:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="source_event.processed.v0">
        Triggered when a source event is successfully processed into usage events.

        ```json theme={null}
        {
          "eventType": "source_event.processed.v0",
          "eventId": "evt_SrcEvt2k73rXprtTm7Qdlr38G",
          "timestamp": "2024-01-15T10:35:00.000Z",
          "data": {
            "sourceEventId": "sev_123abc",
            "sourceId": "src_456def",
            "sourceType": "stripe_revenue",
            "planId": "plan_789ghi",
            "subscriptionId": "sub_abc123",
            "merchantId": "org_def456",
            "externalEventId": "inv_stripe_xyz789",
            "status": "processed",
            "previousStatus": "pending",
            "usageEventIds": ["ue_111aaa", "ue_222bbb"],
            "processedBy": "user_xyz123",
            "processedAt": "2024-01-15T10:35:00.000Z",
            "createdAt": "2024-01-15T10:30:00.000Z",
            "updatedAt": "2024-01-15T10:35:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="source_event.failed.v0">
        Triggered when a source event fails to process due to errors.

        ```json theme={null}
        {
          "eventType": "source_event.failed.v0",
          "eventId": "evt_SrcEvt3k73rXprtTm7Qdlr38G",
          "timestamp": "2024-01-15T10:32:00.000Z",
          "data": {
            "sourceEventId": "sev_456def",
            "sourceId": "src_789ghi",
            "sourceType": "stripe_revenue",
            "planId": "plan_abc123",
            "subscriptionId": "sub_def456",
            "merchantId": "org_ghi789",
            "externalEventId": "inv_stripe_abc456",
            "status": "failed",
            "previousStatus": "pending",
            "errorMessage": "Unable to transform event data: missing required field 'amount'",
            "createdAt": "2024-01-15T10:30:00.000Z",
            "updatedAt": "2024-01-15T10:32:00.000Z"
          }
        }
        ```
      </Accordion>

      <Accordion title="source_event.rejected.v0">
        Triggered when a source event is manually rejected by a user.

        ```json theme={null}
        {
          "eventType": "source_event.rejected.v0",
          "eventId": "evt_SrcEvt4k73rXprtTm7Qdlr38G",
          "timestamp": "2024-01-15T10:40:00.000Z",
          "data": {
            "sourceEventId": "sev_789ghi",
            "sourceId": "src_abc123",
            "sourceType": "stripe_revenue",
            "planId": "plan_def456",
            "subscriptionId": "sub_ghi789",
            "merchantId": "org_jkl012",
            "externalEventId": "inv_stripe_def789",
            "status": "rejected",
            "previousStatus": "pending",
            "rejectionReason": "Duplicate invoice detected",
            "processedBy": "user_abc456",
            "processedAt": "2024-01-15T10:40:00.000Z",
            "createdAt": "2024-01-15T10:30:00.000Z",
            "updatedAt": "2024-01-15T10:40:00.000Z"
          }
        }
        ```
      </Accordion>
    </AccordionGroup>
  </Accordion>
</AccordionGroup>

### Common event fields

All webhook events include these standard fields:

| Field       | Type   | Description                                     |
| ----------- | ------ | ----------------------------------------------- |
| `eventType` | string | The type of event (e.g., `customer.created.v0`) |
| `eventId`   | string | Unique identifier for this specific event       |
| `timestamp` | string | ISO 8601 timestamp when the event occurred      |
| `data`      | object | Event-specific data payload                     |

## Code examples

### Complete webhook handler

Here's a production-ready webhook handler example:

<CodeGroup>
  ```javascript Node.js theme={null}
  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.activated.v0':
        await handleSubscriptionActivated(data);
        break;

      case 'subscription.cancelled.v0':
        await handleSubscriptionCancelled(data);
        break;

      case 'subscription.updated.v0':
        await handleSubscriptionUpdated(data);
        break;

      case 'payment.completed.v0':
        await handlePaymentCompleted(data);
        break;

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

      case 'payment.expired.v0':
        await handlePaymentExpired(data);
        break;

      case 'source_event.pending.v0':
        await handleSourceEventPending(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
  }

  async function handleSubscriptionActivated(data) {
    console.log('Subscription activated:', data.subscriptionId);
    // Provision access, activate features, etc.
    // Your business logic here
  }

  async function handleSourceEventPending(data) {
    console.log('Source event requires manual processing:', data.sourceEventId);
    // Your business logic here
  }

  // ... implement other handlers
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify
  import os
  import json
  import redis
  from svix.webhooks import Webhook
  from threading import Thread

  app = Flask(__name__)
  redis_client = redis.from_url(os.environ.get('REDIS_URL'))
  webhook_secret = os.environ.get('PAYGENTIC_WEBHOOK_SECRET')

  @app.route('/webhooks/paygentic', methods=['POST'])
  def handle_webhook():
      svix_id = request.headers.get('svix-id')
      svix_timestamp = request.headers.get('svix-timestamp')
      svix_signature = request.headers.get('svix-signature')
      
      # Check required headers
      if not all([svix_id, svix_timestamp, svix_signature]):
          return 'Missing required headers', 400
      
      try:
          # Verify webhook signature
          wh = Webhook(webhook_secret)
          payload = wh.verify(request.data, {
              'svix-id': svix_id,
              'svix-timestamp': svix_timestamp,
              'svix-signature': svix_signature
          })
          
          # Check for duplicate processing (idempotency)
          if redis_client.get(f"webhook:{svix_id}"):
              print(f"Webhook {svix_id} already processed")
              return 'Already processed', 200
          
          # Mark as processed (with 24 hour expiry)
          redis_client.setex(f"webhook:{svix_id}", 86400, 'processed')
          
          # Process asynchronously
          thread = Thread(target=process_webhook_event, args=(payload,))
          thread.daemon = True
          thread.start()
          
          # Respond immediately
          return 'OK', 200
          
      except Exception as e:
          print(f"Webhook verification failed: {e}")
          return 'Invalid webhook', 400

  def process_webhook_event(payload):
      event_type = payload.get('eventType')
      data = payload.get('data')
      
      print(f"Processing event: {event_type}")
      
      try:
          if event_type == 'customer.created.v0':
              handle_customer_created(data)
          elif event_type == 'customer.creation_failed.v0':
              handle_customer_creation_failed(data)
          elif event_type == 'subscription.created.v0':
              handle_subscription_created(data)
          elif event_type == 'subscription.activated.v0':
              handle_subscription_activated(data)
          elif event_type == 'subscription.cancelled.v0':
              handle_subscription_cancelled(data)
          elif event_type == 'payment.completed.v0':
              handle_payment_completed(data)
          elif event_type == 'payment.failed.v0':
              handle_payment_failed(data)
          elif event_type == 'payment.expired.v0':
              handle_payment_expired(data)
          elif event_type == 'source_event.pending.v0':
              handle_source_event_pending(data)
          else:
              print(f"Unhandled event type: {event_type}")
      except Exception as e:
          print(f"Error processing webhook: {e}")
          # Send to error tracking service
          # Consider implementing a retry queue

  # Event handlers
  def handle_customer_created(data):
      print(f"New customer created: {data.get('customerId')}")
      # Your business logic here

  def handle_subscription_created(data):
      print(f"New subscription created: {data.get('subscriptionId')}")
      # Your business logic here

  def handle_subscription_activated(data):
      print(f"Subscription activated: {data.get('subscriptionId')}")
      # Provision access, activate features, etc.
      # Your business logic here

  def handle_source_event_pending(data):
      print(f"Source event requires manual processing: {data.get('sourceEventId')}")
      # Your business logic here

  # ... implement other handlers

  if __name__ == '__main__':
      app.run(debug=False)
  ```
</CodeGroup>

### Testing webhooks locally

For local development, use a tunneling service to expose your local server:

```bash theme={null}
# 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

* [Webhook Management Portal](https://platform.paygentic.io/merchant/developer/webhooks) - Manage your webhook endpoints and view logs
* [Paygentic API Documentation](/platform/developers/edge-api) - Complete API reference

## 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
