eliDocs
API Reference

Webhooks

Subscribe to signed event notifications for jobs, quota, and subscription changes — set up an endpoint, verify signatures, and handle retries.

Beliq posts a signed JSON payload to your endpoint when something happens on your account — an API job finishes, you approach a quota limit, or your subscription changes. Each delivery is an HTTP POST carrying an HMAC signature you verify with the endpoint’s secret.

Setting up an endpoint

Webhook endpoints are managed in the dashboard under Webhooks. To add one:

  1. Enter the HTTPS URL that should receive deliveries.
  2. Choose which events to subscribe to.
  3. Beliq generates a signing secret (prefixed whsec_), shown once — copy it immediately and store it securely.

From the same page you can toggle an endpoint active or inactive, rotate its secret, and inspect the delivery log — every attempt, its status code, the response snippet, and any retries.

Endpoint URLs must be public https:// addresses. Private, loopback, and link-local targets are rejected.

The event envelope

Every delivery shares the same envelope. Only data changes by event type.

{
  "id": "evt_3f9a1c0b8d7e4f2a9b6c5d4e3f2a1b0c",
  "type": "job.completed",
  "created_at": "2026-06-21T10:21:58.000Z",
  "organization_id": "8b1d2c3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
  "livemode": true,
  "data": { }
}
Field Type Description
id string Event id, evt_ followed by 32 hex characters. Stable across retries — use it as your idempotency key.
type string The event type (see below).
created_at string ISO 8601 UTC timestamp of when the event was created.
organization_id string UUID of the organization the event belongs to.
livemode boolean true for production events, false for test events.
data object Event-specific payload.

Event types

Event Fires when data
job.completed A /v1/* job finishes successfully (2xx). JobData
job.failed A job fails due to an error on Beliq’s side. JobData
job.validation_failed A job produces error-level validation findings. Also sent alongside job.completed when a validate request returns valid: false. JobData
quota.threshold_reached Monthly usage crosses 80% of your plan limit. QuotaData
quota.exceeded Monthly usage reaches your plan limit. QuotaData
subscription.activated A plan becomes active. SubscriptionData
subscription.updated The plan changes (upgrade or downgrade). SubscriptionData
subscription.canceled The subscription is scheduled to end at the period end. SubscriptionData
subscription.ended Access to a paid plan ends. SubscriptionData
subscription.payment_failed A charge fails (dunning). SubscriptionData

Job data

{
  "job_id": "f2a1b0c9-d8e7-4f6a-5b4c-3d2e1f0a9b8c",
  "operation": "validate",
  "standard": "xrechnung",
  "profile": "xrechnung",
  "status": "completed",
  "input_format": "ubl",
  "output_format": null,
  "processing_ms": 142,
  "validation": {
    "valid": false,
    "error_count": 2,
    "warning_count": 1,
    "format": "ubl",
    "profile_detected": "xrechnung"
  }
}
Field Type Notes
job_id string Identifier for the job.
operation string generate, validate, parse, or convert.
standard string | null Target standard, when applicable.
profile string | null Target profile, when applicable.
status string completed or failed.
input_format string | null Detected or supplied input format.
output_format string | null Output format, when applicable.
processing_ms number | null Server-side processing time in milliseconds.
error_message string Present on job.failed only.
validation object Present when the job produced validation findings: valid, error_count, warning_count, and optional format / profile_detected.

Quota data

{
  "period": { "year": 2026, "month": 6 },
  "used": 8000,
  "limit": 10000,
  "threshold": 0.8
}
Field Type Notes
period object The calendar year and month (UTC) the usage applies to.
used number Requests consumed in the period.
limit number The plan’s monthly limit for the period.
threshold number Present on quota.threshold_reached only (0.8).

Subscription data

{
  "plan": "Growth",
  "interval": "annual",
  "period_start": "2026-06-01T00:00:00.000Z",
  "period_end": "2027-06-01T00:00:00.000Z",
  "currency": "EUR",
  "previous_plan": "Starter",
  "effective_at": "2026-06-21T10:21:58.000Z"
}
Field Type Notes
plan string | null Plan name after the event; null when no plan is active (for example on subscription.ended).
interval string | null monthly or annual.
period_start string | null ISO 8601 UTC start of the current billing period.
period_end string | null ISO 8601 UTC end of the current billing period.
currency string | null Billing currency.
cancel_at_period_end boolean Present on subscription.canceled.
previous_plan string | null Present on subscription.updated and subscription.ended.
effective_at string | null Present on subscription.updated — when the change takes effect (now for an upgrade, period end for a scheduled downgrade).

Transmission events (preview)

Subscribe to these to follow a delivery without polling. Each carries TransmissionData.

Event Fires when data
transmission.queued A document is validated and durably queued for delivery. TransmissionData
transmission.submitted A provider acknowledged the submission. TransmissionData
transmission.cleared The network confirmed technical acceptance and a proof was captured. TransmissionData
transmission.delivered Delivery to the recipient is confirmed. TransmissionData
transmission.rejected The network or recipient refused the document. TransmissionData
transmission.failed Delivery failed permanently after retries. TransmissionData
transmission.at_risk A delivery deadline is threatened or breached. TransmissionData
transmission.received An inbound document has been validated and is available to read. TransmissionData

Transmission data

{
  "transmission_id": "txn_9f3c2a1b8e",
  "direction": "outbound",
  "network": "peppol",
  "status": "delivered",
  "at_risk": false,
  "counterparty": { "scheme": "0208", "id": "9876543210" },
  "deadline_at": "2026-04-20T00:00:00.000Z"
}
Field Type Notes
transmission_id string The transmission this event is about.
direction string outbound or inbound.
network string The network the document travels on.
status string The status after this event. See delivery status.
at_risk boolean true when a delivery deadline is threatened or breached.
counterparty object | null The { scheme, id } at the other end.
deadline_at string | null ISO 8601 delivery deadline, when set.

Request headers

Header Value
X-Beliq-Signature t=<unix-seconds>,v1=<hmac-sha256 hex> — see Verifying signatures.
X-Beliq-Event The event type.
X-Beliq-Event-Id The event id (evt_…). Stable across retries; use it for idempotency.
X-Beliq-Delivery-Id Identifier for this individual delivery attempt record.
User-Agent beliq-webhooks/1.0
Content-Type application/json

Verifying signatures

Each request carries an X-Beliq-Signature header with a Unix timestamp and an HMAC-SHA256 signature computed over <timestamp>.<body> using your endpoint’s secret:

X-Beliq-Signature: t=1700000000,v1=4f3c...

Verify by parsing the header, recomputing the HMAC over the raw request body, comparing in constant time, and rejecting timestamps outside a freshness window (300 seconds is recommended):

verify-webhook.ts
import crypto from "node:crypto";

const FRESHNESS_TOLERANCE_SECONDS = 300;

export function verifyWebhookSignature(
  body: Buffer,
  header: string | undefined,
  secret: string,
): boolean {
  if (!header) return false;

  const match = header.match(/^t=(\d+),v1=([0-9a-f]+)$/);
  if (!match) return false;

  const timestamp = Number(match[1]);
  const provided = Buffer.from(match[2], "hex");

  const nowSeconds = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSeconds - timestamp) > FRESHNESS_TOLERANCE_SECONDS) return false;

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

  if (provided.length !== expected.length) return false;
  return crypto.timingSafeEqual(provided, expected);
}

The signed input is the raw request body — verify before any JSON parsing or normalization. The body must be the exact bytes the request carried; reserializing a parsed object produces a different HMAC.

Idempotency

Deduplicate on X-Beliq-Event-Id (the envelope id): store delivered ids and skip repeats. The id is the same across all retries of an event; the timestamp t is re-signed on each attempt, so never treat (id, t) as a uniqueness pair — dedup on the id alone.

Delivery and retries

Beliq attempts the first delivery immediately, then retries failures up to 7 attempts total with increasing backoff:

Attempt Delay after previous
1 immediate
2 1 minute
3 5 minutes
4 30 minutes
5 2 hours
6 8 hours
7 24 hours

After the final attempt the delivery is marked failed. Each attempt has a 10-second timeout.

  • 2xx — delivery succeeds.
  • 3xx — treated as a failure; redirects are not followed. Register the final URL directly.
  • 4xx / 5xx or a connection error — retried until the schedule is exhausted.
  • An inactive or removed endpoint stops delivery immediately.

Return a 2xx status as soon as you have accepted the event; do the work afterward. Inspect past deliveries and retry state in the dashboard delivery log.