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

# Operations API

Merchant-facing API for **staff mobile apps** that need to open/close a restaurant and update **today's inventory** (86'd items). This is not the Catalog API.

**Why a separate API?** Restaurant status and today's stock-outs apply to both the App Proxy ordering page and the theme-embed widget. They are operational writes, not catalog reads. A token with `catalog:read` cannot call these endpoints.

## Authentication

* In Zapiet Eats go to **Settings → API credentials**.
* Create a personal access token. Copy the secret once (it is not shown again).
* Send it on every request:

```http
Authorization: Bearer {token}
Accept: application/json
```

Optional (must match the token’s store if present):

```http
X-Zapiet-Eats-Shop-Url: your-store.myshopify.com
```

A mismatched shop header returns **403**.

### Scopes

| Scope        | Access                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------ |
| `operations` | **Read and write** restaurant status and today's inventory for the bound store.                  |
| `api:*`      | Full REST access. Accepted here so existing full-access tokens keep working when the flag is on. |

Issue `operations` to a staff mobile app. Do **not** put `api:*` in a mobile binary.

## Errors

```json
{ "error": "unauthorized", "message": "Unable to authenticate the operations request." }
```

| HTTP | `error`            | Meaning                                      |
| ---- | ------------------ | -------------------------------------------- |
| 401  | `unauthorized`     | Missing, expired, revoked, or unbound token  |
| 403  | `forbidden`        | Shop header does not match the token’s store |
| 403  | `missing_scope`    | Valid token without `operations` or `api:*`  |
| 404  | `not_found`        | Unknown id, other store’s id, or flag off    |
| 422  | `validation_error` | Bad body or query params                     |
| 429  | `rate_limited`     | Too many requests                            |

## REST

Base: `https://app.zapieteats.com/api/operations/v1`

Restaurant ids are the same ids Catalog API returns as `locations[].id`.

### List restaurants

```http
GET /api/operations/v1/restaurants
```

```json
{
    "data": [
        {
            "id": 916,
            "name": "Downtown",
            "slug": "downtown",
            "status": "open",
            "timezone": "Europe/London",
            "pickup": true,
            "delivery": true
        }
    ],
    "links": { "first": "...", "next": null, "prev": null }
}
```

`status` is `open` or `closed`. Closing a restaurant pauses pickup and delivery for that location (the same dashboard toggle).

### Open or close a restaurant

```http
PATCH /api/operations/v1/restaurants/{id}/status
```

```json
{ "status": "closed" }
```

Closing dispatches the same cancel-and-refund job as the admin dashboard for NEW orders. Reopening does not restore those orders.

### Today's inventory

Inventory rows are scoped to the restaurant's **local today** (`Restaurant::todayCountAt()`). Products with no row are treated as unlimited (not sold out).

```http
GET /api/operations/v1/restaurants/{id}/inventory
GET /api/operations/v1/restaurants/{id}/inventory?sold_out=1
GET /api/operations/v1/restaurants/{id}/inventory?menu_id=404
```

```json
{
    "data": [
        {
            "menu_id": 404,
            "product_id": "1234567890",
            "variant_id": "9876543210",
            "name": "Burger",
            "variant_name": "Large",
            "remaining": 0,
            "sold_out": true
        }
    ],
    "date": "2026-09-18",
    "timezone": "Europe/London",
    "links": { "first": "...", "next": null, "prev": null }
}
```

### Update today's inventory

```http
PATCH /api/operations/v1/restaurants/{id}/inventory
```

```json
{
    "items": [
        {
            "menu_id": 404,
            "product_id": "1234567890",
            "variant_id": "9876543210",
            "remaining": 0
        }
    ]
}
```

| `remaining` | Effect                                     |
| ----------- | ------------------------------------------ |
| `0`         | 86 / sold out for the rest of today        |
| `N`         | Set units remaining for today              |
| `null`      | Clear today's limit (product is unlimited) |

The response is the restaurant's current today's inventory after the write (re-queried from the database). Metafield sync jobs are dispatched the same way as the admin inventory modal.

## Rate limits

Default **40 requests per second**
