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

# Webhooks

> Receive async validation results via HTTP webhooks

# Webhooks

Webhooks let you receive validation results asynchronously. Instead of blocking your application on a VIES, HMRC, or BFS lookup, submit the request and get the result delivered to your endpoint when it is ready.

Available on Pro and Business plans. Avatcado signs every webhook delivery with HMAC-SHA256 so you can verify it came from Avatcado.

## Setup

1. Configure your webhook URL in the [dashboard](https://avatcado.com/dashboard/webhooks) or via the API (`PUT /v1/webhooks/config`)
2. Your endpoint must accept HTTPS POST requests
3. Your endpoint should return 2xx within 10 seconds
4. Avatcado logs every delivery attempt. You can view delivery history and replay failed deliveries from the dashboard.

## Event types

| Event                  | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `validation.completed` | A single async validation finished successfully           |
| `validation.failed`    | A single async validation failed after all retry attempts |
| `batch.completed`      | All items in an async batch have been processed           |
| `test`                 | Sent when you click "Send test event" in the dashboard    |

## Event payloads

### validation.completed

```json theme={null}
{
  "event": "validation.completed",
  "data": {
    "valid": true,
    "vat_number": "DE123456789",
    "country_code": "DE",
    "company": {
      "name": "ACME GmbH",
      "address": "Berlin"
    },
    "consultation_number": null,
    "requested_at": "2026-03-30T12:00:00Z"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "cached": false,
    "source_status": "live"
  },
  "created_at": "2026-03-30T12:01:00Z"
}
```

### validation.failed

```json theme={null}
{
  "event": "validation.failed",
  "data": {
    "vat_number": "DE123456789"
  },
  "meta": {
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "error": {
    "code": "upstream_unavailable",
    "message": "VIES service unavailable after all retry attempts"
  },
  "created_at": "2026-03-30T12:01:00Z"
}
```

### batch.completed

```json theme={null}
{
  "event": "batch.completed",
  "data": {
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "summary": {
      "total": 197,
      "succeeded": 190,
      "failed": 7
    },
    "results": [
      {
        "data": {
          "valid": true,
          "vat_number": "DE123456789",
          "country_code": "DE",
          "company": { "name": "ACME GmbH", "address": "Berlin" },
          "requested_at": "2026-03-30T12:00:00Z"
        },
        "meta": {
          "cached": false,
          "source_status": "live"
        }
      },
      {
        "error": {
          "code": "upstream_unavailable",
          "message": "VIES service unavailable"
        },
        "meta": { "vat_number": "IT12345678901" }
      }
    ]
  },
  "created_at": "2026-03-30T12:01:00Z"
}
```

### test

```json theme={null}
{
  "event": "test",
  "data": {
    "message": "Webhook configuration is working correctly."
  },
  "created_at": "2026-03-30T12:00:00Z"
}
```

## Verifying signatures

Every delivery includes these headers:

* `X-Avatcado-Signature`: `sha256=<hex digest>`
* `X-Avatcado-Timestamp`: Unix timestamp (seconds)
* `X-Avatcado-Event`: event type
* `X-Avatcado-Delivery-Id`: unique delivery identifier

<Note>
  During the brand transition we also send the legacy `X-Vatly-*` headers with identical values, so existing
  integrations keep working unchanged. The legacy headers are deprecated and will be removed after 2026-07-01.
  Move your verification to the `X-Avatcado-*` headers before then.
</Note>

The signature is computed as HMAC-SHA256 of `{timestamp}.{body}` using your signing secret.

Always verify signatures before processing webhooks. Check the timestamp is within 5 minutes to prevent replay attacks.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createHmac } from 'crypto';

  function verifyWebhook(
    body: string,
    signature: string,
    timestamp: string,
    secret: string
  ): boolean {
    // Prevent replay attacks
    const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
    if (age > 300) return false; // Reject if older than 5 minutes

    const expected = createHmac('sha256', secret)
      .update(`${timestamp}.${body}`)
      .digest('hex');

    return signature === `sha256=${expected}`;
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_webhook(body: str, signature: str, timestamp: str, secret: str) -> bool:
      age = int(time.time()) - int(timestamp)
      if age > 300:
          return False

      expected = hmac.new(
          secret.encode(),
          f"{timestamp}.{body}".encode(),
          hashlib.sha256
      ).hexdigest()

      return signature == f"sha256={expected}"
  ```
</CodeGroup>

## Retry behavior

If your endpoint returns non-2xx or times out, the delivery is logged as failed. You can replay failed deliveries from the dashboard.

The underlying async validation itself is retried separately. If VIES or another upstream service is down, Avatcado retries the validation up to 5 times over several hours before marking it as failed and sending a `validation.failed` webhook.

Webhook delivery and validation retries are independent. A failed delivery does not trigger a re-validation.

## Best practices

* **Return 2xx quickly.** Do heavy processing in the background after acknowledging the webhook.
* **Verify signatures on every request.** Never trust unverified payloads.
* **Handle duplicate deliveries gracefully.** Replays send the same payload with a new delivery ID.
* **Store the `X-Avatcado-Delivery-Id` header** for debugging and deduplication.
