Webhooks
Conto sends HTTP POST requests to your configured URL when events occur — payments requested, approved, denied, executed, confirmed, or failed.
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 |
service_payment.recorded | An x402 or MPP transaction is recorded |
service_payment.failed | An x402 or MPP transaction fails |
agent.created | A new agent is registered |
agent.updated | An agent’s status or config changes |
agent.paused | An agent is paused |
agent.resumed | An agent is resumed from paused state |
wallet.funded | A wallet receives funds |
wallet.low_balance | A wallet balance drops below threshold |
alert.created | A new alert is generated |
policy.violated | A policy violation is detected |
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 { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(body).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// In your handler
app.post('/webhooks/conto', (req, res) => {
const signature = req.headers['x-conto-signature'];
const isValid = verifyWebhook(JSON.stringify(req.body), signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
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');
});
Retry Behavior
Failed deliveries are retried up to 3 times with exponential backoff:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | ~1 second |
| 3 | ~4 seconds |
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.