Search anything...
K
Back to Docs
  • Introduction
  • Quick Start
  • Account Setup
  • AI Studio
  • Chat
  • Agents
  • Voice
  • MCP Servers
  • Workflows
  • Authentication
  • Studio API
  • Chat API
  • Agents API
  • Voice API
  • Workflows API
  • Webhooks
  • Error Codes
  • Creating Custom Agents
  • MCP Integration
  • Building Workflows
  • Prompt Engineering
  • Team Management
  • Billing & Plans
  • Usage Monitoring
  • Single-Tenant Cloud
  • Private VPC Deployment
  • SSO Configuration
  • Security Policies
  • Compliance
  • Troubleshooting
  • API Versioning
DocsAPI ReferenceWebhooks

Webhooks

Webhook endpoints and events

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

EventDescription
workflow.run.startedA workflow execution has started
workflow.run.completedA workflow execution completed successfully
workflow.run.failedA workflow execution failed
test.webhookTest 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:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the request body
X-Webhook-TimestampUnix timestamp when the webhook was sent
X-Webhook-IDUnique 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

  1. Always verify signatures - Never trust webhooks without signature verification
  2. Use timing-safe comparison - Prevent timing attacks when comparing signatures
  3. Validate timestamps - Reject old webhooks to prevent replay attacks
  4. Use HTTPS - Ensure data is encrypted in transit
  5. Rotate secrets - Periodically rotate your webhook secrets
  6. Limit access - Restrict who can configure webhooks in your organization

Troubleshooting

Webhook Not Received

  1. Check your endpoint is publicly accessible
  2. Verify the URL is correct in webhook settings
  3. Check your server logs for incoming requests
  4. Ensure your firewall allows requests from Girard AI IPs

Signature Verification Fails

  1. Ensure you're using the raw request body (not parsed JSON)
  2. Verify you're using the correct webhook secret
  3. Check for encoding issues (use UTF-8)
  4. Compare the full signature including "sha256=" prefix

Timeouts

  1. Return 200 immediately, process async
  2. Check for slow database queries or external API calls
  3. 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
Previous
Workflows API
Next
Error Codes