Skip to content

Understanding a webhook delivery

PointValue
MethodPOST
Headers sentHost, User-Agent: GuzzleHttp/7, Content-Type: application/json, Content-Length. Nothing else: no Accept, no signature, no event identifier, no site identifier
BodyCompact JSON, the object’s fields at the root, plus a changed key. No envelope, no metadata
Event identificationOnly through the URL being called. Declare a distinct URL per key
Timeouts10 seconds to establish the connection, 30 seconds for your response
Attempts5 in total, and only if the connection fails (DNS, refusal, timeout, invalid certificate): 30 seconds between the first three, then the last two in immediate succession
Response codesAll accepted without distinction. A 500 on your side is never retried
Guaranteed duplicateIf you respond in more than 30 seconds, the delivery is considered lost and retried up to four times, even though it has already reached you
RedirectsForbidden. On a 301, 302 or 303, the request is replayed as a GET, the payload is lost, and Ciklik records a misleading 200
TLS certificateValid and issued by a recognised authority. A self-signed certificate makes the delivery fail without leaving any trace
OrderingNot guaranteed. An updated_* may reach you before the created_* of the same object
FreshnessThe payload is re-read at the moment of delivery and may be more recent than the event. Only changed describes the moment of the event
Machine-readable contract/openapi/ciklik-webhooks.yaml
KeyObjectDelayDetails
created_subscriptionSubscription0 sDetails
updated_subscriptionSubscription0 sDetails
deleted_subscriptionSubscription0 sDetails
updated_change_choicesSubscription0 sDetails
created_shippingboxShipment0 sDetails
updated_shippingboxShipment0 sDetails
deleted_shippingboxShipment0 sDetails
created_checkoutorderOrder10 sDetails
updated_checkoutorderOrder0 sDetails
created_checkouttransactionTransaction10 sDetails
updated_checkouttransactionTransaction10 sDetails
created_checkoutinvoiceInvoice15 sDetails
created_userCustomer0 sDetails
updated_userCustomer0 sDetails
created_addressAddress0 sDetails
updated_addressAddress0 sDetails
created_optinMarketing opt-in10 sDetails
updated_optinMarketing opt-in0 sDetails

updated_change_choices is not one of the 17 API keys: it can only be enabled by the Ciklik team, see Configuring webhooks through the API.

  • No signature and no shared secret. The receiving URL is the only secret in the system.
  • No event name in the payload. Nothing tells you whether it is a created_subscription rather than an updated_user.
  • No delivery identifier and no event timestamp, so no idempotency key is provided.
  • No acknowledgement expected. The response code you return is not analysed.

The body consists of the fields of the object concerned, at the root, plus a changed key. The object depends on the event key: it is described field by field on the corresponding reference page (table above). Here is the simplest one in the catalogue, a created_optin, four fields and changed:

created_optin.json
{
"email": "claire.martin@example.com",
"tenant_id": 1,
"valid": true,
"list_id": "9f2c4e7a1b",
"changed": []
}

changed takes three forms depending on the type of event.

An empty JSON array, not an empty object:

{
"changed": []
}

Five rules to know before relying on changed:

  • These are raw database values, not the values as the API returns them elsewhere: dates in Y-m-d H:i:s format in UTC without a timezone, JSON columns as an encoded string, decimals as strings.
  • updated_at appears in changed on almost every modification; it is only missing when two writes fall within the same second.
  • Internal column names may differ from the field names exposed elsewhere in the payload.
  • A delivery may contain fields you did not modify yourself, because an internal process touched the same record at the same time.
  • Never base a business decision on the mere presence of a key in changed: check its value as well.
TypeFormatExample
DatesISO 8601 UTC with microseconds and a Z suffix; T00:00:00.000000Z for a date without a time2026-09-10T08:15:42.000000Z
Order amountsString, comma as decimal separator (total_tax_paid, total_discount_inc, total_paid, total_shipping_paid)"31,81"
Other amountsString, dot as decimal separator (revenue, customerRevenue, shipping)"31.81"
Line pricesFloating-point number (items[].price)29.9
Counters of the shipment objectString, even though the value is numeric (subscriptions_count, shipped_count, orderable_id at the root of the shipment; a plan’s or carrier’s shipped_count stays an integer)"3"

Four places in the payload are not a field contract and must not be relied upon beyond what follows:

  • items[].orderable: internal reflection of the product or plan, not contractual. Only use id, name, price and ref.
  • transporter.relayOptions: content varies by carrier, not contractual, to be ignored.
  • invoice.customer: snapshot of the customer’s details when the invoice was issued, free-form structure.
  • invoice.company: snapshot of your company’s details when the invoice was issued, free-form structure.

The delay depends on the key: 0, 10 or 15 seconds (table of the 18 keys). This is not a freshness guarantee, quite the opposite: the payload is re-read when the delivery is sent, not when the event happens. In between, the object may have been modified again. Only changed faithfully describes the moment of the event; the rest of the body reflects the most recent state known at the time of delivery.

No ordering is guaranteed: delays differ by key and several deliveries may be processed in parallel, so an updated_* may arrive before the created_* of the same object. Duplicates are frequent and normal: successive writes on the same object, cascades between subscription, shipments and orders, or a response too slow that triggers a new delivery even though the first one arrived properly. Treat these cases as the norm, see Best practices and troubleshooting for how to handle them.

Step by step:

event in your store (creation, modification or deletion)
-> resolution of the declared URL (cached for 60 s)
-> queued, with a delay of 0, 10 or 15 s depending on the key
-> POST to your receiving URL
connection: 10 s max, reading your response: 30 s max
-> connection failure (DNS, refusal, timeout, certificate)?
yes -> new attempt, up to 5 in total: 30 s between the first three, then the last two in immediate succession
no -> response code logged as is, no further attempt
-> line logged in the trace kept for 20 days (except on connection failure)
  • The mechanism and the payload of deliveries are exactly the same as for a standard Ciklik store.
  • Subscriptions additionally carry the external_fingerprint field, and orders the prestashop_order_id field.
  • An order moving to a withdrawal request is never notified: this is intentional on the PrestaShop side.
  • The PrestaShop module has its own synchronisation channel, separate from these merchant webhooks. Do not confuse the two.
  • An API token is created automatically for PrestaShop sites, see Configuring webhooks through the API.

See also the PrestaShop section.

The /openapi/ciklik-webhooks.yaml file describes the 18 keys, the three configuration operations and one real example per key. Load it into an API client, generate types from it, or hand it to a coding assistant so it writes your receiving endpoint.