> For the complete documentation index, see [llms.txt](https://docs.akinon.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.akinon.com/technical-guides/commerce/commerce-webhooks.md).

# Commerce Webhooks

A webhook is a message that Commerce sends to a web address you own, the moment something happens: a customer registers, signs in, or a campaign changes. Instead of asking Commerce for changes on a schedule, you publish one address and Commerce calls it.

Setting one up takes three steps, and this page walks through each of them:

1. **Subscribe** to the event you care about, giving Commerce the address to call.
2. **Receive** the message on that address, check that it really came from Akinon, and answer quickly.
3. **Confirm** afterwards, from your own logs and from the delivery records Akinon keeps.

Every webhook is an HTTP `POST` whose body always has the same two parts: an `event` object saying what happened, and a `payload` object carrying the record it happened to:

```json
{
  "event": {"type": "user_registered", "uuid": "c9591f0d1c1d4da58ade880921bd7e93"},
  "payload": { "...": "..." }
}
```

Before you start you will need an API token, and an address that accepts `POST` requests from the public internet.

## <mark style="color:red;">**dj-whisperer**</mark>

The **dj-whisperer** package, developed within Akinon, has been released as an open-source project and is available on PyPI:

* **PyPI Package:** [dj-whisperer](https://pypi.org/project/dj-whisperer/)
* **Documentation:** [dj-whisperer Docs](https://dj-whisperer.readthedocs.io/en/stable/)

Through **dj-whisperer**, you can subscribe to various webhook events in the Commerce system. The official package documentation provides in-depth details on how to configure and utilize webhooks efficiently.

## <mark style="color:red;">**Subscribing to an Event**</mark>

A subscription is made of two pieces, and it is worth knowing what each one decides:

* An **event message definition** turns the event on and chooses how it is delivered. Nothing is sent for an event that has no definition.
* A **hook** describes one destination for queued delivery: where the message goes, how it is signed, which headers it carries, and how long to wait between retries.

Immediate delivery needs only a definition, which carries the address itself. Queued delivery needs both: the definition to turn the event on, and a hook to say where it goes.

<table><thead><tr><th width="230">Decided by</th><th>Immediate</th><th>Queued</th></tr></thead><tbody><tr><td>Whether the event is sent at all</td><td>The definition</td><td>The definition</td></tr><tr><td>Where the message goes</td><td>The definition's <code>url</code></td><td>Each active hook's <code>target_url</code></td></tr><tr><td>How the request proves itself</td><td>The definition's <code>authentication_config</code></td><td>The hook's <code>secret_key</code> signature and <code>additional_headers</code></td></tr><tr><td>Retries</td><td>None</td><td>The hook's <code>retry_countdown_config</code></td></tr><tr><td>Delivery records you can read later</td><td>Not kept</td><td>Kept, in <code>hook_events</code></td></tr></tbody></table>

Both pieces can exist more than once for the same event, and each one is handled on its own: every definition for an event is acted on when it fires, and in queued mode every active hook for that event name receives its own copy with its own delivery record. That is how you feed two systems from one event. It is also why a duplicate definition or a forgotten hook means the same event arriving twice.

Start with the definition. The event names are listed in [Available Commerce Webhook Events](#available-commerce-webhook-events) at the end of this page.

```bash
curl --location 'https://{commerce_url}/api/v1/event_messages/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{
    "event_name": "user_registered",
    "description": "Description",
    "transactional": true,
    "asynchronous": false,
    "url": "http://localhost:8000/dummy/",
    "authentication_config": {
        "auth_type": "basic",
        "username": "username",
        "password": "password"
    }
}'
```

{% hint style="info" %}
The examples on this page call Commerce directly, at `https://{commerce_url}/api/v1/...`. The same endpoints are also reachable through Omnitron, at `https://{omnitron_url}/api/v1/remote/{channel_id}/...`, where `{channel_id}` is the ID of the sales channel you are working on.
{% endhint %}

**Parameters:**

* `event_name`: One of the predefined Commerce event names.
* `description`: A free-text note describing what this definition is for.
* `url`: The address the event will be sent to.
* `authentication_config`: Credentials for that address, if it requires them. Basic authentication is supported.
* `asynchronous`: Chooses the delivery mode (see below).
* `transactional`: Only applies in immediate mode (see below).

### <mark style="color:red;">Choosing a delivery mode</mark> <a href="#choosing-a-delivery-mode" id="choosing-a-delivery-mode"></a>

**Immediate (`asynchronous: false`)** sends the message during the operation that caused it, while the change is still being written. It is the fastest possible notice, and it is the right choice when all you need is the trigger.

Three things follow from sending it that early:

* The payload shows the record as it stands at that instant. Anything written a moment later in the same operation, such as related records and lists that are filled in afterwards, is not in it yet.
* The message is sent once. The delivery is judged on whether the request reached your address at all; the answer your endpoint gives back is not examined, and nothing is re-sent.
* The message is not signed. An immediate message carries no `X-Whisperer-Signature` header, because the signature belongs to the hook and an immediate message is sent without one. Authenticate that address with the `authentication_config` credentials instead, and do not expect a signature to verify.

With `transactional: true`, an address that cannot be reached at all (a wrong host, a refused connection, no answer in time) cancels the operation that caused the event. A customer registration would fail, for example. Use it only when that is genuinely what you want.

**Queued (`asynchronous: true`)** hands the message to a background queue instead. It goes out once the change has been stored completely, so the payload is whole, and it is retried if your endpoint is unavailable. This is the right default for most integrations.

Queued delivery takes its destination from the hook rather than from the definition's `url`, so register a hook for the same event name alongside the definition:

```bash
curl --location 'https://{commerce_url}/api/v1/whisperer/hooks/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{
    "event_type": "user_registered",
    "target_url": "https://example.com/hooks/user-registered/",
    "secret_key": "a-long-random-string",
    "retry_countdown_config": {
        "choice": "exponential",
        "kwargs": {"base": 2, "factor": 60, "limit": 3600}
    },
    "additional_headers": {}
}'
```

* `event_type`: The same event name as in the definition.
* `target_url`: The address the event will be sent to.
* `secret_key`: A random string that you choose and keep private. Akinon does not issue it and never sends it in a request; it is stored on the hook and used to sign every message, so you can prove the request came from Akinon. Optional, but strongly recommended. See [Confirming a Delivery](#confirming-a-delivery).
* `retry_countdown_config`: How long to wait between retries. See [Setting the retry interval](#setting-the-retry-interval).
* `additional_headers`: Any extra headers your address needs, such as an authorization token.
* `config`: Optional. Add `{"auth": {"auth_type": "basic", "username": "…", "password": "…"}}` when your address expects a username and password rather than a header.

A successful registration answers `201` with the stored hook, including the `id` you will need to change it later. Two answers mean the hook was not created:

* `400` with `"… is not a valid choice."`: the `event_type` is not a Commerce event name. The API can list the valid ones for you:

```bash
curl --location 'https://{commerce_url}/api/v1/whisperer/hooks/registry/' \
--header 'Authorization: Token {token}'
```

* `406` with `webhook_100_2`: you already have a hook for this event name and this address. One address can be registered once per event; use the existing hook, or point the new one somewhere else.

### <mark style="color:red;">Managing your hooks</mark> <a href="#managing-your-hooks" id="managing-your-hooks"></a>

```bash
# All your hooks; filter by event_type, target_url or is_active
curl --location 'https://{commerce_url}/api/v1/whisperer/hooks/?event_type=user_registered' \
--header 'Authorization: Token {token}'

# Change one, for example to pause deliveries without losing the configuration
curl --location --request PATCH 'https://{commerce_url}/api/v1/whisperer/hooks/{id}/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{"is_active": false}'

# Stop deliveries to a hook you no longer need
curl --location --request DELETE 'https://{commerce_url}/api/v1/whisperer/hooks/{id}/' \
--header 'Authorization: Token {token}'
```

Set `is_active` back to `true` to resume. Both pausing and deleting stop new deliveries, but a message that was already queued, or that is waiting for its next retry, can still go out after you make the change.

A deleted hook keeps its registration: it stays in the list as inactive, and registering the same event name and address again answers `406`. To use that address again, set `is_active` back to `true` on the existing hook.

You only ever see and change your own hooks; a token cannot read another integration's.

## <mark style="color:red;">**When an Event Is Triggered**</mark>

<table><thead><tr><th width="300">Delivery mode</th><th>The message is sent</th></tr></thead><tbody><tr><td>Immediate</td><td>During the operation that caused it, before the change has finished being stored.</td></tr><tr><td>Queued</td><td>After the change has been stored.</td></tr></tbody></table>

In immediate mode the message can therefore reach you before the operation it describes has finished. If your integration needs the record complete, with every related row written and every list filled in, choose queued delivery, or treat the message as a signal and read the record back from the API before acting on it.

{% hint style="info" %}
In queued mode the payload is captured once, when the message is first sent, and exactly that body is replayed on every retry. Retries never carry a newer version of the record, which is what makes them safe to replay. It also means the API, not a retry, is where you go for the current state.
{% endhint %}

## <mark style="color:red;">**When Your Endpoint Cannot Be Reached**</mark>

A delivery is successful when your endpoint answers with a **2xx** status within **10 seconds**. Everything else counts as a failed attempt: an error status, a timeout, a refused connection.

Queued messages are retried automatically: the first attempt, then up to **10** more, spaced by the interval you configured on the hook. After the eleventh attempt the automatic delivery stops. The event is kept and marked as undelivered, and you can still send it yourself. See [Re-sending an event](#re-sending-an-event).

An immediate message is sent once and is never retried.

{% hint style="info" %}
All of those attempts belong to **one** event and carry the same `event.uuid`. A run of requests for the same customer is the platform working through this budget, not a run of separate events.
{% endhint %}

### <mark style="color:red;">Setting the retry interval</mark> <a href="#setting-the-retry-interval" id="setting-the-retry-interval"></a>

`retry_countdown_config` decides how long to wait before the next attempt. It takes a `choice` and its `kwargs`, where `retry_count` is the number of retries already made. It is therefore `0` when the wait before the first retry is worked out, `1` before the second, and so on.

<table><thead><tr><th width="150">choice</th><th width="260">kwargs</th><th>Wait before the next attempt</th></tr></thead><tbody><tr><td><code>fixed</code></td><td><code>seconds</code></td><td>Always <code>seconds</code>.</td></tr><tr><td><code>linear</code></td><td><code>base</code>, <code>limit</code> (optional)</td><td><code>base * retry_count</code>, capped at <code>limit</code>.</td></tr><tr><td><code>exponential</code></td><td><code>base</code>, <code>factor</code> (default <code>1</code>), <code>limit</code> (optional)</td><td><code>(base ** retry_count) * factor</code>, capped at <code>limit</code>.</td></tr><tr><td><code>random</code></td><td><code>min_value</code>, <code>max_value</code></td><td>A random value in that range.</td></tr></tbody></table>

Because that count starts at `0`, `linear` waits nothing at all before its first retry. `fixed` and `random` ignore the count entirely.

A sensible default is exponential backoff that starts at one minute and never waits longer than an hour:

```json
{
  "choice": "exponential",
  "kwargs": {"base": 2, "factor": 60, "limit": 3600}
}
```

That gives waits of 1, 2, 4, 8, 16 and 32 minutes, then 60 minutes before each remaining retry. A brief outage is caught within a minute, and an endpoint that stays down is given about five hours in total before the automatic attempts run out.

Set a `limit` whenever you choose `exponential`. Without one the waits keep doubling and the last attempts end up days apart.

## <mark style="color:red;">**Confirming a Delivery**</mark> <a href="#confirming-a-delivery" id="confirming-a-delivery"></a>

### <mark style="color:red;">On your own endpoint</mark>

Every request arrives as a `POST` with these headers:

<table><thead><tr><th width="270">Header</th><th>Value</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/json</code></td></tr><tr><td><code>X-Whisperer-Event</code></td><td>The event name, for example <code>user_registered</code>.</td></tr><tr><td><code>X-Whisperer-Signature</code></td><td>Proof that the request came from Akinon, in the form <code>sha256=&#x3C;hex digest></code>. Present on queued messages when the hook has a <code>secret_key</code>.</td></tr><tr><td>Your own headers</td><td>Anything you put in <code>additional_headers</code>, plus basic authentication if you configured it.</td></tr></tbody></table>

The `event.uuid` in the body identifies the event, not the attempt: every retry of the same event repeats it. Use it as your idempotency key, so a repeated delivery does not produce a second record or a second e-mail on your side.

**Verify the signature.** It is computed over the raw request body, before any parsing, using the same `secret_key` you chose when you registered the hook. Your own copy of that string is the one you verify with; if you have lost it, read the hook back with `GET /whisperer/hooks/` or set a new one with `PATCH`.

```python
import hashlib
import hmac

def is_from_akinon(request, secret_key):
    received = request.headers.get("X-Whisperer-Signature", "")
    expected = "sha256={}".format(
        hmac.new(
            secret_key.encode("utf-8"),
            request.body,           # the raw body, before any parsing
            hashlib.sha256,
        ).hexdigest()
    )
    return hmac.compare_digest(received, expected)
```

A hook with no `secret_key` sends no signature header at all, so an endpoint that requires one should treat a missing header as a failed check rather than a pass. Immediate messages are not signed, so protect that address with the basic authentication in `authentication_config` instead.

{% hint style="warning" %}
**Answer within 10 seconds.** Acknowledge the request first and do the real work afterwards, in your own background queue. An endpoint that finishes its processing before answering is recorded as a failed attempt once it runs long, and the message is then re-sent, even though the work succeeded.
{% endhint %}

### <mark style="color:red;">On the Akinon side</mark>

Every attempt at a queued delivery is recorded: what was sent, when, what your endpoint answered, and how long it took. You can read these records yourself instead of opening a support ticket:

```bash
# What was sent for one specific event, and how did the endpoint answer?
curl --location 'https://{commerce_url}/api/v1/whisperer/hook_events/?uuid=c9591f0d1c1d4da58ade880921bd7e93' \
--header 'Authorization: Token {token}'

# Everything that could not be delivered since a given moment
curl --location 'https://{commerce_url}/api/v1/whisperer/hook_events/?delivered=false&created_date__gte=2025-01-20T00:00:00Z' \
--header 'Authorization: Token {token}'

# Every attempt made for one record
curl --location 'https://{commerce_url}/api/v1/whisperer/hook_events/?event_type=user_registered&object_id=12345' \
--header 'Authorization: Token {token}'
```

Available filters: `uuid`, `event_type`, `delivered`, `response_http_status`, `object_id`, `content_type`, `webhook`, and `created_date` with the `gt`, `gte`, `lt` and `lte` suffixes.

Each record answers a different half of "did you really send it, and what happened":

<table><thead><tr><th width="250">Field</th><th>What it tells you</th></tr></thead><tbody><tr><td><code>uuid</code></td><td>The <code>event.uuid</code> your endpoint received. This is what ties a record here to a line in your own log.</td></tr><tr><td><code>request_datetimes</code></td><td>The timestamp of <strong>every</strong> attempt, most recent first. This is the proof of what was sent and when.</td></tr><tr><td><code>retry_count</code></td><td>How many attempts have been made so far.</td></tr><tr><td><code>delivered</code></td><td><code>true</code> once an attempt was answered with a 2xx.</td></tr><tr><td><code>response_http_status</code></td><td>The status your endpoint answered with on the last attempt.</td></tr><tr><td><code>response_content</code></td><td>The body your endpoint answered with. Usually where the reason for a failure is.</td></tr><tr><td><code>response_time</code></td><td>How long that attempt took. Compare it against the 10-second limit.</td></tr><tr><td><code>request_payload</code></td><td>The exact body that was sent, and that every retry repeats.</td></tr><tr><td><code>object_id</code></td><td>The identifier of the record the event is about, the same value as the identifier carried in the payload.</td></tr><tr><td><code>webhook</code></td><td>The hook it was sent through, including its <code>target_url</code>.</td></tr></tbody></table>

So a delivery you cannot find in your own logs falls into one of three cases: there is no record here at all, and the event was never raised; there is a record with a `response_http_status` your endpoint returned, and the request did arrive; or `delivered` is `false` with a timeout or connection error, and the request never got through.

Immediate messages are not recorded this way. There is no stored attempt to look up afterwards, which is another reason to prefer queued delivery for anything you need to audit.

### <mark style="color:red;">Re-sending an event</mark> <a href="#re-sending-an-event" id="re-sending-an-event"></a>

Once your endpoint is healthy again, an event that was given up on can be sent again. The stored body is replayed unchanged:

```bash
curl --location --request POST \
'https://{commerce_url}/api/v1/whisperer/hook_events/{id}/retry/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{"force": false}'
```

You get `200` when the delivery succeeds. You get `406` when it fails, when the event had already been delivered, or when it is still inside its automatic retry window. Add `"force": true` to send it anyway.

## <mark style="color:red;">**A Complete Example**</mark>

Suppose you want your CRM to be told whenever a customer creates an account.

**1. Register the hook,** so queued deliveries know where to go and how to sign themselves:

```bash
curl --location 'https://{commerce_url}/api/v1/whisperer/hooks/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{
    "event_type": "user_registered",
    "target_url": "https://example.com/hooks/user-registered/",
    "secret_key": "a-long-random-string",
    "retry_countdown_config": {"choice": "exponential", "kwargs": {"base": 2, "factor": 60, "limit": 3600}}
}'
```

**2. Create the definition** that turns the event on, in queued mode:

```bash
curl --location 'https://{commerce_url}/api/v1/event_messages/' \
--header 'Authorization: Token {token}' \
--header 'Content-Type: application/json' \
--data '{
    "event_name": "user_registered",
    "description": "Send new customers to the CRM",
    "asynchronous": true,
    "transactional": false,
    "url": "https://example.com/hooks/user-registered/"
}'
```

**3. Register a customer on the storefront.** Your endpoint receives:

```http
POST /hooks/user-registered/ HTTP/1.1
Content-Type: application/json
X-Whisperer-Event: user_registered
X-Whisperer-Signature: sha256=6854d309ec710fa7b818d42c2d14b4d6b290a31ef23c224a4e127d790154458c

{
  "event": {"type": "user_registered", "uuid": "c9591f0d1c1d4da58ade880921bd7e93"},
  "payload": {
    "pk": 71133,
    "email": "john.doe@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "email_allowed": true,
    "date_joined": "2024-12-30T10:50:22.024234Z"
  }
}
```

The `payload` is shortened here for readability; the full set of fields is under [UserRegisteredEvent](#available-commerce-webhook-events).

**4. Handle it.** Verify `X-Whisperer-Signature` against your `secret_key`, and stop if it does not match. Check whether you have already seen `event.uuid`, and if you have, answer `200` and do nothing else. Otherwise write the job to your own queue and answer `200` straight away. The CRM call happens after you have answered, not before.

**5. Confirm it arrived,** using the `uuid` from the body:

```bash
curl --location 'https://{commerce_url}/api/v1/whisperer/hook_events/?uuid=c9591f0d1c1d4da58ade880921bd7e93' \
--header 'Authorization: Token {token}'
```

`delivered: true` with a single entry in `request_datetimes` means it went out once and you accepted it. `delivered: false` with a growing `request_datetimes` and `retry_count` means the platform is still working through its eleven attempts, and `response_http_status` and `response_content` say what your endpoint answered each time.

## <mark style="color:red;">**Available Commerce Webhook Events**</mark> <a href="#available-commerce-webhook-events" id="available-commerce-webhook-events"></a>

In most payloads the `pk` field is the identifier of the record the event is about.

<table><thead><tr><th width="270">Event name</th><th>Triggered when</th></tr></thead><tbody><tr><td><code>basket_offer_created</code></td><td>A campaign, coupon or discount code is created.</td></tr><tr><td><code>basket_offer_updated</code></td><td>A campaign, coupon or discount code is changed.</td></tr><tr><td><code>user_logged_in</code></td><td>A customer signs in.</td></tr><tr><td><code>user_registered</code></td><td>A customer creates an account.</td></tr><tr><td><code>user_updated</code></td><td>A customer's details change. Signing in updates the last-login date, so this event follows a sign-in as well.</td></tr><tr><td><code>conversation_replied</code></td><td>A message sent by a customer receives a reply.</td></tr></tbody></table>

### <mark style="color:red;">**1. BasketOfferCreatedEvent**</mark>

**event\_name:** `basket_offer_created`

This event is triggered when a **new campaign, coupon, or discount code** is created.

#### **Payload Structure**

```json
{
  "pk": 727,
  "label": "Extra 50% Off Baskets Over 100 TRY",
  "promotion": {
    "id": 727,
    "created_date": "2024-09-23T08:30:18.382660Z",
    "modified_date": "2025-01-28T12:20:15.198191Z",
    "translations": null,
    "name": "Extra 50% Off",
    "slug": "extra-50-off"
  },
  "condition": {
    "id": 727,
    "product_collection": 628,
    "condition_type": "amount",
    "kwargs": {
      "query": [],
      "value": 100,
      "price_type": "unit_price",
      "consume_type": "globally",
      "data_sources": [
        "example-store"
      ],
      "sub_conditions": []
    },
    "created_date": "2024-09-23T08:30:18.388607Z",
    "modified_date": "2025-01-28T12:20:15.208973Z",
    "translations": null,
    "upsell_message": "Add {remaining} more to your basket to get an extra 50% off."
  },
  "benefit": {
    "id": 727,
    "product_collection": 628,
    "benefit_type": "percentage",
    "kwargs": {
      "coupon": {},
      "percentage": 75,
      "price_type": "unit_price",
      "consume_type": "globally"
    },
    "created_date": "2024-09-23T08:30:18.391237Z",
    "modified_date": "2025-01-28T12:20:15.214853Z"
  },
  "voucher_code": null,
  "status": "active",
  "is_visible": true,
  "is_visible_on_list": true,
  "start_datetime": "2025-01-06T08:27:00Z",
  "end_datetime": "2025-01-31T06:00:00Z",
  "user": null,
  "offer_type": "sitewide",
  "allowed_quantity_per_basket": 1,
  "priority": 2,
  "is_mergable": true,
  "max_usage_per_user": 0,
  "end_timedelta": null,
  "activation_timedelta": null,
  "activation_date": null,
  "kwargs": {},
  "currencies": [
    "try"
  ],
  "is_available_for_data_sources": false,
  "max_stock_limit": 0,
  "modified_date": "2025-01-28T12:20:15.221904Z"
}
```

### <mark style="color:red;">**2. BasketOfferUpdatedEvent**</mark>

**event\_name:** `basket_offer_updated`

This event is triggered when a **campaign, coupon, or discount code is updated**. The payload structure is identical to `BasketOfferCreatedEvent`.

### <mark style="color:red;">**3. UserLoggedInEvent**</mark>

**event\_name:** `user_logged_in`

This event is triggered when a **user logs into the Commerce system**.

#### **Payload Structure**

```json
{
  "pk": 71133,
  "username": "836f82db99121b3481011f16b49dfa5fbc714a0d1b1b9f784a1ebbbf5b39577f",
  "first_name": "John",
  "last_name": "Doe",
  "email": "john.doe@example.com",
  "is_active": true,
  "date_joined": "2024-12-30T10:50:22.024234Z",
  "last_login": "2025-03-21T10:23:25.026743Z",
  "email_allowed": true,
  "sms_allowed": true,
  "whatsapp_allowed": false,
  "call_allowed": false,
  "gender": null,
  "date_of_birth": null,
  "attributes": {
    "confirm": true,
    "logged_ip": "203.0.113.42",
    "register_client_type": "default"
  },
  "phone": null,
  "attributes_kwargs": {},
  "user_type": "registered",
  "modified_date": "2025-01-07T13:41:02.975697Z"
}
```

### <mark style="color:red;">**4. UserUpdatedEvent**</mark>

**event\_name:** `user_updated`

This event is triggered when a **user updates their information** in the Commerce system. Since the last login date is updated when a user logs in, this event is also triggered when a user successfully logs into the system. The payload structure is identical to `UserLoggedInEvent`.

### <mark style="color:red;">**5. UserRegisteredEvent**</mark>

**event\_name:** `user_registered`

This event is triggered when a **new user registers** in the Commerce system. The payload structure is identical to `UserLoggedInEvent`.

### <mark style="color:red;">**6. ConversationRepliedEvent**</mark>

**event\_name:** `conversation_replied`

A conversation is a thread between a customer and a seller, started by the customer: a question about a product, or a message about an order item. This event is triggered when the **seller replies** to such a thread. A message written by the customer does not trigger it.

The payload is the whole conversation, not just the new reply: the customer it belongs to, the seller who replied, the item it is about, and every message in the thread in order. `is_answered` is `true`, and the last entry in `message_set` is the reply that caused the event. It is the one whose `content_type` is `datasource`.

The conversation is identified by `id` here rather than `pk`.

#### **Payload Structure**

```json
{
  "id": 4312,
  "subject": "Question about the product",
  "user": {
    "id": 71133,
    "first_name": "John",
    "last_name": "Doe",
    "email": "john.doe@example.com",
    "phone": null,
    "gender": null
  },
  "datasource": {
    "pk": 1,
    "name": "example-store",
    "slug": "example-store",
    "title": "Example Store",
    "supplier_code": "EXS-01",
    "address": null,
    "email": "support@example.com",
    "phone_number": null,
    "fax_number": null,
    "kep_address": null,
    "mersis_number": null,
    "trade_association": null,
    "extras": {},
    "price_list": 1,
    "stock_list": 1,
    "is_active": true
  },
  "item_object": {
    "pk": 98231,
    "name": "Wooden Serving Plate",
    "sku": "SKU-100234-XL",
    "absolute_url": "/wooden-serving-plate-p-98231",
    "product_images": [
      {
        "pk": 55120,
        "product": 98231,
        "image": "products/sku-100234-xl-1.jpg",
        "image_path": "products/sku-100234-xl-1.jpg",
        "order": 0,
        "height": 1200,
        "width": 1200
      }
    ]
  },
  "item_content": "product",
  "message_set": [
    {
      "id": 9001,
      "message_content": "What is the diameter of this plate?",
      "user_type": "registered",
      "content_object": {
        "id": 71133,
        "email": "john.doe@example.com",
        "first_name": "John",
        "last_name": "Doe"
      },
      "content_type": "user",
      "created_date": "2025-03-21T10:23:25.026743Z"
    },
    {
      "id": 9002,
      "message_content": "Hello, the diameter is 28 cm.",
      "user_type": "registered",
      "content_object": {
        "id": 1,
        "email": "support@example.com",
        "name": "example-store",
        "title": "Example Store"
      },
      "content_type": "datasource",
      "created_date": "2025-03-21T11:02:10.551204Z"
    }
  ],
  "conversation_type": "question",
  "last_message_date": "2025-03-21T11:02:10.551204Z",
  "is_public": true,
  "is_answered": true
}
```

**Fields worth noting:**

* `conversation_type`: `message`, `question` or `review`.
* `item_content` and `item_object`: what the conversation is about. `item_content` is `product` when the customer asked about a product, and `order_item` when it is about a line in an order; `item_object` then carries that record. Both are `null` for a conversation that is not attached to anything.
* `is_public`: whether the thread may be shown on the storefront, for example under a product.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.akinon.com/technical-guides/commerce/commerce-webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
