# Order Events

The Innovorder platform fires two order webhook events: `orders.paid` and `orders.cancelled`. The `orders.paid` event is triggered whenever an order is successfully placed and paid for. The `orders.cancelled` event is triggered whenever a previously paid order is cancelled. Both events deliver the exact same **OmnichannelOrder** payload structure, differing only in the `status` field (`PAID` vs `CANCELLED`). This is the primary mechanism to inject orders into external systems and to keep them synchronized with cancellations.

Kiosk "pay at the counter" orders

An `orders.paid` delivery does not always mean the guest paid on the ordering channel. A kiosk order placed to be settled at the counter carries the reserved key `metadata.kioskPaymentState` set to `"NOT_PAID_AT_KIOSK"`. When the restaurant has no POS device receiving kiosk orders, that sale is recorded through a synthetic external payment, so it reaches you as `orders.paid` with `status: "PAID"` and a `payments[]` entry of type `"ExternalPayment"`.

The key is written once at order creation and never changes afterwards: it says where the guest was expected to pay, not whether the money has been collected. Read it, not `status`, to know whether a kiosk order was paid at the kiosk. See the full rule in **Orders &gt; Create Orders**.

Retry Policy

Your server **must** return an HTTP `200 OK` response code with an empty body to acknowledge receipt. If not, we will resend the webhook based on an exponential backoff schedule.

Note: We do not retry requests if your server responds with `401 Unauthorized` or `404 Not Found`.

### Event Structure

Every order webhook delivery is wrapped in the following envelope:

```json
{
  "event_type": "orders.paid",
  "event_id": "c4d2261e-2779-4eb6-beb0-cb41235c751e",
  "event_time": 1427343990,
  "payload": { ... }
}
```

-   `event_type`: Either `"orders.paid"` or `"orders.cancelled"`.
-   `event_id`: HMAC-SHA256 hash that uniquely identifies this delivery. Use it for deduplication.
-   `event_time`: Unix timestamp in milliseconds indicating when the event was generated.
-   `payload`: The complete **OmnichannelOrder** object described below.

### Complete Payload Example

Below is a full, realistic example of an `orders.paid` webhook delivery showing all key fields:

```json
{
  "event_type": "orders.paid",
  "event_id": "a3f8c2d1e9b74056ab12cd34ef567890abcdef1234567890abcdef1234567890",
  "event_time": 1711360200000,
  "payload": {
    "omnichannelOrderId": "98765432",
    "reportOrderId": 987654321,
    "orderUuid": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "brandId": 100,
    "restaurantId": 200,
    "customerId": 5001,
    "userId": null,
    "dailyOrderId": 42,
    "status": "PAID",
    "channelId": "2",
    "consumptionModeId": 1,
    "deviceId": "WEB-001",
    "deviceName": "Web Ordering",
    "seller": null,
    "ticketNumber": "A042",
    "tableName": null,
    "comment": null,
    "numberOfGuests": 1,
    "currency": "EUR",
    "startDate": "2026-03-25T12:30:00.000Z",
    "lastUpdateDate": "2026-03-25T12:32:00.000Z",
    "businessYear": 2026,
    "businessMonth": 3,
    "businessDay": 25,
    "service": 0,
    "shift": { "name": "Lunch" },
    "totalPriceWithTaxIncluded": 1850,
    "totalPriceWithTaxExcluded": 1542,
    "totalPriceDiscountedWithTaxIncluded": 1550,
    "totalPriceDiscountedWithTaxExcluded": 1292,
    "totalTax": 258,
    "totalDiscount": 300,
    "turnover": 1550,
    "totalAmountFree": 0,
    "totalItemFree": 0,
    "totalAmountCancelled": 0,
    "totalItemCancelled": 0,
    "items": [
      {
        "itemId": 5001,
        "parentItemId": null,
        "name": "Classic Burger",
        "type": "PRODUCT",
        "status": "VALIDATED",
        "quantity": 1000,
        "totalPriceWithTaxIncluded": 1200,
        "originalUnitPrice": 1200,
        "vatValue": 1000,
        "skuValue": "BUR-001",
        "free": false,
        "tags": [{ "value": "Burger" }],
        "categories": [{ "value": "Main Course" }],
        "discounted": false,
        "measurementUnit": "UNIT",
        "measuredQuantity": 1
      }
    ],
    "payments": [
      {
        "type": "Card",
        "label": "Visa",
        "quantity": 1,
        "value": 1550,
        "luncheonCode": null,
        "transactionId": null
      }
    ],
    "taxes": [
      {
        "taxRate": 1000,
        "totalTax": 258,
        "totalInclTax": 1550,
        "totalExclTax": 1292
      }
    ],
    "discounts": [
      { "name": "SUMMER10", "value": 300 }
    ],
    "fees": [
      {
        "name": "Delivery Fee",
        "taxRate": 2000,
        "quantity": 1,
        "amount": 350,
        "type": "Delivery"
      }
    ],
    "grants": [],
    "entranceFees": [],
    "loyaltyPrograms": [],
    "pricingRule": null,
    "customer": {
      "phone": "+33612345678",
      "email": "jean.dupont@example.com",
      "firstName": "Jean",
      "lastName": "Dupont",
      "badgeNumber": null,
      "disabledAt": null,
      "student": null
    },
    "restaurant": {
      "name": "Chez Yann",
      "contactPhone": "0147689047",
      "externalId": "SITE01_REST01",
      "address": {
        "streetNumber": "12",
        "route": "Rue de la Paix",
        "postalCode": "75002",
        "locality": "Paris"
      }
    },
    "deliveryAddress": {
      "phone": "+33612345678",
      "route": "45 Avenue des Champs-Elysees",
      "postalCode": "75008",
      "locality": "Paris",
      "streetNumber": "45",
      "additionalInfo": "3rd floor",
      "digicode": "4521",
      "apartmentNumber": "12B"
    },
    "photos": [],
    "cancellationReason": null,
    "cancellationReasonDescription": null,
    "metadata": null
  }
}
```

### OmnichannelOrder Payload Reference

The payload contains the complete details of the order. All monetary values are expressed in **cents** (e.g., 1850 = 18.50 EUR) unless otherwise noted. Below is a comprehensive field-by-field reference grouped by category.

#### Core Identifiers

| Field | Type | Description |
| --- | --- | --- |
| omnichannelOrderId | String | Unique identifier of the order. |
| reportOrderId | Number | Numeric ID used in reporting and exports. |
| orderUuid | String (UUID) | UUID v4 identifier of the order. |
| brandId | Number | Unique identifier of the brand. |
| restaurantId | Number | Unique identifier of the restaurant. |
| customerId | Number \| null | ID of the customer who placed the order, or `null` for anonymous orders. |
| userId | Number \| null | ID of the authenticated user (e.g., POS operator), or `null`. |
| dailyOrderId | Number | Sequential order number for the current business day. |
| channelId | String (Enum) | Ordering channel: `"1"` = Kiosk, `"2"` = Web, `"5"` = POS. |
| consumptionModeId | Number (Enum) | Consumption mode: `1` = Delivery, `2` = Take Away, `3` = Sit In, `4` = Drive. |
| deviceId | String \| null | Identifier of the device that created the order. |
| deviceName | String \| null | Human-readable name of the device. |
| seller | String \| null | Name of the seller/cashier, if applicable. |
| ticketNumber | String \| null | Printed ticket number displayed to the customer (e.g., `"A042"`). |
| tableName | String \| null | Table name for sit-in orders, or `null`. |
| comment | String \| null | Free-text comment left by the customer. |
| numberOfGuests | Number | Number of guests for the order. |
| currency | String | ISO 4217 currency code (e.g., `"EUR"`). |

#### Timestamps

| Field | Type | Description |
| --- | --- | --- |
| startDate | String (ISO 8601) | Timestamp when the order was created. |
| lastUpdateDate | String (ISO 8601) | Timestamp of the last modification to the order. |
| businessYear | Number | Fiscal year of the order. |
| businessMonth | Number | Fiscal month of the order (1-12). |
| businessDay | Number | Fiscal day of the order (1-31). |
| service | Number | Service period index (0-based). |
| shift.name | String \| null | Name of the shift (e.g., `"Lunch"`, `"Dinner"`). |

#### Status

| Field | Type | Description |
| --- | --- | --- |
| status | String (Enum) | Order status: `"PAID"`, `"CANCELLED"`, or `"VALIDATED"`. |
| cancellationReason | String \| null | Reason code for cancellation. Present only when `status` is `"CANCELLED"`. |
| cancellationReasonDescription | String \| null | Human-readable description of the cancellation reason. |
| metadata | String \| null | JSON-encoded metadata of the originating Web/Kiosk order, as sent on `POST /orders` (`null` when the order has no Web/Kiosk counterpart, e.g. a POS sale). The platform owns a single key inside it, `kioskPaymentState`, set to `"NOT_PAID_AT_KIOSK"` when a kiosk guest ordered to pay at the counter, which can happen while `status` already reads `"PAID"`. Every other key is yours and is passed through untouched. |

#### Pricing (all values in cents)

| Field | Type | Description |
| --- | --- | --- |
| totalPriceWithTaxIncluded | Number | Total price including tax, before discounts. |
| totalPriceWithTaxExcluded | Number | Total price excluding tax, before discounts. |
| totalPriceDiscountedWithTaxIncluded | Number | Total price including tax, after discounts. |
| totalPriceDiscountedWithTaxExcluded | Number | Total price excluding tax, after discounts. |
| totalTax | Number | Total tax amount. |
| totalDiscount | Number | Total discount amount applied to the order. |
| turnover | Number | Net revenue (total after discounts, tax included). |
| totalAmountFree | Number | Total value of free (complimentary) items. |
| totalItemFree | Number | Count of free items in the order. |
| totalAmountCancelled | Number | Total value of cancelled items. |
| totalItemCancelled | Number | Count of cancelled items in the order. |

#### Item Object

| Field | Type | Description |
| --- | --- | --- |
| itemId | Number | Unique identifier of the item. |
| parentItemId | Number \| null | ID of the parent item for sub-items (e.g., menu steps). `null` for top-level items. |
| name | String | Display name of the product. |
| type | String | Item type (e.g., `"PRODUCT"`, `"MENU"`, `"STEP"`). |
| status | String | Item status (e.g., `"VALIDATED"`, `"CANCELLED"`). |
| quantity | Number | Quantity in **thousandths** (e.g., `1000` = 1 unit, `2500` = 2.5 units). |
| totalPriceWithTaxIncluded | Number | Total item price including tax (cents). |
| originalUnitPrice | Number | Original unit price before any discount (cents). |
| vatValue | Number | VAT rate in **basis points** (e.g., `1000` = 10.00%). |
| skuValue | String \| null | SKU code of the product. |
| free | Boolean | Whether the item was given for free. |
| tags | Array | List of tag objects. Each has a `value` string. |
| categories | Array | List of category objects. Each has a `value` string. |
| discounted | Boolean | Whether a discount was applied to this item. |
| reduced | Boolean \| undefined | Whether the item has a reduced price (e.g., pricing rule). |
| measurementUnit | String | Unit of measurement (e.g., `"UNIT"`, `"KG"`). |
| measuredQuantity | Number | Human-readable quantity (e.g., `1` for 1 unit, `0.5` for 500g). |

#### Payment Object

| Field | Type | Description |
| --- | --- | --- |
| type | String | Payment type: `"Cash"`, `"Card"`, `"EWallet"`, `"Change"`, etc. |
| label | String \| null | Display label for the payment method (e.g., `"Visa"`). |
| quantity | Number | Number of payment units (typically `1`). |
| value | Number | Payment amount in cents. |
| luncheonCode | String \| null | Luncheon voucher code, if applicable. |
| transactionId | String \| null | External transaction ID from the payment provider. |

#### Tax Object

| Field | Type | Description |
| --- | --- | --- |
| taxRate | Number | Tax rate in **basis points** (e.g., `1000` = 10.00%, `2000` = 20.00%). |
| totalTax | Number | Total tax amount for this rate (cents). |
| totalInclTax | Number | Total amount including this tax (cents). |
| totalExclTax | Number | Total amount excluding this tax (cents). |

#### Fee Object

| Field | Type | Description |
| --- | --- | --- |
| name | String | Name of the fee (e.g., `"Delivery Fee"`). |
| taxRate | Number | Tax rate applied to the fee (basis points). |
| quantity | Number | Number of fee units. |
| amount | Number | Fee amount in cents. |
| type | String | Fee type: `"Delivery"` or `"Service"`. |

#### Discount Object

| Field | Type | Description |
| --- | --- | --- |
| name | String | Name or code of the discount (e.g., `"SUMMER10"`). |
| value | Number | Discount amount in cents. |

#### Grant Object

| Field | Type | Description |
| --- | --- | --- |
| labelTicket | String | Label printed on the ticket for the grant. |
| amount | Number | Unit grant amount in cents. |
| quantity | Number | Number of grant units. |
| totalAmount | Number | Total grant amount in cents (`amount * quantity`). |

#### EntranceFee Object

| Field | Type | Description |
| --- | --- | --- |
| labelTicket | String | Label printed on the ticket for the entrance fee. |
| amount | Number | Unit entrance fee amount in cents. |
| vat | Number | VAT rate applied to the entrance fee (basis points). |
| type | String | Type of entrance fee. |
| totalTaxEntranceFee | Number | Total tax amount for the entrance fee (cents). |
| quantity | Number | Number of entrance fee units. |
| totalAmount | Number | Total entrance fee amount in cents. |

#### Customer Object

| Field | Type | Description |
| --- | --- | --- |
| phone | String \| null | Customer's phone number. |
| email | String \| null | Customer's email address. |
| firstName | String \| null | Customer's first name. |
| lastName | String \| null | Customer's last name. |
| badgeNumber | String \| null | RFID/NFC badge number. |
| disabledAt | String \| null | Timestamp when the customer account was disabled, or `null` if active. |
| student | Object \| null | Student information object containing `class`, `section`, `subSection`, and `studentNumber`. |
| balanceInformation | Object \| null | E-Wallet balance info containing `ewalletBalanceBeforeOrder` and `ewalletBalanceAfterOrder` (cents). |

#### Restaurant Object

| Field | Type | Description |
| --- | --- | --- |
| name | String | Restaurant name. |
| contactPhone | String \| null | Restaurant contact phone number. |
| externalId | String \| null | External identifier for the restaurant (used for third-party integrations). |
| address.streetNumber | String \| null | Street number of the restaurant address. |
| address.route | String \| null | Street name. |
| address.postalCode | String \| null | Postal/ZIP code. |
| address.locality | String \| null | City name. |
| address.country | String \| null | Country code or name. |

#### Delivery Address

| Field | Type | Description |
| --- | --- | --- |
| phone | String \| null | Contact phone number for delivery. |
| streetNumber | String \| null | Street number. |
| route | String \| null | Street name. |
| postalCode | String \| null | Postal/ZIP code. |
| locality | String \| null | City name. |
| additionalInfo | String \| null | Additional delivery instructions (e.g., `"3rd floor"`). |
| digicode | String \| null | Building entry code. |
| apartmentNumber | String \| null | Apartment or unit number. |

#### Note: Object Mapping for Students

For student account integrations, mapping specific fields is often required.

```javascript
// Example JS extraction
const payload = event.payload;

const mapping = {
  badgeNumber: payload.customer.badgeNumber,
  registrationNumber: payload.customer.student.studentNumber, // Matricule
  accountId: payload.customerId, // ID Convive
  siteId: payload.restaurant.externalId.split('_')[0]
};
```

**Note:** The OmnichannelOrder payload is the same structure returned by `GET /v1/orders`. See [Retrieve Orders](https://developers.innovorder.io/docs/orders/orders-retrieve.md) for query parameters and filtering options.
