# Receiving events with webhooks

Receive signed invoice and compliance updates without polling the API.

Webhooks notify your integration after something important happens in a Live or Test account. They are especially useful for asynchronous Tax ID verification, VERI*FACTU outcomes, and balance movements, but can also report customer, invoice, amendment and PDF activity.

## Create a Test destination

Create the destination with a Test API key so experiments cannot mix with Live events. Subscribe only to events your integration understands; use `"*"` only when you deliberately want every current and future event type.

### cURL

```bash
curl https://api.fiscalrail.com/v1/event-destinations \
  --request POST \
  --header "Authorization: Bearer ak_test_..." \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Invoice updates",
    "url": "https://example.com/webhooks/fiscalrail",
    "enabled_events": [
      "invoice.amended",
      "invoice.verifactu_registration.accepted",
      "invoice.verifactu_registration.accepted_with_errors",
      "invoice.verifactu_registration.rejected"
    ]
  }'
```

The response contains a `whsec_...` signing secret. Store it securely. FiscalRail can return it when retrieving that destination, but list responses omit it.

Your endpoint must use public HTTPS. For local development, expose the handler through a trusted HTTPS tunnel and replace the destination URL when it changes.

## Verify before parsing

FiscalRail signs the exact raw request body and sends the result in `FiscalRail-Signature`. Verify the signature before parsing JSON or starting work.

### Python

```python
import hashlib
import hmac
import time


def verify_fiscalrail_signature(raw_body, header, secret):
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts["t"])
    if abs(time.time() - timestamp) > 300:
        return False

    signed = str(timestamp).encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

### Ruby

```ruby
require "openssl"

def valid_fiscalrail_signature?(raw_body, header, secret)
  parts = header.split(",").to_h { _1.split("=", 2) }
  timestamp = Integer(parts.fetch("t"))
  return false if (Time.now.to_i - timestamp).abs > 300

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  OpenSSL.fixed_length_secure_compare(expected, parts.fetch("v1"))
rescue KeyError, ArgumentError
  false
end
```

The signature covers `timestamp + "." + raw_body` using HMAC-SHA256. Reject timestamps more than five minutes old and compare signatures in constant time. Every retry receives a new timestamp and signature.

## Acknowledge quickly

Return any `2xx` response as soon as the event has been authenticated and durably queued. Do slow work after acknowledging it.

Deliveries are at least once and are not ordered. Store the Event ID before processing and ignore IDs you have already handled. Do not assume `invoice.issued` arrives before `invoice.amended`, or that a VERI*FACTU result arrives immediately after issuance.

Webhook payloads are intentionally thin. They identify the Event and related resource but omit the immutable `data` snapshot. Retrieve the Event by ID when you need the historical snapshot, or retrieve the related resource when you need its current state.

Subscribe a Live destination to `billing.balance_transaction.created` to
reconcile usage debits and top-up credits. Retrieve the full Event to read the
immutable Balance Transaction snapshot. A usage transaction's `source_event`
identifies the successful domain Event that caused the debit.

## Exercise the complete flow

Issue or correct an invoice in the same Test account. Your endpoint should receive the subscribed event. Confirm that your handler:

1. verifies the raw body and timestamp;
2. records the Event ID exactly once;
3. returns `2xx` promptly;
4. retrieves the Event or related invoice asynchronously;
5. safely ignores event types it does not understand.

FiscalRail attempts a failed delivery up to five times. If every attempted delivery keeps failing for 24 hours, the destination is disabled. Fix the endpoint and enable the destination again through the API or dashboard.

See the [Events reference](/en/docs/api/events) for the event envelope and the [Webhooks reference](/en/docs/api/event-destinations) for destination fields and delivery rules.
