Receiving events with webhooks
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 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.
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"])
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:
- verifies the raw body and timestamp;
- records the Event ID exactly once;
- returns
2xxpromptly; - retrieves the Event or related invoice asynchronously;
- 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 for the event envelope and the Webhooks reference for destination fields and delivery rules.