---
title: "Understanding a webhook delivery"
description: "The technical contract of a Ciklik webhook delivery: request, payload, changed field, formats, timeouts, retries and the PrestaShop case."
url: https://www.ciklik.academy/en/webhooks/comprendre-un-envoi-de-webhook/
lang: en
lastUpdated: 2026-10-23
---

# Understanding a webhook delivery

> Box and PrestaShop
>
> This page applies to both box stores and PrestaShop stores.

> For developers
>
> The complete technical contract of a Ciklik delivery: the request, its payload, how to identify the event, when it is retried. Everything you need to write your receiving URL without reading another page.

## 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, redirect loops): 30 seconds before the 2nd, 3rd and 4th attempts, then the 5th immediately after |
| 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`](https://www.ciklik.academy/openapi/ciklik-webhooks.yaml)                                                                                                 |

## The 18 event keys

| Key                           | Object           | Delay | Details                                                                                                                   |
| ----------------------------- | ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------- |
| `created_subscription`        | Subscription     | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-abonnements/#created_subscription)                            |
| `updated_subscription`        | Subscription     | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-abonnements/#updated_subscription)                            |
| `deleted_subscription`        | Subscription     | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-abonnements/#deleted_subscription)                            |
| `updated_change_choices`      | Subscription     | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-abonnements/#updated_change_choices)                          |
| `created_shippingbox`         | Shipment         | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-expeditions/#created_shippingbox)                             |
| `updated_shippingbox`         | Shipment         | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-expeditions/#updated_shippingbox)                             |
| `deleted_shippingbox`         | Shipment         | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-expeditions/#deleted_shippingbox)                             |
| `created_checkoutorder`       | Order            | 10 s  | [Details](https://www.ciklik.academy/en/webhooks/evenements-commandes-transactions-factures/#created_checkoutorder)       |
| `updated_checkoutorder`       | Order            | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-commandes-transactions-factures/#updated_checkoutorder)       |
| `created_checkouttransaction` | Transaction      | 10 s  | [Details](https://www.ciklik.academy/en/webhooks/evenements-commandes-transactions-factures/#created_checkouttransaction) |
| `updated_checkouttransaction` | Transaction      | 10 s  | [Details](https://www.ciklik.academy/en/webhooks/evenements-commandes-transactions-factures/#updated_checkouttransaction) |
| `created_checkoutinvoice`     | Invoice          | 15 s  | [Details](https://www.ciklik.academy/en/webhooks/evenements-commandes-transactions-factures/#created_checkoutinvoice)     |
| `created_user`                | Customer         | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#created_user)                        |
| `updated_user`                | Customer         | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#updated_user)                        |
| `created_address`             | Address          | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#created_address)                     |
| `updated_address`             | Address          | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#updated_address)                     |
| `created_optin`               | Marketing opt-in | 10 s  | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#created_optin)                       |
| `updated_optin`               | Marketing opt-in | 0 s   | [Details](https://www.ciklik.academy/en/webhooks/evenements-clients-adresses-optins/#updated_optin)                       |

`updated_change_choices` is not one of the 17 API keys: add it in the **Webhooks** field of your site’s **Advanced** tab, see [Configuring webhooks through the API](https://www.ciklik.academy/en/webhooks/configurer-les-webhooks-par-lapi/).

## 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_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 event is not in the body
>
> Your only way to identify the event is the declared URL. Use a distinct URL per key, for example `/webhooks/ciklik/created-subscription`, rather than a single URL receiving everything.

## 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`:

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

## The changed field

`changed` takes three forms depending on the type of event.

**Creation :**

An empty JSON array, not an empty object:

```json
{
    "changed": []
}
```

**Update :**

A `{column: new value}` object, real fragment of `updated_subscription`:

```json
{
    "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"
    }
}
```

**Deletion :**

```json
{
    "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: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.

## 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, comma as thousands separator from 1,000 (`revenue`, `customerRevenue`, `shipping`)                                                              | `"31.81"`, `"1,250.00"`       |
| 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"`                         |

> Mind the interpretation
>
> Several amount formats coexist within a single order. Normalise each value on the receiving side, and never add two fields together without converting them to the same format.

## 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 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.

## 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](https://www.ciklik.academy/en/webhooks/bonnes-pratiques-et-depannage/) for how to handle them.

## 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 before the 2nd, 3rd and 4th, then the 5th immediately after
       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

- The mechanism and the payload of deliveries are exactly the same as for a standard Ciklik store.
- The `external_fingerprint` (subscriptions) and `prestashop_order_id` (orders) fields are filled in; on a standard Ciklik store, they are `null`.
- An order moving to a withdrawal request is **never** notified, whatever the store: this transient status is unknown to PrestaShop.
- 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](https://www.ciklik.academy/en/webhooks/configurer-les-webhooks-par-lapi/).

See also the [PrestaShop](https://www.ciklik.academy/en/prestashop/) section.

## Machine-readable contract

The [`/openapi/ciklik-webhooks.yaml`](https://www.ciklik.academy/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.

## See also

[Activating and testing a webhook](https://www.ciklik.academy/en/webhooks/activer-et-tester-un-webhook/)

[Configuring webhooks through the API](https://www.ciklik.academy/en/webhooks/configurer-les-webhooks-par-lapi/)

[Best practices and troubleshooting](https://www.ciklik.academy/en/webhooks/bonnes-pratiques-et-depannage/)

[Webhooks](https://www.ciklik.academy/en/webhooks/)
