Understanding a webhook delivery
The contract at a glance
Section titled “The contract at a glance”| Point | Value |
|---|---|
| Method | POST |
| Headers sent | Host, User-Agent: GuzzleHttp/7, Content-Type: application/json, Content-Length. Nothing else: no Accept, no signature, no event identifier, no site identifier |
| Body | Compact JSON, the object’s fields at the root, plus a changed key. No envelope, no metadata |
| Event identification | Only through the URL being called. Declare a distinct URL per key |
| Timeouts | 10 seconds to establish the connection, 30 seconds for your response |
| Attempts | 5 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 codes | All accepted without distinction. A 500 on your side is never retried |
| Guaranteed duplicate | If 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 |
| Redirects | Forbidden. On a 301, 302 or 303, the request is replayed as a GET, the payload is lost, and Ciklik records a misleading 200 |
| TLS certificate | Valid and issued by a recognised authority. A self-signed certificate makes the delivery fail without leaving any trace |
| Ordering | Not guaranteed. An updated_* may reach you before the created_* of the same object |
| Freshness | The 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 |
The 18 event keys
Section titled “The 18 event keys”| Key | Object | Delay | Details |
|---|---|---|---|
created_subscription | Subscription | 0 s | Details |
updated_subscription | Subscription | 0 s | Details |
deleted_subscription | Subscription | 0 s | Details |
updated_change_choices | Subscription | 0 s | Details |
created_shippingbox | Shipment | 0 s | Details |
updated_shippingbox | Shipment | 0 s | Details |
deleted_shippingbox | Shipment | 0 s | Details |
created_checkoutorder | Order | 10 s | Details |
updated_checkoutorder | Order | 0 s | Details |
created_checkouttransaction | Transaction | 10 s | Details |
updated_checkouttransaction | Transaction | 10 s | Details |
created_checkoutinvoice | Invoice | 15 s | Details |
created_user | Customer | 0 s | Details |
updated_user | Customer | 0 s | Details |
created_address | Address | 0 s | Details |
updated_address | Address | 0 s | Details |
created_optin | Marketing opt-in | 10 s | Details |
updated_optin | Marketing opt-in | 0 s | Details |
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.
What Ciklik does not send
Section titled “What Ciklik does not send”- 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_subscriptionrather than anupdated_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 delivery payload
Section titled “The delivery payload”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:
{ "email": "claire.martin@example.com", "tenant_id": 1, "valid": true, "list_id": "9f2c4e7a1b", "changed": []}The changed field
Section titled “The changed field”changed takes three forms depending on the type of event.
An empty JSON array, not an empty object:
{ "changed": []}A {column: new value} object, real fragment of updated_subscription:
{ "changed": { "next_billing": "2026-11-10 08:15:42", "end_date": "2026-11-09 21:59:59", "updated_at": "2026-09-10T08:15:42.000000Z" }}{ "changed": { "deleted": true }}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:sformat in UTC without a timezone, JSON columns as an encoded string, decimals as strings. updated_atappears inchangedon 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.
Data formats
Section titled “Data formats”| Type | Format | Example |
|---|---|---|
| Dates | ISO 8601 UTC with microseconds and a Z suffix; T00:00:00.000000Z for a date without a time | 2026-09-10T08:15:42.000000Z |
| Order amounts | String, comma as decimal separator (total_tax_paid, total_discount_inc, total_paid, total_shipping_paid) | "31,81" |
| Other amounts | String, dot as decimal separator (revenue, customerRevenue, shipping) | "31.81" |
| Line prices | Floating-point number (items[].price) | 29.9 |
| Counters of the shipment object | String, 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" |
Free-form objects
Section titled “Free-form objects”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 useid,name,priceandref.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.
Timing, ordering and duplicates
Section titled “Timing, ordering and duplicates”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.
Delivery lifecycle
Section titled “Delivery lifecycle”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)If your store runs PrestaShop
Section titled “If your store runs PrestaShop”- The mechanism and the payload of deliveries are exactly the same as for a standard Ciklik store.
- Subscriptions additionally carry the
external_fingerprintfield, and orders theprestashop_order_idfield. - 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.
Machine-readable contract
Section titled “Machine-readable contract”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.