> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gomry.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks to Retail

> Tell Gomry Retail an order changed, so it reconciles now instead of on its next sweep

# Webhooks to Retail

Webhooks are the one direction that runs from your integration to Retail. They are **optional and make things faster**. Retail already checks every open order on its own schedule (order state hourly, fulfilment every 10 minutes), so a missed webhook delays a sale but never loses one.

Send one when an order is accepted, rejected or cancelled, or when a ticket becomes retrievable.

## Endpoint

```
POST https://retail.gomry.com/api/webhooks/{vendorKey}
```

`{vendorKey}` is the supplier identifier Gomry assigns you. It identifies you. It does not authenticate you.

## A webhook is a wake-up call

**Retail never changes an order's state from a webhook body.** A payload that says "accepted" is a claim, and acting on it would let a forged or buggy message create a payment to you for a purchase that never happened. This is true even when the signature is valid: a signature proves who sent the bytes, not that what they say is right.

So Retail reads one thing out of the body, an order reference, and then asks you through your own [Search orders](/vendor-api/search-orders) endpoint. Delivery follows through [Fulfilment](/vendor-api/fulfilment). Those endpoints are the truth. The webhook only has to say which order to look at.

## Payload

Keep it small. Retail reads an event type and an order reference from the top level of the body:

```json theme={null}
{
  "event": "order.accepted",
  "reference": "8b1f3c5e2a9d4f7081c6e2b4a0d9f3e1"
}
```

<ParamField body="reference" type="string" required>
  **Retail's own `reference`**, exactly as it arrived on [Place order](/vendor-api/place-order). Retail passes this value straight to `GET /api/vendor/v1/orders?reference=…`, so it must be something your search endpoint can find.
</ParamField>

<ParamField body="event" type="string">
  A short name for what happened. Retail stores it and filters on it. Use your own vocabulary in lowercase, for example `order.accepted` or `delivery.available`.
</ParamField>

<Warning>
  **Do not also send `order_id`, `orderId` or `id` at the top level.** Retail takes the first identifier it finds, in this order: `order_id`, `orderId`, `reference`, `id`. A top-level `order_id` holding your own order number wins over `reference`, gets searched as a reference, matches nothing, and the webhook is recorded as delivered while nothing happens. Put your own ids inside a nested object if you want them in the record.
</Warning>

Also accepted, for systems that cannot send JSON: a form-encoded body, including one where a field holds a JSON string. The same field names apply.

A body larger than **64 KB** is rejected.

## Authentication

Choose one mode with us when you [go live](/vendor-api/going-live). Signing is strongly preferred.

<Tabs>
  <Tab title="HMAC signature (preferred)">
    Sign the **exact raw bytes** of the body with HMAC-SHA256, using the webhook secret Gomry issued you, and send the hex digest in a header. The default header is `X-Vendor-Signature`. We can configure a different name if your system already uses one.

    ```
    X-Vendor-Signature: 5d41402abc4b2a76b9719d911017c592ae7f4c2c6d1b1e0f3a8c9e2d4b6f8a0c
    ```

    A `sha256=` prefix is also accepted. Sign the bytes you send, not a re-serialized object: re-encoding JSON does not reliably reproduce them.

    ```javascript Node.js theme={null}
    import { createHmac } from "node:crypto";

    const body = JSON.stringify({ event: "order.accepted", reference });
    const signature = createHmac("sha256", process.env.GOMRY_RETAIL_WEBHOOK_SECRET)
      .update(body)
      .digest("hex");

    await fetch(`https://retail.gomry.com/api/webhooks/${vendorKey}`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-vendor-signature": signature,
        "x-webhook-id": deliveryId,
      },
      body,
    });
    ```

    Once signing is switched on for you, an unsigned delivery is rejected with `401`.
  </Tab>

  <Tab title="IP allowlist">
    If your system cannot sign, Retail accepts deliveries only from the source addresses you give us. This authenticates the connection, not the message, and it breaks when your egress addresses change. Tell us before they do.
  </Tab>
</Tabs>

## Redeliveries

Send a unique id per delivery in `X-Webhook-Id` (`X-Delivery-Id` and `X-Request-Id` also work). Retail uses it to recognize a redelivery and process it once. Without one, Retail treats two byte-identical bodies as the same delivery.

## Responses

| Status | Meaning | Should you redeliver? |
| - | - | - |
| `200` | Stored. The body says what Retail did: `reconcile`, `duplicate` or `stored_only`. | No |
| `400` | The body is larger than 64 KB. | No, fix the payload |
| `401` / `403` | Signature missing or wrong, or source address not allowed. | No, fix the configuration |
| `404` | No supplier with this `vendorKey`. | No |
| `429` | Rate limited. `Retry-After` gives the seconds to wait. | Yes, after the wait |

Retail answers `200` as soon as a webhook is stored, **even if its own processing then fails**. That is deliberate: a `500` would ask you to send it again and amplify an outage on our side. A failed reconciliation stays recorded and is retried by Retail's sweep.

`stored_only` means the body had no reference Retail could read. It is kept, and nothing is done with it. If you see it for order events, check the field names above.

```json 200 theme={null}
{ "received": true, "action": "reconcile" }
```
