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

# Webhook Integration

> Receive HTTP POST webhook notifications for payment events (succeeded, pending_review, declined, refunded, expired, incomplete). Includes HMAC-SHA256 signature verification, full payload structure, retry policy (4 attempts), and idempotency handling

## Overview

Webhooks allow Payviox to send real-time notifications to your server when payment events occur. When a payment is processed, Payviox sends an HTTP POST request to your configured webhook URL with the payment details.

<Info>
  Webhooks are essential for keeping your application synchronized with payment statuses. Configure your webhook URL in the [Payviox Dashboard](https://dash.payviox.com) under **Settings** > **Webhooks**.
</Info>

## Webhook Configuration

### Setting Up Your Webhook URL

<Steps>
  <Step title="Configure Your Endpoint">
    Create an endpoint on your server to receive POST requests (e.g., `https://yourdomain.com/api/webhook`)
  </Step>

  <Step title="Add URL to Dashboard">
    Go to your [Payviox Dashboard](https://dash.payviox.com) and navigate to **Settings** > **Webhooks**
  </Step>

  <Step title="Enter Your Webhook URL">
    Paste your endpoint URL in the webhook URL field
  </Step>

  <Step title="Save Your Webhook Token">
    Copy your webhook token - you'll need it to verify webhook signatures
  </Step>

  <Step title="Select Webhook Events">
    Choose which events you want to receive. By default, only `succeeded` is enabled.
  </Step>
</Steps>

### Available Events

You can subscribe to the following webhook events:

| Event            | Description                                                  | Recommended                |
| ---------------- | ------------------------------------------------------------ | -------------------------- |
| `succeeded`      | Payment completed successfully                               | ✅ Always enable            |
| `pending_review` | Payment flagged for fraud review                             | ✅ If using fraud detection |
| `declined`       | Payment declined by fraud prevention                         | ✅ If using fraud detection |
| `refunded`       | Refund processed                                             | ✅ If you process refunds   |
| `expired`        | Payment session expired without completion                   | ⚪ Optional                 |
| `incomplete`     | Crypto payment underpaid — customer sent less than requested | ⚪ Optional (crypto only)   |

<Warning>
  Always verify webhook signatures to ensure requests are coming from Payviox and not from malicious actors.
</Warning>

## Webhook Payload Structure

Payviox sends webhooks with the following structure:

```json theme={null}
{
  "amount": 10000,
  "currency": "USD",
  "fees": 350,
  "metadata": {
    "integration_session_id": "sess_abc123xyz"
  },
  "type": "succeeded",
  "provider": "stripe",
  "order_id": "order_123456",
  "items": [
    {
      "name": "Premium Subscription",
      "quantity": 1,
      "price": 10000
    }
  ],
  "payment_method": "card",
  "customer": {
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+1234567890",
    "address": {
      "line1": "123 Main St",
      "city": "Paris",
      "postal_code": "75001",
      "country": "FR"
    }
  }
}
```

### Payload Fields

<ParamField path="amount" type="integer" required>
  The payment amount in the smallest currency unit (e.g., cents for USD)
</ParamField>

<ParamField path="currency" type="string" required>
  Three-letter ISO currency code (e.g., USD, EUR, GBP)
</ParamField>

<ParamField path="fees" type="integer">
  Total fees amount in the smallest currency unit (e.g., cents for USD). The customer pays `amount + fees = total`. This field is present when fees have been calculated for the transaction.
</ParamField>

<ParamField path="metadata" type="object" required>
  Custom metadata associated with the payment session

  <Expandable title="metadata properties">
    <ParamField path="integration_session_id" type="string">
      The unique session identifier from Payviox
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="type" type="string" required>
  The payment event type. Possible values:

  * `succeeded`: Payment completed successfully. You can fulfill the order.
  * `pending_review`: Payment flagged for fraud review. Do NOT ship until you receive `succeeded` or `declined`.
  * `declined`: Payment declined by fraud prevention. Customer has been automatically refunded.
  * `refunded`: A refund has been processed for this transaction.
  * `expired`: Payment session expired without completion.
  * `incomplete`: A crypto payment came in short (less than the requested amount). Opt-in event. Includes `amount_remaining` (how much is still due). Do NOT fulfill until you receive `succeeded`.
</ParamField>

<ParamField path="provider" type="string" required>
  The payment provider used (e.g., stripe, paypal, crypto)
</ParamField>

<ParamField path="order_id" type="string" required>
  Your unique order identifier
</ParamField>

<ParamField path="items" type="array" required>
  Array of items purchased

  <Expandable title="item properties">
    <ParamField path="name" type="string">
      Item name
    </ParamField>

    <ParamField path="quantity" type="integer">
      Quantity purchased
    </ParamField>

    <ParamField path="price" type="integer">
      Item price in smallest currency unit
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="payment_method" type="string">
  Specific payment method used (e.g., card, bank\_transfer)
</ParamField>

<ParamField path="customer" type="object">
  Optional. Customer information if available from the payment provider.
  This field is only present when customer data was collected during payment.

  <Expandable title="customer properties">
    <ParamField path="name" type="string">
      Optional. Customer's full name
    </ParamField>

    <ParamField path="email" type="string">
      Optional. Customer's email address
    </ParamField>

    <ParamField path="phone" type="string">
      Optional. Customer's phone number
    </ParamField>

    <ParamField path="address" type="object">
      Optional. Customer's billing address

      <Expandable title="address properties">
        <ParamField path="line1" type="string">Optional. Street address line 1</ParamField>
        <ParamField path="line2" type="string">Optional. Street address line 2</ParamField>
        <ParamField path="city" type="string">Optional. City</ParamField>
        <ParamField path="state" type="string">Optional. State/Province</ParamField>
        <ParamField path="postal_code" type="string">Optional. Postal/ZIP code</ParamField>
        <ParamField path="country" type="string">Optional. Two-letter country code (ISO 3166-1 alpha-2)</ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="amount_remaining" type="integer">
  Only present on `incomplete` events. The amount still due, in the smallest currency unit (e.g., cents for USD) — how much more the customer must send to complete the payment. The amount already received equals `amount - amount_remaining`.
</ParamField>

## Event Types

Payviox sends different webhook events based on the payment lifecycle. Here's what each event means and how to handle it:

### `succeeded` - Payment Successful

<Card title="✅ Payment Successful" icon="check" color="#22c55e">
  The payment has been validated by the payment processor. **You can safely fulfill the order.**

  **Typical flow:**

  1. Customer completes payment
  2. Payment processor confirms the charge
  3. You receive `succeeded` webhook
  4. Ship the order / provide the service
</Card>

### `pending_review` - Fraud Review Required

<Card title="⚠️ Pending Review" icon="shield" color="#f59e0b">
  The payment has been flagged by our fraud detection system for manual review. **Do NOT ship until you receive a final decision.**

  **Typical flow:**

  1. Customer completes payment
  2. Fraud detection flags the transaction
  3. You receive `pending_review` webhook
  4. Admin reviews the transaction
  5. You receive either `succeeded` or `declined` webhook
</Card>

<Warning>
  Never fulfill orders that are in `pending_review` status. Wait for the final `succeeded` or `declined` webhook.
</Warning>

### `declined` - Fraud Declined

<Card title="❌ Declined (Fraud)" icon="ban" color="#ef4444">
  The payment was automatically declined by the fraud prevention system. **The customer has been automatically refunded.**

  **Typical flow:**

  1. Customer completes payment
  2. Fraud score exceeds threshold
  3. Automatic refund is issued
  4. You receive `declined` webhook
  5. Do NOT ship the order
</Card>

### `refunded` - Payment Refunded

<Card title="↩️ Refunded" icon="rotate-left" color="#3b82f6">
  A refund has been processed for this transaction.

  **Typical flow:**

  1. Original payment was successful
  2. Refund is requested (by you or the customer)
  3. Refund is processed
  4. You receive `refunded` webhook
</Card>

### `expired` - Session Expired

<Card title="⏰ Expired" icon="clock" color="#6b7280">
  The payment session expired before the customer completed payment.

  **Typical flow:**

  1. Payment session is created
  2. Customer does not complete payment within the session lifetime
  3. Session expires automatically
  4. You receive `expired` webhook
</Card>

### `incomplete` - Crypto Payment Underpaid

<Card title="⚠️ Incomplete" icon="triangle-exclamation" color="#e6a01e">
  A crypto payment came in short — the customer sent less than the requested amount. This is an **opt-in** event (enable it in your webhook settings), and only applies to crypto payments.

  The payload includes **`amount_remaining`** (how much is still due, in the smallest currency unit). The amount already received equals `amount - amount_remaining`.

  **Typical flow:**

  1. Customer pays a crypto invoice but sends less than the requested amount
  2. You receive `incomplete` (with `amount_remaining`)
  3. Customer sends the remaining amount to the same address
  4. You receive `succeeded`

  **Do NOT fulfill** the order until you receive `succeeded`.
</Card>

## Webhook Headers

Every webhook request includes these headers:

| Header         | Description                            |
| -------------- | -------------------------------------- |
| `Content-Type` | Always `application/json`              |
| `Signature`    | HMAC SHA256 signature for verification |

## Signature Verification

<Warning>
  **Critical Security Step:** Always verify the webhook signature before processing the payload. This ensures the request is legitimate and from Payviox.
</Warning>

The `Signature` header contains an HMAC SHA256 hash of the request body, signed with your webhook token. Here's how to verify it:

### Verification Algorithm

1. Get the raw request body (JSON string)
2. Compute HMAC SHA256 hash using your webhook token as the secret key
3. Compare the computed signature with the `Signature` header
4. Only process the webhook if signatures match

### Implementation Examples

<CodeGroup>
  ```php PHP theme={null}
  <?php

  function verifyWebhookSignature($payload, $receivedSignature, $webhookToken) {
      // Compute the expected signature
      $computedSignature = hash_hmac('sha256', $payload, $webhookToken);
      
      // Use hash_equals to prevent timing attacks
      return hash_equals($computedSignature, $receivedSignature);
  }

  // Example usage in a Laravel controller
  public function handleWebhook(Request $request) {
      // Get the raw payload
      $payload = $request->getContent();
      
      // Get the signature from headers
      $receivedSignature = $request->header('Signature');
      
      // Your webhook token from the dashboard
      $webhookToken = env('PAYVIOX_WEBHOOK_TOKEN');
      
      // Verify the signature
      if (!verifyWebhookSignature($payload, $receivedSignature, $webhookToken)) {
          // Invalid signature - reject the request
          return response()->json(['error' => 'Invalid signature'], 401);
      }
      
      // Parse the verified payload
      $data = json_decode($payload, true);
      
      // Process the webhook based on type
      switch ($data['type']) {
          case 'succeeded':
              // Payment succeeded - fulfill the order
              $this->fulfillOrder($data['order_id'], $data);
              break;
              
          case 'pending_review':
              // Payment flagged for fraud review - DO NOT ship yet
              $this->updateOrderStatus($data['order_id'], 'pending_review');
              break;
              
          case 'declined':
              // Payment declined by fraud prevention - customer refunded
              $this->handleDeclinedPayment($data['order_id']);
              break;
              
          case 'refunded':
              // Payment was refunded
              $this->handleRefund($data['order_id'], $data);
              break;
      }
      
      // Return 200 OK to acknowledge receipt
      return response()->json(['status' => 'success'], 200);
  }

  private function fulfillOrder($orderId, $webhookData) {
      // Your order fulfillment logic
      $order = Order::where('order_id', $orderId)->first();
      
      if ($order) {
          $order->status = 'completed';
          $order->payment_provider = $webhookData['provider'];
          $order->payment_amount = $webhookData['amount'];
          $order->session_id = $webhookData['metadata']['integration_session_id'];
          $order->save();
          
          // Send confirmation email, provision services, etc.
      }
  }
  ```

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

  function verifyWebhookSignature(payload, receivedSignature, webhookToken) {
      // Compute the expected signature
      const computedSignature = crypto
          .createHmac('sha256', webhookToken)
          .update(payload)
          .digest('hex');
      
      // Use timingSafeEqual to prevent timing attacks
      return crypto.timingSafeEqual(
          Buffer.from(computedSignature),
          Buffer.from(receivedSignature)
      );
  }

  const app = express();

  // Important: Use express.raw() to get the raw body for signature verification
  app.post('/api/webhook', express.raw({ type: 'application/json' }), (req, res) => {
      // Get the raw payload as string
      const payload = req.body.toString('utf8');
      
      // Get the signature from headers
      const receivedSignature = req.headers['signature'];
      
      // Your webhook token from environment variable
      const webhookToken = process.env.PAYVIOX_WEBHOOK_TOKEN;
      
      // Verify the signature
      if (!verifyWebhookSignature(payload, receivedSignature, webhookToken)) {
          return res.status(401).json({ error: 'Invalid signature' });
      }
      
      // Parse the verified payload
      const data = JSON.parse(payload);
      
      // Process the webhook based on type
      switch (data.type) {
          case 'succeeded':
              fulfillOrder(data.order_id, data);
              break;
              
          case 'pending_review':
              // Payment flagged for fraud review - DO NOT ship yet
              updateOrderStatus(data.order_id, 'pending_review');
              break;
              
          case 'declined':
              // Payment declined by fraud prevention - customer refunded
              handleDeclinedPayment(data.order_id);
              break;
              
          case 'refunded':
              handleRefund(data.order_id, data);
              break;
      }
      
      // Return 200 OK to acknowledge receipt
      res.json({ status: 'success' });
  });

  async function fulfillOrder(orderId, webhookData) {
      // Your order fulfillment logic
      const order = await Order.findOne({ order_id: orderId });
      
      if (order) {
          order.status = 'completed';
          order.payment_provider = webhookData.provider;
          order.payment_amount = webhookData.amount;
          order.session_id = webhookData.metadata.integration_session_id;
          await order.save();
          
          // Send confirmation email, provision services, etc.
      }
  }

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

  ```python Python / Flask theme={null}
  import hmac
  import hashlib
  import json
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  def verify_webhook_signature(payload, received_signature, webhook_token):
      """Verify the webhook signature"""
      # Compute the expected signature
      computed_signature = hmac.new(
          webhook_token.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()
      
      # Use compare_digest to prevent timing attacks
      return hmac.compare_digest(computed_signature, received_signature)

  @app.route('/api/webhook', methods=['POST'])
  def handle_webhook():
      # Get the raw payload
      payload = request.get_data(as_text=True)
      
      # Get the signature from headers
      received_signature = request.headers.get('Signature')
      
      # Your webhook token from environment variable
      webhook_token = os.environ.get('PAYVIOX_WEBHOOK_TOKEN')
      
      # Verify the signature
      if not verify_webhook_signature(payload, received_signature, webhook_token):
          return jsonify({'error': 'Invalid signature'}), 401
      
      # Parse the verified payload
      data = json.loads(payload)
      
      # Process the webhook based on type
      if data['type'] == 'succeeded':
          fulfill_order(data['order_id'], data)
      elif data['type'] == 'pending_review':
          # Payment flagged for fraud review - DO NOT ship yet
          update_order_status(data['order_id'], 'pending_review')
      elif data['type'] == 'declined':
          # Payment declined by fraud prevention - customer refunded
          handle_declined_payment(data['order_id'])
      elif data['type'] == 'refunded':
          handle_refund(data['order_id'], data)
      
      # Return 200 OK to acknowledge receipt
      return jsonify({'status': 'success'}), 200

  def fulfill_order(order_id, webhook_data):
      """Handle successful payment"""
      # Your order fulfillment logic
      order = Order.query.filter_by(order_id=order_id).first()
      
      if order:
          order.status = 'completed'
          order.payment_provider = webhook_data['provider']
          order.payment_amount = webhook_data['amount']
          order.session_id = webhook_data['metadata']['integration_session_id']
          db.session.commit()
          
          # Send confirmation email, provision services, etc.

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```ruby Ruby / Sinatra theme={null}
  require 'sinatra'
  require 'json'
  require 'openssl'

  def verify_webhook_signature(payload, received_signature, webhook_token)
    # Compute the expected signature
    computed_signature = OpenSSL::HMAC.hexdigest(
      OpenSSL::Digest.new('sha256'),
      webhook_token,
      payload
    )
    
    # Use secure_compare to prevent timing attacks
    Rack::Utils.secure_compare(computed_signature, received_signature)
  end

  post '/api/webhook' do
    # Get the raw payload
    request.body.rewind
    payload = request.body.read
    
    # Get the signature from headers
    received_signature = request.env['HTTP_SIGNATURE']
    
    # Your webhook token from environment variable
    webhook_token = ENV['PAYVIOX_WEBHOOK_TOKEN']
    
    # Verify the signature
    unless verify_webhook_signature(payload, received_signature, webhook_token)
      halt 401, { error: 'Invalid signature' }.to_json
    end
    
    # Parse the verified payload
    data = JSON.parse(payload)
    
    # Process the webhook based on type
    case data['type']
    when 'succeeded'
      fulfill_order(data['order_id'], data)
    when 'pending_review'
      # Payment flagged for fraud review - DO NOT ship yet
      update_order_status(data['order_id'], 'pending_review')
    when 'declined'
      # Payment declined by fraud prevention - customer refunded
      handle_declined_payment(data['order_id'])
    when 'refunded'
      handle_refund(data['order_id'], data)
    end
    
    # Return 200 OK to acknowledge receipt
    content_type :json
    { status: 'success' }.to_json
  end

  def fulfill_order(order_id, webhook_data)
    # Your order fulfillment logic
    order = Order.find_by(order_id: order_id)
    
    if order
      order.status = 'completed'
      order.payment_provider = webhook_data['provider']
      order.payment_amount = webhook_data['amount']
      order.session_id = webhook_data['metadata']['integration_session_id']
      order.save
      
      # Send confirmation email, provision services, etc.
    end
  end
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "io"
      "log"
      "net/http"
      "os"
  )

  type WebhookPayload struct {
      Amount        int                    `json:"amount"`
      Currency      string                 `json:"currency"`
      Metadata      map[string]interface{} `json:"metadata"`
      Type          string                 `json:"type"`
      Provider      string                 `json:"provider"`
      OrderID       string                 `json:"order_id"`
      Items         []Item                 `json:"items"`
      PaymentMethod string                 `json:"payment_method"`
  }

  type Item struct {
      Name     string `json:"name"`
      Quantity int    `json:"quantity"`
      Price    int    `json:"price"`
  }

  func verifyWebhookSignature(payload string, receivedSignature string, webhookToken string) bool {
      // Compute the expected signature
      mac := hmac.New(sha256.New, []byte(webhookToken))
      mac.Write([]byte(payload))
      computedSignature := hex.EncodeToString(mac.Sum(nil))
      
      // Use hmac.Equal to prevent timing attacks
      return hmac.Equal([]byte(computedSignature), []byte(receivedSignature))
  }

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
      // Get the raw payload
      body, err := io.ReadAll(r.Body)
      if err != nil {
          http.Error(w, "Error reading request body", http.StatusBadRequest)
          return
      }
      payload := string(body)
      
      // Get the signature from headers
      receivedSignature := r.Header.Get("Signature")
      
      // Your webhook token from environment variable
      webhookToken := os.Getenv("PAYVIOX_WEBHOOK_TOKEN")
      
      // Verify the signature
      if !verifyWebhookSignature(payload, receivedSignature, webhookToken) {
          http.Error(w, `{"error": "Invalid signature"}`, http.StatusUnauthorized)
          return
      }
      
      // Parse the verified payload
      var data WebhookPayload
      if err := json.Unmarshal(body, &data); err != nil {
          http.Error(w, "Error parsing JSON", http.StatusBadRequest)
          return
      }
      
      // Process the webhook based on type
      switch data.Type {
      case "succeeded":
          fulfillOrder(data.OrderID, data)
      case "pending_review":
          // Payment flagged for fraud review - DO NOT ship yet
          updateOrderStatus(data.OrderID, "pending_review")
      case "declined":
          // Payment declined by fraud prevention - customer refunded
          handleDeclinedPayment(data.OrderID)
      case "refunded":
          handleRefund(data.OrderID, data)
      }
      
      // Return 200 OK to acknowledge receipt
      w.Header().Set("Content-Type", "application/json")
      w.WriteHeader(http.StatusOK)
      json.NewEncoder(w).Encode(map[string]string{"status": "success"})
  }

  func fulfillOrder(orderID string, webhookData WebhookPayload) {
      // Your order fulfillment logic
      log.Printf("Fulfilling order %s", orderID)
      // Update database, send emails, provision services, etc.
  }

  func main() {
      http.HandleFunc("/api/webhook", handleWebhook)
      log.Println("Webhook server listening on port 3000")
      log.Fatal(http.ListenAndServe(":3000", nil))
  }
  ```
</CodeGroup>

## Retry Logic

Payviox implements an automatic retry mechanism for failed webhook deliveries:

<Steps>
  <Step title="First Attempt">
    Instant delivery (0 seconds)
  </Step>

  <Step title="Second Attempt">
    30 seconds after first failure
  </Step>

  <Step title="Third Attempt">
    5 minutes after second failure
  </Step>

  <Step title="Fourth Attempt">
    30 minutes after third failure
  </Step>
</Steps>

<Info>
  After 4 failed attempts, the webhook will be marked as failed. You can manually retry failed webhooks from the Payviox Dashboard.
</Info>

### Success Criteria

A webhook is considered successfully delivered when your endpoint:

* Returns HTTP status code `200`
* Responds within 30 seconds

## Troubleshooting

### Common Issues

<AccordionGroup>
  <Accordion title="Signature Verification Failing">
    **Possible causes:**

    * Using the wrong webhook token
    * Parsing/modifying the request body before verification
    * Incorrect HMAC algorithm (must be SHA256)

    **Solution:** Always verify against the raw request body, before any parsing or modifications.
  </Accordion>

  <Accordion title="Webhooks Not Being Received">
    **Possible causes:**

    * Incorrect webhook URL in dashboard
    * Firewall blocking Payviox IP addresses
    * Server not responding within 30 seconds

    **Solution:** Check your webhook URL configuration and server logs.
  </Accordion>

  <Accordion title="Duplicate Webhook Processing">
    **Possible causes:**

    * Not implementing idempotency checks
    * Slow response times causing retries

    **Solution:** Implement idempotency using session\_id and order\_id, and respond quickly with 200 OK.
  </Accordion>

  <Accordion title="Webhook Timeout">
    **Possible causes:**

    * Processing taking too long before responding
    * Database locks or slow queries

    **Solution:** Return 200 OK immediately and process webhooks asynchronously in a queue.
  </Accordion>
</AccordionGroup>

## Security Checklist

<Checklist>
  <Check>Always verify webhook signatures before processing</Check>
  <Check>Use HTTPS for your webhook endpoint</Check>
  <Check>Store webhook token securely (environment variables)</Check>
  <Check>Implement rate limiting on your webhook endpoint</Check>
  <Check>Log all webhook attempts with timestamps</Check>
  <Check>Validate webhook payload structure</Check>
  <Check>Use timing-safe comparison functions for signature verification</Check>
  <Check>Implement idempotency to handle duplicate webhooks</Check>
</Checklist>

## Need Help?

If you're having trouble with webhook integration:

* Check the [Webhook Logs](https://dash.payviox.com/webhooks) in your dashboard
* Review your server logs for errors
* Contact [support@payviox.com](mailto:support@payviox.com) for assistance

<Tip>
  Include your webhook attempt IDs from the dashboard when contacting support for faster resolution.
</Tip>
