# Webhooks

> **⚠️ CRITICAL SECURITY RECOMMENDATION**
>
> Without signature verification, your webhook endpoint is vulnerable to spoofing attacks. Malicious actors could send fake webhook requests to your system, potentially causing:
>
> * Incorrect order status updates
> * Unauthorized payment confirmations
> * Data corruption in your systems
> * Financial losses from fraudulent transactions
>
> **Always verify webhook signatures in production environments.**

### Quick Verification Checklist

Before deploying your webhook handler, ensure:

* [ ] You're reading the `X-Jeel-Signature` header from webhook requests
* [ ] You're using your `client_secret` for HMAC computation
* [ ] You're comparing signatures using constant-time comparison (to prevent timing attacks)
* [ ] You're rejecting webhooks with invalid signatures (return 401/403)
* [ ] You've tested with both valid and invalid signatures

### Delivery Behavior

Jeel Pay sends webhook requests with a **10 second timeout**. Your endpoint should complete signature verification, persist the event, and return a successful response within that window.

A webhook delivery is considered successful when your server returns any HTTP `2xx` status code (`200`-`299`) before the timeout. Any non-`2xx` response, connection failure, or timeout is considered a failed delivery.

If a webhook delivery fails or times out, Jeel Pay retries delivery using the following retry logic:

| Setting   | Value                    |
| --------- | ------------------------ |
| Mechanism | Exponential time backoff |
| Duration  | 30 seconds               |
| Limit     | 7 retry attempts         |

### Quick Implementation

Below are production-ready code examples you can copy and paste into your application.

> **Important:** Always verify the signature on the **raw request body** (as a string), NOT on parsed JSON. Parsing and re-stringifying JSON can change whitespace or key ordering, causing signature verification to fail.
>
> **Note:** The signature header is `X-Jeel-Signature`. HTTP headers are case-insensitive, so you may receive it as `x-jeel-signature` depending on your framework.

#### Java (Spring Boot)

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

public class WebhookSignatureVerifier {
    
    /**
     * Verifies the webhook signature using HMAC-SHA256
     * 
     * @param signature The X-Jeel-Signature header value
     * @param body The raw webhook request body (as received)
     * @param clientSecret Your client_secret from the Jeel Pay dashboard
     * @return true if signature is valid, false otherwise
     */
    public boolean verifySignature(String signature, String body, String clientSecret) {
        try {
            // Compute HMAC-SHA256
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                clientSecret.getBytes(StandardCharsets.UTF_8), 
                "HmacSHA256"
            );
            mac.init(secretKey);
            byte[] hmacBytes = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
            
            // Base64 encode
            String computed = Base64.getEncoder().encodeToString(hmacBytes);
            
            // Constant-time comparison to prevent timing attacks
            return MessageDigest.isEqual(
                signature.getBytes(StandardCharsets.UTF_8),
                computed.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }
}
```

**Usage in your controller:**

```java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class WebhookController {
    
    private final WebhookSignatureVerifier verifier = new WebhookSignatureVerifier();
    
    @PostMapping("/webhook")
    public ResponseEntity<Void> handleWebhook(
            @RequestHeader("X-Jeel-Signature") String signature,
            @RequestBody String body) {
        
        String clientSecret = System.getenv("JEEL_CLIENT_SECRET");
        if (clientSecret == null || clientSecret.isEmpty()) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
        
        if (!verifier.verifySignature(signature, body, clientSecret)) {
            // Reject the webhook
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        // Process the verified webhook
        return ResponseEntity.ok().build();
    }
}
```

#### PHP

```php
<?php

class WebhookSignatureVerifier {
    
    /**
     * Verifies the webhook signature using HMAC-SHA256
     * 
     * @param string $signature The X-Jeel-Signature header value
     * @param string $body The raw webhook request body
     * @param string $clientSecret Your client_secret from the Jeel Pay dashboard
     * @return bool true if signature is valid, false otherwise
     */
    public function verifySignature(string $signature, string $body, string $clientSecret): bool {
        // Compute HMAC-SHA256
        $computed = base64_encode(hash_hmac('sha256', $body, $clientSecret, true));
        
        // Constant-time comparison to prevent timing attacks
        return hash_equals($signature, $computed);
    }
}

// Usage in your webhook endpoint:
$signature = $_SERVER['HTTP_X_JEEL_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$clientSecret = $_ENV['JEEL_CLIENT_SECRET'] ?? null;

if (!$clientSecret) {
    error_log('JEEL_CLIENT_SECRET not configured');
    http_response_code(500);
    exit('Server configuration error');
}

$verifier = new WebhookSignatureVerifier();
if (!$verifier->verifySignature($signature, $body, $clientSecret)) {
    http_response_code(401);
    exit('Invalid signature');
}

// Process the verified webhook
http_response_code(200);
```

#### JavaScript (Node.js)

```javascript
const crypto = require('crypto');

class WebhookSignatureVerifier {
    /**
     * Verifies the webhook signature using HMAC-SHA256
     * 
     * @param {string} signature - The X-Jeel-Signature header value
     * @param {string} body - The raw webhook request body
     * @param {string} clientSecret - Your client_secret from the Jeel Pay dashboard
     * @returns {boolean} true if signature is valid, false otherwise
     */
    verifySignature(signature, body, clientSecret) {
        // Compute HMAC-SHA256
        const hmac = crypto.createHmac('sha256', clientSecret);
        hmac.update(body);
        const computed = hmac.digest('base64');
        
        // Constant-time comparison to prevent timing attacks
        try {
            return crypto.timingSafeEqual(
                Buffer.from(signature),
                Buffer.from(computed)
            );
        } catch (e) {
            // Buffers are different lengths
            return false;
        }
    }
}

// Usage in Express.js:
const express = require('express');
const app = express();

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-jeel-signature'];
    const body = req.body.toString(); // raw body as string
    const clientSecret = process.env.JEEL_CLIENT_SECRET;
    
    if (!clientSecret) {
        console.error('JEEL_CLIENT_SECRET not configured');
        return res.status(500).send('Server configuration error');
    }
    
    const verifier = new WebhookSignatureVerifier();
    if (!verifier.verifySignature(signature, body, clientSecret)) {
        return res.status(401).send('Invalid signature');
    }
    
    // Process the verified webhook
    res.status(200).send('OK');
});
```

#### Python

```python
import hmac
import hashlib
import base64
import os

class WebhookSignatureVerifier:
    
    @staticmethod
    def verify_signature(signature: str, body: str, client_secret: str) -> bool:
        """
        Verifies the webhook signature using HMAC-SHA256
        
        Args:
            signature: The X-Jeel-Signature header value
            body: The raw webhook request body
            client_secret: Your client_secret from the Jeel Pay dashboard
            
        Returns:
            True if signature is valid, False otherwise
        """
        # Compute HMAC-SHA256
        computed = base64.b64encode(
            hmac.new(
                client_secret.encode('utf-8'),
                body.encode('utf-8'),
                hashlib.sha256
            ).digest()
        ).decode('utf-8')
        
        # Constant-time comparison to prevent timing attacks
        return hmac.compare_digest(signature, computed)

# Usage in Flask:
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Jeel-Signature', '')
    body = request.get_data(as_text=True)
    client_secret = os.environ.get('JEEL_CLIENT_SECRET')
    
    if not client_secret:
        app.logger.error('JEEL_CLIENT_SECRET not configured')
        abort(500)
    
    if not WebhookSignatureVerifier.verify_signature(signature, body, client_secret):
        abort(401)
    
    # Process the verified webhook
    return 'OK', 200
```

### Webhook Payload Structure

After verifying the signature, parse the webhook body to process the event.

#### Payload Fields

| Field           | Type           | Description                                                   |
| --------------- | -------------- | ------------------------------------------------------------- |
| `checkout_id`   | string (UUID)  | Unique identifier for the checkout                            |
| `status`        | string         | Current status: `PENDING`, `REJECTED`, `SUCCEEDED`, `EXPIRED` |
| `checkout_type` | string         | Type: `SCHOOLING` or `ITEMS`                                  |
| `metadata`      | object         | Key-value pairs you provided during checkout creation         |
| `reference_id`  | string \| null | Your reference ID provided during checkout                    |

#### Example: Checkout Type SCHOOLING

```json
{
  "checkout_id": "9e79d502-231d-449b-b419-a674b687df51",
  "status": "SUCCEEDED",
  "checkout_type": "SCHOOLING",
  "metadata": {
    "example_key_1": "example value 1",
    "example_key_2": "example value 2"
  },
  "reference_id": "order_1234"
}
```

#### Example: Checkout Type ITEMS

```json
{
  "checkout_id": "9e79d502-231d-449b-b419-a674b687df51",
  "status": "SUCCEEDED",
  "checkout_type": "ITEMS",
  "metadata": {
    "example_key_1": "example value 1",
    "example_key_2": "example value 2"
  },
  "reference_id": "order_1234"
}
```

> Note: The actual payload is compact JSON (no whitespace). The formatted version above is for readability only.

### Testing Your Implementation

Use these commands to verify your signature verification works correctly.

#### Test with Valid Signature

First, generate a valid signature for testing:

```bash
# Set your values
CLIENT_SECRET="your_client_secret_here"
BODY='{"checkout_id":"9e79d502-231d-449b-b419-a674b687df51","status":"SUCCEEDED","checkout_type":"SCHOOLING","metadata":{},"reference_id":"order_1234"}'

# Generate signature (using openssl)
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$CLIENT_SECRET" -binary | base64)

echo "Generated signature: $SIGNATURE"

# Test your endpoint
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "X-Jeel-Signature: $SIGNATURE" \
  -d "$BODY"

# Expected: HTTP 200 OK
```

#### Test with Invalid Signature

```bash
# Test with wrong signature
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "X-Jeel-Signature: invalid_signature_here" \
  -d "$BODY"

# Expected: HTTP 401 Unauthorized
```

#### Testing Checklist

* [ ] Valid signature returns HTTP 200
* [ ] Invalid signature returns HTTP 401/403
* [ ] Missing signature header returns HTTP 401/403
* [ ] Tampered body (change one character) returns HTTP 401/403
* [ ] Different secret produces different signature

### How Signature Verification Works

#### The Algorithm

1. **Extract the body**: Take the raw webhook request body as a string (before JSON parsing)
2. **Compute HMAC**: Use HMAC-SHA256 with your `client_secret` as the key and the body as the message
3. **Base64 encode**: Convert the HMAC bytes to a Base64 string
4. **Compare**: Use constant-time comparison to match against the `X-Jeel-Signature` header

#### Why Constant-Time Comparison?

Regular string comparison (`==`) stops at the first different character. Attackers can measure response times to guess the signature one character at a time. Constant-time comparison takes the same amount of time regardless of where the strings differ, preventing this attack.

#### Security Benefits

* **Authenticity**: Only Jeel Pay (with the secret) can generate valid signatures
* **Integrity**: Any modification to the body will invalidate the signature
* **Non-repudiation**: Proves the webhook came from Jeel Pay

#### Best Practices

1. Store your `client_secret` securely (environment variables, secrets manager)
2. Never log the signature or client\_secret
3. Reject webhooks with missing or invalid signatures immediately
4. Verify signatures before any business logic
5. Use HTTPS for your webhook endpoints to prevent MITM attacks


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.jeel.co/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
