> ## Documentation Index
> Fetch the complete documentation index at: https://conto.finance/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for payment events, A2A payment requests, service spend records, and agent freeze events

# 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

1. Go to **Settings** > **Webhooks** in the dashboard
2. Enter your HTTPS endpoint URL
3. Copy the generated **signing secret**, you'll need this to verify payloads

<Warning>
  Webhook URLs must use HTTPS. HTTP, localhost, and private IP ranges are blocked for security (SSRF
  protection).
</Warning>

## 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        |

## Payload Format

Every webhook POST includes these headers and a JSON body:

### Headers

| 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

```json theme={null}
{
  "event": "payment.confirmed",
  "timestamp": "2026-04-07T12:00:00.000Z",
  "data": {
    "paymentId": "cmm5h0req000l49h7irxzh11p",
    "amount": 50,
    "transactionHash": "0xabc...",
    "transactionId": "cmm5g0txn000l49h7hqwyg11p",
    "organizationId": "cmm5i0org000l49h7jsyai11p",
    "agentId": "cmm5a7agt008t49h7bjqza99x"
  }
}
```

## Verifying Signatures

Every payload is signed with your webhook secret using HMAC-SHA256. Verify signatures to confirm the request came from Conto.

```typescript theme={null}
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.
