Webhooks overview

Events a shop can subscribe to, and how GreenLeaze delivers, signs and retries them.

Event types

A shop can subscribe to the following event types. Each has its own page with the full payload.

Event typeFired whenPayload family
order_createdAn order is createdOrder created
order_confirmedAn order is validated, or a booking deposit is paidOrder (full)
order_bookedA booking deposit is paidOrder (full) + booking
order_booking_cancelledA booking is cancelled and its deposit refundedOrder (full) + booking + cancellation
order_cancelledAn order is cancelledOrder cancelled (shop)
order_archivedA non-active order is archivedOrder (full) + archivedAt
order_unarchivedAn archived order is restoredOrder (full) + archivedAt
order_abandonedThe abandoned-cart reminder is sent for a draft orderOrder abandoned
subscription_shop_status_changedThe shop-facing status of an order changesShop status changed
invoice_upload_failedAn invoice declared through the API never received a usable PDFInvoice upload failed

A shop receives an event from three kinds of destination:

  1. Webhook subscriptions. Subscriptions scoped to the shop, created from the dashboard. Each subscription = one event type + one URL + one secret (secureToken).
  2. External-app destinations. An active external app (Shopify middleware, PrestaShop, WooCommerce…) whose configuration holds webhookValidateOrderUrl receives order_confirmed, and one with webhookCancelBookingUrl receives order_booking_cancelled. These have no webhook subscription: the app configuration is the subscription.
  3. LEGACY -~~ Order URLs.~~~~ The urls.success given when the order was created receives the order_confirmed payload (see order success URL). The other order URLs and the discount-code URL are request/response callbacks, documented in shop integration callbacks. ~~

Delivery

Every event listed above goes out through the GreenLeaze webhook gateway.

Request

POST <destination url>
Content-Type: application/json
User-Agent: GreenLeaze-Webhook-Gateway/1.0
X-Webhook-Delivery-Id: <uuid>
X-Webhook-Signature: <base64 HMAC-SHA256 of the raw body>   (only when a secret is set)

<JSON payload>
HeaderMeaning
X-Webhook-Delivery-IdGateway delivery id. Stable across the retries of one delivery, so use it to deduplicate.
X-Webhook-Signaturebase64(HMAC_SHA256(secret, rawBody)). Sent only when the destination has a secret (see below).

The event type is in neither the body nor the headers. A destination knows what it receives from its subscription: register one URL per event type, or put the event type in the URL (path or query string).

Signature

DestinationSecret used
Webhook subscriptionIts secureToken (required for shops)
Webhook subscription linked to an appIts secureToken, else the app's webhookSecret
External-app destinationThe app's configuration.webhookSecret
Order success URLThe shop's API key

Sign the raw request bytes. Re-serialising parsed JSON can reorder keys or change number formatting and break the comparison.

import { createHmac, timingSafeEqual } from 'crypto';

export function isGreenLeazeSignatureValid(params: {
  rawBody: Buffer;
  signature: string | undefined;
  secret: string;
}): boolean {
  if (!params.signature) return false;
  const expected = createHmac('sha256', params.secret)
    .update(params.rawBody)
    .digest();
  const received = Buffer.from(params.signature, 'base64');
  return (
    received.length === expected.length && timingSafeEqual(received, expected)
  );
}

Responses and retries

Destination answersGateway does
2xxSuccess. The body (first part) is kept in the delivery log.
429, 5xx, timeout (25 s), network errorRetries with exponential backoff: 5 s, 10 s, 20 s, 40 s…
Any other 4xxGives up immediately (dead). A retry would not fix it.
  • The attempt count is the destination's maxAttempts which is 8 by default.
  • Delivery is at least once: deduplicate on X-Webhook-Delivery-Id.
  • A delivery replayed from the dashboard ("relancer") is a new delivery with a new X-Webhook-Delivery-Id. Make the handler idempotent on business keys as well, for example id + event type.
  • Delivery logs are visible in your GreenLeaze Dashboard

Which destinations an event reaches

GreenLeaze selects the active webhook subscriptions of the event's shop, then filters them on the order's origin, read from the payload's source field. External-app destinations are not filtered this way (see order_confirmed for their own rule).

source in the payloadOnly destinations with
"dashboard"runsForDashboardSubscription = true
any other non-empty valuerunsForApiSubscription = true
null / absentno filter
⚠️

order_cancelled sets source to the cancellation origin (shop,

greenleaze, …), never dashboard. A destination with runsForApiSubscription = false therefore never receives order_cancelled, even for an order created from the dashboard.

Delayed and held deliveries

  • LEGACY - Order success URL. A webhook_delay configured for the shop and the order's source turns the call into a scheduled delivery, sent after delayInMinutes.
  • Held until the manual-validation decision. An external-app destination whose app has delayWebhookUntilAutoApproval receives order_confirmed late when a manual validation is open on the order. The delivery is scheduled for the auto-approval time + 24 h, released at once if the order is approved, and cancelled if the order is rejected.

Conventions

ConventionRule
DatesISO 8601 strings in UTC, e.g. "2026-04-20T10:00:00.000Z".
Absent vs nullA field marked optional is left out of the JSON when it has no value. null means the value exists and is empty.
AmountsEuros, as JSON numbers with up to two decimals. TTC = VAT included, HT = VAT excluded. One exception: deliveryMode.price is a string (unfortunatly legacy).
VATvat is a percentage (20 = 20 %).
IdentifiersGreenLeaze ids are UUIDs. In products[], id / combinationId are the shop's product and variant ids, and greenleazeId is the GreenLeaze order line id.
Order keysid is the GreenLeaze order (subscription) id. transactionId is the customer-facing order reference. cartId / cartSecureKey are the values the shop gave when it created the order.
Unknown fieldsIgnore them. Fields are added without notice; only documented fields are maintained and considered as a change if removed or renamed

Order status

draft · deposit_paid · validating · active · inactive · canceled · canceled_with_fees · finished

Order shopStatus

not-opened · opened · account-selected · pending-additional-information · rejected · bank-selected · bank-connected · pending-company-validation · draft · canceled · finished · payment-method-entered · adresses-entered · customer-created · accepted-pending-customer-validation · active

An order that enters validation (status: "validating") is already shopStatus: "active": product decision, shops see the order as active from that point.

Trust the webhook event type then the shopStatus

Do not check status field. ShopStatus will contain the data if the order is confirmed or not. But first check the event (event order_confirmed = don't look to shopStatus, the order is confirmed)