# Integration Guide: Sales & Catalog Synchronization

This guide shows how to build a reliable downstream sales and product-catalog synchronization. It is intended for reporting, ERP, product-information, and data-platform integrations that need a complete order history alongside the catalog used to sell those orders.

### Before you start

-   Use a service account with access to the brand or restaurant scope that you intend to synchronize.
-   Choose one order scope per request: `brandId`, `brandIds`, `restaurantId`, `restaurantIds`, or `customerId`.
-   Configure the target menu and, when relevant, the `consommation_mode` outside the extraction job. Do not assume that an active menu is unique.
-   Store a synchronization watermark and make loads idempotent before scheduling recurring runs.

### 1\. Authenticate

Exchange the credentials supplied for the integration for an access token. The login payload is form-encoded, not JSON. Tokens remain valid for 24 hours, or 14 days when `rememberMe=true`.

### `POST /oauth/login` - Request an access token

Send an application/x-www-form-urlencoded body. Re-authenticate after a 401 token\_expired response.

#### Request Body

```json
rememberMe=false
username=integration@example.com
password=YOUR_SECURE_PASSWORD
grant_type=password
```

##### Request Body Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| rememberMe | boolean | false | The remember me value. |
| username | string | integration@example.com | The username value. |
| password | string | YOUR\_SECURE\_PASSWORD | The password value. |
| grant\_type | string | password | The grant type value. |

#### Response

```json
{
  "access_token": "eyJhbGciOiJIUzI1...",
  "token_type": "Bearer",
  "status": 201,
  "code": "token_created",
  "message": "You have successfully logged in.",
  "data": {
    "user": {
      "restaurants": [
        {
          "restaurantId": 2337
        }
      ]
    }
  }
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| access\_token | string | "eyJhbGciOiJIUzI1..." | The access token value. |
| token\_type | string | "Bearer" | The token type value. |
| status | integer | 201 | HTTP status code returned by the API. |
| code | string | "token\_created" | Machine-readable application code for the result. |
| message | string | "You have successfully logged in." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.user | object | {…} | Object containing user fields. |
| data.user.restaurants | array | \[…\] | List of restaurants entries. |
| data.user.restaurants\[\] | object | {…} | Object containing restaurants fields. |
| data.user.restaurants\[\].restaurantId | integer | 2337 | Identifier of the restaurant. |

Send `Authorization: Bearer <access_token>` with the order and menu-listing requests below.

### 2\. Extract sales by update time

Use `GET /v1/orders` for both the initial backfill and recurring synchronization. For incremental jobs, `ticketStartDate` and `ticketEndDate` filter`lastUpdateDate`; `startDate` and `endDate` instead filter the original transaction date.

### `GET /v1/orders` - List updated orders

Provide exactly one scope parameter. The response is paginated and wrapped in data.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Example scope. Use one, and only one, supported scope parameter. |
| ticketStartDate | ISO 8601 date-time | Yes | Inclusive lower bound for lastUpdateDate in an incremental run. |
| ticketEndDate | ISO 8601 date-time | Yes | Upper bound for lastUpdateDate in an incremental run. |
| limit | integer | No | Page size from 1 to 1000. Use a fixed value for the complete run. |
| offset | integer | No | Zero-based page offset; start at 0 and increase after every successful page. |

#### Response

```json
{
  "status": 200,
  "code": "omnichannel_orders_succeed",
  "data": {
    "count": 1,
    "omnichannelOrders": [
      {
        "reportOrderId": 317030553,
        "omnichannelOrderId": "51544449",
        "restaurantId": 2337,
        "status": "PAID",
        "startDate": "2026-08-05T13:58:30.000Z",
        "lastUpdateDate": "2026-08-05T14:02:10.000Z",
        "totalPriceDiscountedWithTaxIncluded": 2790,
        "items": []
      }
    ]
  }
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "omnichannel\_orders\_succeed" | Machine-readable application code for the result. |
| data | object | {…} | Endpoint-specific response payload. |
| data.count | integer | 1 | Total number of matching records. |
| data.omnichannelOrders | array | \[…\] | List of omnichannel orders entries. |
| data.omnichannelOrders\[\] | object | {…} | Object containing omnichannel orders fields. |
| data.omnichannelOrders\[\].reportOrderId | integer | 317030553 | Identifier of the associated report order. |
| data.omnichannelOrders\[\].omnichannelOrderId | string | "51544449" | Identifier of the associated omnichannel order. |
| data.omnichannelOrders\[\].restaurantId | integer | 2337 | Identifier of the restaurant. |
| data.omnichannelOrders\[\].status | string | "PAID" | HTTP status code returned by the API. |
| data.omnichannelOrders\[\].startDate | string | "2026-08-05T13:58:30.000Z" | Date or timestamp for start. |
| data.omnichannelOrders\[\].lastUpdateDate | string | "2026-08-05T14:02:10.000Z" | Date or timestamp for last update. |
| data.omnichannelOrders\[\].totalPriceDiscountedWithTaxIncluded | integer | 2790 | The total price discounted with tax included value. |
| data.omnichannelOrders\[\].items | array | \[\] | List of items entries. |

### 3\. Make the incremental job recoverable

1.  Define a closed extraction window and replay a configurable overlap before the last successful watermark to capture late corrections.
2.  Read pages with a fixed `limit`, beginning at `offset=0`. Set the next offset to the current offset plus the requested limit, then stop when that next offset is greater than or equal to `data.count`. Do not advance by `omnichannelOrders.length`: the count and offset apply to source reporting rows before response de-duplication.
3.  Upsert each order by `reportOrderId` and each line by the composite key `reportOrderId + itemId`.
4.  Advance the watermark only after every page, transformation, and target write has succeeded. A failed run must be replayed from the previous successful watermark.

#### Corrections and cancellations

Do not discard `CANCELLED`, `PAID_CANCELLED`, or `CONTRA_ENTRY` records. A paid order that is later reversed may surface both an origin record and a contra-entry record. Preserve each reporting record by `reportOrderId`, its status, and its monetary values. When present, `reportOrderContraEntry` identifies the origin and contra-entry order references; do not assume that those two rows share an `omnichannelOrderId`.

### 4\. Map sales and catalog data

The following mapping is intentionally neutral so it can be applied to a data warehouse, ERP, or product-information system. Keep the API values in their source units until a single, explicit conversion step at the target boundary.

| Target concept | Innovorder field | Integration rule |
| --- | --- | --- |
| Sales-header key | reportOrderId | Unique source reporting record; use as the idempotent order key. |
| Business order reference | omnichannelOrderId | Retain for traceability and operational reconciliation. |
| Scope and timing | brandId, restaurantId, channelId, startDate, lastUpdateDate | Use for ownership, reporting dimensions, source-time analysis, and incremental processing. |
| Business date | businessYear, businessMonth, businessDay | Build the business date in the target time/reporting model. |
| Sales line key | items\[\].itemId | Use with reportOrderId to make a unique line key. |
| Parent/add-in relationship | items\[\].parentItemId, items\[\].itemType | Keep the parent link and item type so products, customizations, and cross-selling lines remain distinguishable. |
| Product reference | items\[\].skuValue; data.products\[productId\].sku.value | SKU may be null or absent. Preserve the product/item ID as a fallback source identifier. |
| Amounts | totalPrice\*, totalDiscount, price | Values are in cents. Convert by 100 only if the target stores major currency units. |
| VAT | items\[\].vatValue; data.products\[productId\].vat | Values are basis points: 1000 represents 10%. Divide by 100 to obtain a percentage. |
| Correction state | status | Persist status changes, including cancelled and contra-entry states, rather than deleting prior imports. |

### 5\. Refresh the product catalog

First list the menus accessible to the configured restaurant or brand. Select the menu using your integration configuration (for example a known menu ID and channel), then retrieve its full catalog snapshot.

### `GET /menus` - List accessible menus

Requires a bearer token. Supply a brandId or restaurantId scope and select the intended menu deterministically in your integration configuration.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| restaurantId | integer | Yes | Example scope for a restaurant-specific catalog. |

#### Response

```json
{
  "status": 200,
  "code": "menu_succeed",
  "message": "You can access these menus.",
  "data": [
    {
      "menuId": 28115,
      "name": "Main Menu",
      "restaurantId": 2337,
      "isActive": true,
      "channels": [
        {
          "channelId": 5
        }
      ]
    }
  ]
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "menu\_succeed" | Machine-readable application code for the result. |
| message | string | "You can access these menus." | Human-readable result message. Do not use this value for program logic. |
| data | array | \[…\] | Endpoint-specific response payload. |
| data\[\] | object | {…} | Endpoint-specific response payload. |
| data\[\].menuId | integer | 28115 | Identifier of the menu. |
| data\[\].name | string | "Main Menu" | The name value. |
| data\[\].restaurantId | integer | 2337 | Identifier of the restaurant. |
| data\[\].isActive | boolean | true | Whether this resource is active. |
| data\[\].channels | array | \[…\] | List of channels entries. |
| data\[\].channels\[\] | object | {…} | Object containing channels fields. |
| data\[\].channels\[\].channelId | integer | 5 | Identifier of the associated channel. |

### `GET /menus/{menuId}/hash_map` - Retrieve the full catalog snapshot

This endpoint is accessible as Guest and does not require a bearer token. consommation\_mode is optional and may affect catalog VAT and availability.

#### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| menuId | integer | Yes | The configured menu identifier. |
| consommation\_mode | string | No | Configured consumption mode used consistently for catalog refreshes. |

#### Response

```json
{
  "status": 200,
  "code": "menu_hash_map_succeed",
  "message": "You can access these menus.",
  "data": {
    "layout": [
      {
        "categoryId": 437731,
        "products": [
          {
            "productId": 5973839
          }
        ]
      }
    ],
    "categories": {
      "437731": {
        "categoryId": 437731,
        "name": "Best sellers"
      }
    },
    "products": {
      "5973839": {
        "productId": 5973839,
        "name": "Menu Duo",
        "price": 2490,
        "vat": 1000,
        "sku": {
          "value": "C24"
        }
      }
    },
    "steps": {
      "2381448": {
        "stepId": 2381448,
        "name": "Choose your pizza"
      }
    }
  }
}
```

##### Response Properties

Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.

| Property | Type | Example | Description |
| --- | --- | --- | --- |
| status | integer | 200 | HTTP status code returned by the API. |
| code | string | "menu\_hash\_map\_succeed" | Machine-readable application code for the result. |
| message | string | "You can access these menus." | Human-readable result message. Do not use this value for program logic. |
| data | object | {…} | Endpoint-specific response payload. |
| data.layout | array | \[…\] | List of layout entries. |
| data.layout\[\] | object | {…} | Object containing layout fields. |
| data.layout\[\].categoryId | integer | 437731 | Identifier of the category. |
| data.layout\[\].products | array | \[…\] | List of products entries. |
| data.layout\[\].products\[\] | object | {…} | Object containing products fields. |
| data.layout\[\].products\[\].productId | integer | 5973839 | Identifier of the product. |
| data.categories | object | {…} | Object containing categories fields. |
| data.categories.437731 | object | {…} | Object containing 437731 fields. |
| data.categories.437731.categoryId | integer | 437731 | Identifier of the category. |
| data.categories.437731.name | string | "Best sellers" | The name value. |
| data.products | object | {…} | Object containing products fields. |
| data.products.5973839 | object | {…} | Object containing 5973839 fields. |
| data.products.5973839.productId | integer | 5973839 | Identifier of the product. |
| data.products.5973839.name | string | "Menu Duo" | The name value. |
| data.products.5973839.price | integer | 2490 | The price value. |
| data.products.5973839.vat | integer | 1000 | The vat value. |
| data.products.5973839.sku | object | {…} | Object containing sku fields. |
| data.products.5973839.sku.value | string | "C24" | The value value. |
| data.steps | object | {…} | Object containing steps fields. |
| data.steps.2381448 | object | {…} | Object containing 2381448 fields. |
| data.steps.2381448.stepId | integer | 2381448 | Identifier of the associated step. |
| data.steps.2381448.name | string | "Choose your pizza" | The name value. |

The catalog payload structures are `layout`, `categories`, `products`, and `steps`. Metadata such as `menuId`, `name`, or `image` may also be present. Categories, products, and steps are hash maps keyed by their IDs; use `layout` to reconstruct the hierarchy.

### 6\. Handle failures without losing data

| Condition | Recommended behavior |
| --- | --- |
| 401 token\_expired | Authenticate again, then retry the affected idempotent read request. |
| 403 | Stop the job and verify service-account scope, restaurant/brand IDs, and menu configuration. |
| 429 or 5xx | Use bounded exponential backoff and retry the GET request; keep the same extraction window. |
| Invalid response or transformation failure | Quarantine/log the payload, fail the run safely, and do not advance the watermark. |
| Partial target load | Replay the window. Idempotent upserts prevent duplicates while completing the missing work. |

For complete endpoint contracts, see [Authentication](https://developers.innovorder.io/docs/authentication/authentication.md), [Retrieve Orders](https://developers.innovorder.io/docs/orders/orders-retrieve.md), and [Menus](https://developers.innovorder.io/docs/catalog/menu.md).
