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

# Webhooks

All webhooks sent by the Grid API include a signature in the `X-Grid-Signature` header, which allows you to verify the authenticity of the webhook. This is critical for security, as it ensures that only legitimate webhooks from Grid are processed by your system.

## Signature Verification Process

1. **Obtain your Grid public key**
   * This is available in your [dashboard](https://app.lightspark.com) under API settings.
   * The key is in PEM format and can be used with standard cryptographic libraries

2. **Verify incoming webhooks**
   * Extract the signature from the `X-Grid-Signature` header
   * Decode the base64 signature
   * Create a SHA-256 hash of the entire request body
   * Verify the signature using the Grid webhook public key and the hash
   * Only process the webhook if the signature verification succeeds

## Verification Examples

### Node.js Example

```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();

// Your Grid public key provided during integration
const GRID_WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----`;

app.post('/webhooks/uma', (req, res) => {
  const signatureHeader = req.header('X-Grid-Signature');

  if (!signatureHeader) {
    return res.status(401).json({ error: 'Signature missing' });
  }

  try {
    let signature: Buffer;
    try {
      // Parse the signature as JSON. It's in the format {"v": "1", "s": "base64_signature"}
      const signatureObj = JSON.parse(signatureHeader);
      if (signatureObj.v && signatureObj.s) {
        // The signature is in the 's' field
        signature = Buffer.from(signatureObj.s, "base64");
      } else {
        throw new Error("Invalid JSON signature format");
      }
    } catch {
      // If JSON parsing fails, treat as direct base64
      signature = Buffer.from(signatureHeader, "base64");
    }

    // Create verifier with the public key and correct algorithm
    const verifier = crypto.createVerify("SHA256");
    const payload = await request.text();
    verifier.update(payload);
    verifier.end();

    // Verify the signature using the webhook public key
    const isValid = verifier.verify(
      {
        key: GRID_WEBHOOK_PUBLIC_KEY,
        format: "pem",
        type: "spki",
      },
      signature,
    );

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Webhook is verified, process it based on type
    const webhookData = req.body;

    if (webhookData.type.startsWith('INCOMING_PAYMENT.')) {
      // Process incoming payment webhook
      // ...
    } else if (webhookData.type.startsWith('OUTGOING_PAYMENT.')) {
      // Process outgoing payment webhook
      // ...
    }

    // Acknowledge receipt of the webhook
    return res.status(200).json({ received: true });
  } catch (error) {
    console.error('Signature verification error:', error);
    return res.status(401).json({ error: 'Signature verification failed' });
  }
});

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

### Python Example

```python theme={null}
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from flask import Flask, request, jsonify
import base64

app = Flask(__name__)

# Your Grid public key provided during integration
GRID_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""

# Load the public key
public_key = serialization.load_pem_public_key(
    GRID_PUBLIC_KEY.encode('utf-8')
)

@app.route('/webhooks/uma', methods=['POST'])
def handle_webhook():
    # Get signature from header
    signature = request.headers.get('X-Grid-Signature')
    if not signature:
        return jsonify({'error': 'Signature missing'}), 401

    try:
        # Get the raw request body
        request_body = request.get_data()

        # Create a SHA-256 hash of the request body
        hash_obj = hashes.Hash(hashes.SHA256())
        hash_obj.update(request_body)
        digest = hash_obj.finalize()

        # Decode the base64 signature
        signature_bytes = base64.b64decode(signature)

        # Verify the signature
        try:
            public_key.verify(
                signature_bytes,
                request_body,
                ec.ECDSA(hashes.SHA256())
            )
        except Exception as e:
            return jsonify({'error': 'Invalid signature'}), 401

        # Webhook is verified, process it based on type
        webhook_data = request.json

        if webhook_data['type'].startswith('INCOMING_PAYMENT.'):
            # Process incoming payment webhook
            # ...
            pass
        elif webhook_data['type'].startswith('OUTGOING_PAYMENT.'):
            # Process outgoing payment webhook
            # ...
            pass

        # Acknowledge receipt of the webhook
        return jsonify({'received': True}), 200
    except Exception as e:
        print(f'Signature verification error: {e}')
        return jsonify({'error': 'Signature verification failed'}), 401

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

## Testing

To test your webhook implementation, you can trigger a test webhook from the Grid dashboard. This will send a test webhook to the endpoint you provided during the integration process. The test webhook will also be sent automatically when you update your platform configuration with a new webhook URL.

An example of the test webhook payload is shown below:

```json theme={null}
{
  "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
  "type": "TEST",
  "timestamp": "2025-08-15T14:32:00Z",
  "data": {}
}
```

You should verify the signature of the webhook using the Grid public key and the process outlined in the [Signature Verification Process](#signature-verification-process) section and then reply with a 200 OK response to acknowledge receipt of the webhook.

## Security Considerations

* **Always verify signatures**: Never process webhooks without verifying their signatures.
* **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks.
* **Implement idempotency**: Use the `id` field to prevent processing duplicate webhooks.
* **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly.

## Retry Policy

The Grid API will retry webhooks with the following policy based on the webhook type:

| Webhook Type         | Retry Policy                                                                 | Notes                                                                                                      |
| -------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `TEST`               | No retries                                                                   | Used for testing webhook configuration                                                                     |
| `OUTGOING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks)                                                                       |
| `INCOMING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| `BULK_UPLOAD.*`      | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks)                                                                       |
| `INVITATION.*`       | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks)                                                                       |
| `CUSTOMER.*`         | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks)                                                                       |
| `ACCOUNT.*`          | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks)                                                                       |
