Skip to main content
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 provided to you during the integration process. Reach out to us at support@lightspark.com or over Slack to get the public key.
    • 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

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 === 'INCOMING_PAYMENT') {
      // Process incoming payment webhook
      // ...
    } else if (webhookData.type === '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

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'] == 'INCOMING_PAYMENT':
            # Process incoming payment webhook
            # ...
            pass
        elif webhook_data['type'] == '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:
{
  "test": true,
  "timestamp": "2023-08-15T14:32:00Z",
  "webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
  "type": "TEST"
}
You should verify the signature of the webhook using the Grid public key and the process outlined in the 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 webhookId 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 TypeRetry PolicyNotes
TESTNo retriesUsed for testing webhook configuration
OUTGOING_PAYMENTRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on 409 (duplicate webhooks)
INCOMING_PAYMENTRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow
BULK_UPLOADRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on 409 (duplicate webhooks)
INVITATION_CLAIMEDRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on 409 (duplicate webhooks)
KYC_STATUSRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on 409 (duplicate webhooks)
ACCOUNT_STATUSRetry with exponential backoff up to 7 days with maximum interval of 30 minsNo retry on 409 (duplicate webhooks)
I