Webhooks
Conto sends HTTP POST requests to your configured URL when payments progress, an A2A payment request is created, a service spend record is written, or an agent is frozen or unfrozen.
Setup
- Go to Settings > Webhooks in the dashboard
- Enter your HTTPS endpoint URL
- Copy the generated signing secret — you’ll need this to verify payloads
Webhook URLs must use HTTPS. HTTP, localhost, and private IP ranges are blocked for security (SSRF
protection).
Event Types
| Event | Fires when |
|---|
payment.requested | A payment request is created |
payment.approved | A payment passes policy evaluation |
payment.denied | A payment is blocked by policy |
payment.executed | A payment transaction is submitted onchain |
payment.confirmed | A payment is confirmed onchain |
payment.failed | A payment transaction fails |
payment_request.created | An agent-to-agent payment request is created |
service_payment.recorded | An x402 or MPP transaction is recorded |
service_payment.failed | An x402 or MPP transaction fails |
agent.frozen | An agent is frozen |
agent.unfrozen | An agent is restored to active status |
Every webhook POST includes these headers and a JSON body:
| Header | Description |
|---|
X-Conto-Event | Event type (e.g., payment.confirmed) |
X-Conto-Timestamp | ISO 8601 timestamp of the event |
X-Conto-Signature | HMAC-SHA256 signature of the body |
X-Conto-Delivery-Attempt | Attempt number (1, 2, or 3) |
Body
{
"event": "payment.confirmed",
"timestamp": "2026-04-07T12:00:00.000Z",
"data": {
"paymentId": "req_abc123",
"amount": 50,
"transactionHash": "0xabc...",
"transactionId": "tx_xyz",
"organizationId": "org_123",
"agentId": "agent_456"
}
}
Verifying Signatures
Every payload is signed with your webhook secret using HMAC-SHA256. Verify signatures to confirm the request came from Conto.
import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(rawBody: string, signature: string | undefined, secret: string): boolean {
if (!signature) return false;
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const signatureBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expected, 'hex');
if (signatureBuffer.length !== expectedBuffer.length) {
return false;
}
return timingSafeEqual(signatureBuffer, expectedBuffer);
}
// In your handler
app.post('/webhooks/conto', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('x-conto-signature');
const rawBody = req.body.toString('utf8');
if (!verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const { event, data } = JSON.parse(rawBody);
switch (event) {
case 'payment.confirmed':
console.log('Payment confirmed:', data.transactionHash);
break;
case 'payment.denied':
console.log('Payment denied:', data.reasons);
break;
}
res.status(200).send('OK');
});
The same rule applies in other frameworks: verify the exact raw request body before you parse it.
Retry Behavior
Failed deliveries are retried up to 3 times with exponential backoff:
| Attempt | Timing |
|---|
| 1 | Immediate |
| 2 | ~1 second later |
| 3 | ~3 seconds after the initial delivery |
A delivery is considered failed if your endpoint returns a non-2xx status code or doesn’t respond within 10 seconds.
Agent Callback URLs
In addition to organization-level webhooks, individual agents can have a callbackUrl set during creation. When set, webhooks are delivered to both the organization URL and the agent’s callback URL.
Delivery Targets
| Target | Configured via | Receives events for |
|---|
| Organization webhook | Settings > Webhooks | All events in the org |
| Agent callback URL | Agent config | Events for that agent only |
Both targets receive the same payload format and signature headers.