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

# Webhooks

> Integration Guide for Webhook Events

<Info>
  Before integrating webhooks, make sure you've completed the
  [Quickstart](/quickstart) guide and have your authentication set up.
</Info>

## Overview

When you make a request to our API, you'll typically get an immediate response. However, some operations like payments can take time to process. Instead of timing out, we return a pending status and use webhooks to notify you of the final result.

You have two options for handling these async operations:

* Poll the API endpoints periodically (not recommended for production)
* **Use webhooks to receive real-time event updates (recommended)**

### Webhooks vs Polling

<AccordionGroup>
  <Accordion title="Polling Approach">
    * Make repeated GET requests to check transaction status - Higher latency
      and more resource intensive - May miss state changes between polls - Better
      suited for testing/debugging
  </Accordion>

  <Accordion title="Webhook Approach">
    * Receive instant notifications when state changes - More efficient and
      scalable - No missed events - Recommended for production use
  </Accordion>
</AccordionGroup>

## Setting Up Webhooks

### 1. Create Your Webhook URL

Create a POST endpoint on your server to receive webhook events. The endpoint should:

1. Accept JSON payloads
2. Return a 200 OK response
3. Process events idempotently (handle duplicates safely)

<CodeGroup>
  ```python Python theme={null} theme={null} theme={null}
  from flask import Flask, request
  import hmac
  import hashlib
  import json

  app = Flask(__name__)

  def validate_signature(payload, checksum, business_id):
    # Separate event and data
    event = payload['event']
    data = json.dumps(payload['data'])
    message = f"{event}|{data}"

    # Create HMAC SHA256 hash
    expected = hmac.new(
      business_id.encode('utf-8'),
      message.encode('utf-8'),
      hashlib.sha256
    ).hexdigest().upper()

    return hmac.compare_digest(expected, checksum)

  @app.route('/webhook', methods=['POST'])
  def handle_webhook():
    payload = request.json
    checksum = payload.get('checksum')
    business_id = 'YOUR_BUSINESS_ID'

    # Validate webhook signature
    if not validate_signature(payload, checksum, business_id):
      return 'Invalid signature', 401

    # Process the webhook event
    event_type = payload.get('event')
    if event_type == 'payment.session.succeeded':
      handle_successful_payment(payload['data'])
    elif event_type == 'payment.session.failed':
      handle_failed_payment(payload['data'])

    # Return 200 to acknowledge receipt
    return 'Webhook received', 200
  ```

  ```javascript Node.js theme={null} theme={null} theme={null}
  import crypto from "crypto";
  import express from "express";
  import stringify from "json-stable-stringify";

  const app = express();
  app.use(express.json());

  function validateSignature(payload, checksum, businessId) {
    const { event, data } = payload;

    const stringData = JSON.stringify(data);
    const message = `${event}|${stringData}`;

    // Create HMAC SHA256 hash
    const expectedChecksum = crypto
      .createHmac("sha256", businessId)
      .update(message)
      .digest("hex")
      .toUpperCase();

    return crypto.timingSafeEqual(
      Buffer.from(expectedChecksum),
      Buffer.from(checksum),
    );
  }

  app.post("/webhook", (req, res) => {
    const payload = req.body;
    const checksum = payload.checksum;
    const businessId = "YOUR_BUSINESS_ID";

    // Validate webhook signature
    if (!validateSignature(payload, checksum, businessId)) {
      return res.status(401).send("Invalid signature");
    }

    // Process webhook event
    const eventType = payload.event;
    switch (eventType) {
      case "payment.session.succeeded":
        handleSuccessfulPayment(payload.data);
        break;
      case "payment.session.failed":
        handleFailedPayment(payload.data);
        break;
    }

    // Acknowledge receipt
    res.status(200).send("Webhook received");
  });
  ```

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

  function validate_signature($payload, string $checksum, string $business_id): bool
  {
      $event = $payload->event;
      $data = json_encode($payload->data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
      $message = $event . '|' . $data;

      $expected = strtolower(hash_hmac('sha256', $message, $business_id));
      $checksum = strtolower($checksum);

      return hash_equals($expected, $checksum);
  }

  $data = file_get_contents('php://input');
  $payload = json_decode($data);
  $checksum = $payload->checksum;
  $business_id = '<business-id>';

  // Validate webhook signature
  if (!validate_signature($payload, $checksum, $business_id)) {
      http_response_code(401);
      echo 'Invalid signature' . "\n";
      exit;
  }

  // Process webhook event
  $eventType = $payload->event;
  switch($eventType) {
    case 'payment.session.succeeded':
      handleSuccessfulPayment($payload->data->);
      break;
    case 'payment.session.failed':
      handleFailedPayment($payload->data);
      break;
  }

  // Acknowledge receipt
  http_response_code(200);
  echo 'Webhook received';
  ```
</CodeGroup>

### 2. Register Your Webhook URL

Add your webhook URL to your account settings:

```bash theme={null}
PATCH /accounts/settings
```

```json Request theme={null}
{
  "webhook_urls": ["https://your-domain.com/webhooks"]
}
```

```json Response theme={null}
{
  "data": {
    "settings": {
      "webhook_urls": ["https://your-domain.com/webhooks"]
    }
  }
}
```

## Security

<Card title="Security Notice" icon="shield-halved">
  **Important Security Considerations:**

  * Never expose sensitive credentials in client-side code or VCS
  * Always validate request signatures and origins
  * Use HTTPS for all API communications
  * Implement proper access control and authentication
  * Follow secure key management practices

  <Accordion title="Detailed Security Guidelines">
    <Warning>
      **Never Share or Expose:**

      * API Keys
      * Secret Keys
      * Encryption Keys
      * Webhook Secrets
      * Authentication Tokens
    </Warning>

    **Key Security Measures:**

    1. Store sensitive data in secure environment variables or dedicated key management systems
    2. Implement IP whitelisting where possible
    3. Validate all incoming webhook signatures
    4. Use strong TLS/SSL for all connections
    5. Rotate credentials regularly
    6. Log and monitor access attempts
    7. Follow the principle of least privilege

    **Implementation Tips:**

    ```javascript theme={null} theme={null} theme={null}
    // ❌ Avoid hardcoding secrets
    const apiKey = "sk_live_123..."; // Bad practice

    // ✅ Use environment variables
    const apiKey = process.env.API_KEY; // Good practice

    // ✅ Validate webhook signatures
    const isValidSignature = verifyWebhookSignature(
      payload,
      signature,
      webhookSecret,
    );
    ```

    <Info>
      For additional security best practices, refer to our Security Guidelines in the documentation.
    </Info>
  </Accordion>
</Card>

### Verifying Webhook Origins

Secure your webhook endpoint using either or both:

#### 1. Checksum Validation

Each webhook includes a checksum for verification:

```json theme={null}
{
  "checksum": "32762AE880695AE7343A649CB9C36CA6FF83AA258A139804AEF7D73B421DE097",
  "data": {
    "card_id": "81817411-9ffd-42ba-8bc8-f407b5cef9d9",
    "amount": 1000,
    "reference": "b070b0d2-e394-4783-a6f0-f10ccb3cae89",
    "currency": "USD"
  },
  "event": "card.transaction"
}
```

To validate:

1. Concatenate: `event|json_encoded_data`
2. Create HMAC SHA-256 hash using your business ID as the key
3. Compare with the received checksum

<Warning>
  The encoded data must exclude the checksum field and be in alphabetical order:

  ```json Valid Order theme={null}
  {
    "amount": 1000,
    "card_id": "81817411-9ffd-42ba-8bc8-f407b5cef9d9",
    "currency": "USD",
    "reference": "b070b0d2-e394-4783-a6f0-f10ccb3cae89"
  }
  ```
</Warning>

#### 2. IP Whitelisting

Whitelist these Juicyway IPs:

```bash theme={null}
104.248.130.0
104.248.136.223
142.93.166.85
142.93.170.65
159.65.121.43
165.22.78.88
209.38.217.122
209.38.227.232
46.101.179.170
46.101.190.207
64.226.91.150
167.172.97.202
```

## Go-Live Checklist

<Steps>
  <Step title="Verify Public Access">
    Ensure your webhook URL is publicly accessible (no localhost)
  </Step>

  <Step title="URL Configuration">Add trailing `/` if using .htaccess</Step>
  <Step title="Test Integration">Verify JSON parsing and 200 OK responses</Step>

  <Step title="Handle Long Tasks">
    Return 200 OK before processing lengthy operations
  </Step>

  <Step title="Monitor Failed Webhooks">
    Track non-200 responses in your logs
  </Step>

  <Step title="Implement Idempotency">Handle duplicate events safely</Step>
</Steps>

## Supported Events

<Note>
  In sandbox, successful transactions remain pending. Only failure events are
  sent.
</Note>

### Payment Events

<AccordionGroup>
  <Accordion title="payment.session.failed|succeeded">
    ```json theme={null}
    {
      "checksum": "",
      "data": {
        "amount": 10000,
        "callback_urls": {},
        "cancellation_reason": null,
        "channel_reference": "",
        "collection_mode": null,
        "correlation_id": "",
        "currency": "NGN",
        "customer": {
          "account_id": "",
          "billing_address": {
            "city": "",
            "country": "NG",
            "line1": "",
            "state": "",
            "zip_code": ""
          },
          "email": "",
          "first_name": "",
          "id": "",
          "last_name": "",
          "phone_number": "",
          "type": ""
        },
        "date": "",
        "description": "",
        "fee": null,
        "id": "",
        "merchant": {
          "address": {
            "city": "",
            "country": "NG",
            "line1": "",
            "line2": null,
            "state": "",
            "zip_code": ""
          },
          "email": "",
          "id": "",
          "mcc": "",
          "name": "",
          "phone": ""
        },
        "order": {
          "identifier": "",
          "items": [
            {
              "name": "Transfer",
              "type": "digital"
            }
          ]
        }
        "mode": "live",
        "order": {
          "identifier": "",
          "items": [
            {
              "name": "Transfer",
              "type": "digital"
            }
          ]
        },
        "payer": {
          "account_name": null,
          "account_number": null,
          "bank_name": ""
        },
        "payment_method": {
          "account_numner": "",
          "account_name": "",
          "bank_code": "",
          "bank_name": "",
          "currency": "",
          "id": "",
          "type": "bank_account"
        },
        "provider_id": "",
        "redirect_url": null,
        "reference": "",
        "status": "success|failed",
        "transaction_id": "",
        "type": "payin|payout"
      },
      "event": "payment.session.succeeded|payment.session.failed"

    }

    ```
  </Accordion>
</AccordionGroup>

<Card title="Next Steps">
  * Learn about [API Request Authentication](/authentication) - Review common
    [Error Handling](/errors) - Explore the full [API
    Reference](/api-reference/overview)
</Card>

```
```
