Webhooks allow you to receive real-time notifications when events occur in your Girard AI account. Instead of polling the API for updates, webhooks push data to your endpoint automatically.
Overview
When an event occurs (like a workflow completing), Girard AI sends an HTTP POST request to your configured webhook URL with details about the event.
Setting Up Webhooks
1. Create a Webhook Endpoint
Create an endpoint on your server that:
- Accepts POST requests
- Returns a 2xx status code quickly (within 30 seconds)
- Verifies the webhook signature for security
2. Register Your Webhook
Register your webhook endpoint in the Girard AI dashboard under Settings > Webhooks, or via the API:
curl -X POST https://api.girardai.com/v1/webhooks \
-H "Authorization: Bearer gai_sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Webhook",
"url": "https://your-server.com/webhooks/girardai",
"events": ["workflow.run.completed", "workflow.run.failed"]
}'
Webhook Events
Workflow Events
| Event | Description |
|---|---|
workflow.run.started | A workflow execution has started |
workflow.run.completed | A workflow execution completed successfully |
workflow.run.failed | A workflow execution failed |
test.webhook | Test event for verifying webhook setup |
Event Payload Structure
All webhook payloads follow this structure:
{
"id": "evt_abc123xyz",
"type": "workflow.run.completed",
"createdAt": "2026-01-17T12:00:00Z",
"data": {
"id": "run_xyz789",
"workflowId": "wf_abc123",
"workflowName": "My Workflow",
"status": "COMPLETED",
"durationMs": 1234,
"tokensUsed": 150
}
}
Signature Verification
Important: Always verify webhook signatures to ensure requests are from Girard AI and haven't been tampered with.
Signature Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request body |
X-Webhook-Timestamp | Unix timestamp when the webhook was sent |
X-Webhook-ID | Unique identifier for this webhook delivery |
Verifying the Signature
The signature is computed as:
sha256=HMAC-SHA256(request_body, webhook_secret)
Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = `sha256=${crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex')}`;
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhooks/girardai', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body.toString();
// Verify timestamp is recent (within 5 minutes)
const timestampAge = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (timestampAge > 300) {
return res.status(400).send('Timestamp too old');
}
// Verify signature
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = JSON.parse(payload);
console.log('Received webhook:', event.type);
// Return 200 quickly
res.status(200).send('OK');
});
Python Example
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook_signature(payload, signature, secret):
expected = f"sha256={hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()}"
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/girardai', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
payload = request.get_data()
# Verify timestamp is recent (within 5 minutes)
timestamp_age = int(time.time()) - int(timestamp)
if timestamp_age > 300:
abort(400, 'Timestamp too old')
# Verify signature
if not verify_webhook_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
abort(401, 'Invalid signature')
# Process the webhook
event = request.get_json()
print(f"Received webhook: {event['type']}")
return 'OK', 200
Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func verifySignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
// Verify timestamp
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
http.Error(w, "Timestamp too old", http.StatusBadRequest)
return
}
// Read body
payload, _ := io.ReadAll(r.Body)
// Verify signature
if !verifySignature(payload, signature, os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook
fmt.Println("Webhook received")
w.WriteHeader(http.StatusOK)
}
Best Practices
1. Respond Quickly
Return a 2xx response within 30 seconds. Process the webhook asynchronously:
app.post('/webhooks', async (req, res) => {
// Return immediately
res.status(200).send('OK');
// Process asynchronously
setImmediate(async () => {
await processWebhook(req.body);
});
});
2. Handle Retries
Girard AI retries failed webhooks with exponential backoff:
- 1st retry: 1 second delay
- 2nd retry: 2 seconds delay
- 3rd retry: 4 seconds delay (max 3 retries)
Make your handler idempotent using the X-Webhook-ID header:
const processedWebhooks = new Set();
async function handleWebhook(event, webhookId) {
if (processedWebhooks.has(webhookId)) {
return; // Already processed
}
processedWebhooks.add(webhookId);
// Process the event...
}
3. Validate Timestamps
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks:
const timestamp = parseInt(req.headers['x-webhook-timestamp']);
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > 300) {
return res.status(400).send('Webhook expired');
}
4. Use HTTPS
Always use HTTPS endpoints. Girard AI will not send webhooks to HTTP URLs in production.
5. Handle Failures Gracefully
Log failures and alert on repeated failures:
try {
await processWebhook(event);
} catch (error) {
console.error('Webhook processing failed:', error);
// Still return 200 to prevent retries for application errors
// Only return non-2xx for transient errors you want retried
}
Testing Webhooks
Send a Test Webhook
Use the dashboard or API to send a test webhook:
curl -X POST https://api.girardai.com/v1/webhooks/{webhook_id}/test \
-H "Authorization: Bearer gai_sk_your_api_key"
Local Development
For local testing, use a tunnel service like ngrok:
ngrok http 3000
Then register the ngrok URL as your webhook endpoint during development.
Webhook Delivery Logs
View delivery history and debug failed webhooks in the dashboard under Settings > Webhooks > {webhook} > Deliveries.
Each delivery log includes:
- Event type and ID
- HTTP status code
- Response body (truncated)
- Delivery duration
- Retry count
Security Considerations
- Always verify signatures - Never trust webhooks without signature verification
- Use timing-safe comparison - Prevent timing attacks when comparing signatures
- Validate timestamps - Reject old webhooks to prevent replay attacks
- Use HTTPS - Ensure data is encrypted in transit
- Rotate secrets - Periodically rotate your webhook secrets
- Limit access - Restrict who can configure webhooks in your organization
Troubleshooting
Webhook Not Received
- Check your endpoint is publicly accessible
- Verify the URL is correct in webhook settings
- Check your server logs for incoming requests
- Ensure your firewall allows requests from Girard AI IPs
Signature Verification Fails
- Ensure you're using the raw request body (not parsed JSON)
- Verify you're using the correct webhook secret
- Check for encoding issues (use UTF-8)
- Compare the full signature including "sha256=" prefix
Timeouts
- Return 200 immediately, process async
- Check for slow database queries or external API calls
- Increase timeout if your infrastructure requires it
API Reference
List Webhooks
GET /v1/webhooks
Create Webhook
POST /v1/webhooks
{
"name": "string",
"url": "string",
"events": ["string"]
}
Update Webhook
PATCH /v1/webhooks/{id}
{
"name": "string",
"url": "string",
"events": ["string"],
"isActive": boolean
}
Delete Webhook
DELETE /v1/webhooks/{id}
Test Webhook
POST /v1/webhooks/{id}/test
Get Delivery History
GET /v1/webhooks/{id}/deliveries
Rotate Secret
POST /v1/webhooks/{id}/rotate-secret