Webhooks and Event-Driven Integration
AGLedger delivers webhook events for mandate lifecycle transitions, agent-to-agent workflows, disputes, and federation activity. 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,
"isPaused": false,
"format": "standard",
"secret": "a1b2c3d4...64-char-hex-signing-secret...",
"secretGraceActive": false,
"secretGraceExpiresAt": null,
"circuitState": "closed",
"consecutiveFailures": 0,
"lastSuccessfulAt": null,
"createdAt": "2026-04-09T12:00:00.000Z"
}
Save the secret — it is returned only on creation and rotation. You need it to verify payload authenticity.
Response fields:
format—"standard"(default) or"cloudevents"(CloudEvents 1.0 envelope)circuitState—"closed"(healthy),"open"(failing, deliveries paused), or"half_open"(probing)consecutiveFailures— number of consecutive delivery failuressecretGraceActive— true when a previous secret is still valid after rotation
Event types
AGLedger supports 28 webhook event types across six categories.
Mandate lifecycle
| Event | When it fires |
|-------|--------------|
| mandate.created | New mandate created (includes registration and activation) |
| mandate.receipt_submitted | Receipt submitted against mandate |
| mandate.verification_complete | Verification engine completed check |
| mandate.fulfilled | Mandate settled (accountability loop closed) |
| mandate.settled | Deprecated alias for mandate.fulfilled — use mandate.fulfilled |
| mandate.failed | Mandate failed verification |
| mandate.expired | Mandate expired (deadline passed without fulfillment) |
| mandate.cancelled | Mandate cancelled |
Agent-to-agent
| Event | When it fires |
|-------|--------------|
| mandate.proposed | Agent proposed a new mandate to another agent |
| mandate.proposal_accepted | Proposed mandate accepted by target agent |
| mandate.proposal_rejected | Proposed mandate rejected by target agent |
| mandate.delegated | Mandate delegated to a sub-agent |
| mandate.revision_requested | Principal requested revision of a receipt |
Settlement and disputes
| Event | When it fires |
|-------|--------------|
| signal.emitted | Settlement signal emitted |
| dispute.opened | Dispute opened against a mandate |
| dispute.resolved | Dispute resolved |
Proxy ingestion
| Event | When it fires |
|-------|--------------|
| proxy.session.synced | Proxy session data synchronized |
| proxy.mandate.detected | Mandate detected from proxy session |
| proxy.mandate.formalized | Detected mandate formalized into the ledger |
Federation
| Event | When it fires |
|-------|--------------|
| federation.mandate.offered | Mandate offered to a federated instance |
| federation.mandate.accepted | Federated instance accepted a mandate |
| federation.mandate.state_changed | Federated mandate changed state |
| federation.settlement.signal | Settlement signal received from federated instance |
| federation.gateway.registered | Federation gateway registered |
| federation.gateway.revoked | Federation gateway access revoked |
Entity references
| Event | When it fires |
|-------|--------------|
| mandate.reference_added | External reference attached to a mandate |
| agent.reference_added | External reference attached to an agent |
Note:
mandate.registeredandmandate.activatedwere removed in v0.15.2 (F-329). These internal transitions are now absorbed into themandate.createdevent. If you previously subscribed to those types, update your subscriptions to usemandate.created.
Payload format
Each delivery includes the event type, mandate data (ID, status, contract type), timestamp, HMAC signature, and a monotonic eventSequence counter. The sequence counter is per-subscription and strictly increasing — use it to detect out-of-order or duplicate deliveries in HA environments.
Your endpoint must respond within 10 seconds. AGLedger retries 3 times (1s, 10s, 60s backoff) before moving to DLQ.
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)
);
}
Check delivery status
Poll the delivery log to verify your endpoint received events:
curl "$API_BASE/v1/webhooks/$WEBHOOK_ID/deliveries?limit=50" \
-H "Authorization: Bearer $API_KEY"
Each delivery record includes:
eventType— the event (e.g., "mandate.created")status— "PENDING", "DELIVERED", "FAILED", or "DEAD_LETTER"attemptNumber— current retry attempt (1-based)responseStatus— HTTP status code from your endpoint (null if connection failed)responseBody— truncated response body from your endpointsignature— HMAC-SHA256 signature header sent with the deliveryrequestBody— raw JSON body sent (the exact bytes signed by HMAC)nextRetryAt— scheduled time for next retry (null if terminal)createdAt— when the delivery was createddeliveredAt— when delivery was confirmed (null if not yet delivered)
Manage webhooks
# List all webhooks
curl "$API_BASE/v1/webhooks" -H "Authorization: Bearer $API_KEY"
# Get single webhook
curl "$API_BASE/v1/webhooks/$WEBHOOK_ID" -H "Authorization: Bearer $API_KEY"
# Update webhook (change URL or event types)
curl -X PATCH "$API_BASE/v1/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "eventTypes": ["mandate.created", "mandate.fulfilled", "dispute.opened"] }'
# Test connectivity
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/ping" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" -d '{}'
# Pause (stop deliveries 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 during grace period)
curl -X POST "$API_BASE/v1/webhooks/$WEBHOOK_ID/rotate" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{}'
# Delete (returns 204)
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/$DLQ_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 — AGLedger rejects non-HTTPS webhook URLs
- SSRF protection — internal/private IP addresses are rejected (169.254.x.x, 10.x.x.x, etc.)
- HMAC signatures — every payload is signed with your webhook secret
- Secret rotation — rotate secrets with a grace period for zero-downtime key changes
- Circuit breaker — automatic disabling after sustained failures; auto-disables on HTTP 410 (Gone) response
- Monotonic sequence — per-subscription
eventSequencecounter serialized via row-level locking, preventing duplicate sequence numbers in HA deployments
Validated against the live API (v0.19.10) on 2026-04-17.