Webhooks and Event-Driven Integration
AGLedger delivers webhook events for mandate lifecycle transitions. Register an HTTPS endpoint, and AGLedger pushes events as they happen.
Register a webhook
curl -X POST "$API_BASE/v1/webhooks" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-system.example.com/webhooks/agledger",
"eventTypes": ["mandate.created", "mandate.fulfilled"]
}'
{
"id": "019d3b10-03cb-...",
"url": "https://your-system.example.com/webhooks/agledger",
"eventTypes": ["mandate.created", "mandate.fulfilled"],
"isActive": true,
"secret": "a1b2c3d4...64-char-hex-signing-secret..."
}
Save the secret — you need it to verify payload authenticity.
Event types
| Event | When it fires |
|-------|--------------|
| mandate.created | New mandate created |
| mandate.registered | Enterprise approved |
| mandate.activated | Work may begin |
| mandate.fulfilled | Accountability loop closed |
| mandate.cancelled | Mandate cancelled |
| mandate.receipt_submitted | Receipt submitted |
| mandate.verification_complete | Verification engine completed |
Verify HMAC signatures
AGLedger signs every payload with HMAC-SHA256. The signature header uses Stripe-style format:
X-Agledger-Signature: t=1774812000,v1=abc123def456...
Python
import hmac, hashlib
def verify_webhook(payload_body: str, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
expected_hash = parts["v1"]
signing_input = f"{timestamp}.{payload_body}"
computed = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, expected_hash)
JavaScript
import crypto from 'node:crypto';
function verifyWebhook(payloadBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('=', 2))
);
const signingInput = `${parts.t}.${payloadBody}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signingInput)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed), Buffer.from(parts.v1)
);
}
Manage webhooks
# List all
curl "$API_BASE/v1/webhooks" -H "Authorization: Bearer $API_KEY"
# Test connectivity
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/ping" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" -d '{}'
# Pause during maintenance
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/pause" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
# Resume
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/resume" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
# Rotate secret (old secret remains valid briefly)
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/rotate" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
# Delete
curl -X DELETE "$API_BASE/v1/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $API_KEY"
Dead letter queue
Failed deliveries (after 3 retries at 1s, 10s, 60s backoff) go to the DLQ:
# List failed deliveries
curl "$API_BASE/v1/webhooks/$WEBHOOK_ID/dlq" \
-H "Authorization: Bearer $API_KEY"
# Retry a single delivery
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/dlq/$ENTRY_ID/retry" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
# Retry all
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/dlq/retry-all" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
Security
- HTTPS only — non-HTTPS URLs are rejected
- SSRF protection — internal/private IP addresses are rejected
- HMAC signatures — every payload is signed
- Secret rotation — zero-downtime key changes
- 10-second timeout — your endpoint must respond within 10 seconds
Validated with 18 assertions against the live API. View test source.