# Asynchronous operations

> Commands to a product travel from the API through a Gateway and over Bluetooth, which
> takes seconds, not milliseconds. So the API answers straight away with an `event_id`,
> and tells you the result later, over the [realtime connection](/realtime/connecting).
> This page covers the whole lifecycle.

It applies to [`POST /v2/products/{id}/state`](/api/endpoints/products-change-state) and
[`POST /v2/products/{id}/settings`](/api/endpoints/products-change-settings).

## The lifecycle

| Step | Who | What happens |
| --- | --- | --- |
| 1 | You | Connect to realtime, and wait until you're connected. |
| 2 | You → API | Send the command, for example `{"operation": "open"}`. |
| 3 | API | Checks the command can run. If not, answers with an HTTP error (below) and stops. |
| 4 | API → you | Answers `200` with `{"success": true, "data": {"event_id": "a1b2c3d4"}}`. The device hasn't moved yet. |
| 5 | API → Gateway | Sends the command to the first candidate Gateway and waits up to **30 seconds** for its answer. |
| 6 | Gateway → product | Relays the command over Bluetooth; the product acts and reports back. |
| 7a | API → you | **Success:** a `lot_change` event with the same `event_id` and the product's new state. |
| 7b | API → you | **Failure:** a `gateway_error` event with the same `event_id`. |

`event_id` is 8 hexadecimal characters.

## How the Gateway is chosen

The candidates are the Gateways that currently see the product over Bluetooth. The API
tries them **one at a time, strongest signal first**, waiting up to 30 seconds for each:

- If a Gateway answers that the operation worked, you get `lot_change`.
- If it times out, or fails for a reason another Gateway might not hit (for example a
  Bluetooth error), the API moves on to the next Gateway.
- If the product itself reports a problem that no other Gateway can fix (`type` 0, 1,
  5, 6, 7 or 8 below: for example jammed, or a car on top), it stops there and you get
  `gateway_error`.
- If every Gateway fails, you get `gateway_error` with the last error.

So with several Gateways in range, a result can take a multiple of 30 seconds.

## Errors before anything is sent

These come back as HTTP errors in step 3, and nothing is sent to the device:

| HTTP | Code | Meaning | What to do |
| --- | --- | --- | --- |
| 403 | `30` | The product's subscription doesn't include remote control. | See [Plans and permissions](/concepts/plans-and-permissions). |
| 403 | `4` | The product isn't visible to your token. | Check the product is on a lot your token can see; see [Data model](/concepts/data-model#what-an-api-token-sees). |
| 404 | `6` | No product with that id. | Check the id. |
| 403 | `214` | An earlier operation on this product hasn't finished. | Wait for its result; don't retry in a loop. |
| 403 | `704` | No Gateway currently sees the product. | Check the Gateway is online, on the same lot and in range. |
| 403 | `206` | The product isn't paired. | Pair it with the Connect app. |
| 422 | `23` | Invalid body, for example an unknown `operation`. | Fix the request. |

## `gateway_error` types

```json title="gateway_error"
{
  "type": 5,
  "event_id": "a1b2c3d4",
  "data": { "id": 123, "name": "Spot 12" },
  "message": "Device jammed"
}
```

When no Gateway answered in time, `type` is the **string** `"Timeout"` and there's no
`message`. Otherwise it's a number. These are **not** the HTTP error codes.

| `type` | `message` | Tries the next Gateway? |
| --- | --- | --- |
| `0` | Auth error | No |
| `1` | Invalid guest key | No |
| `2` | Device disconnected | Yes |
| `3` | Bluetooth error | Yes |
| `4` | Device not found | Yes |
| `5` | Device jammed | No |
| `6` | Car on top | No |
| `7` | Sensor dirty | No |
| `8` | Device not initialized | No |
| `9` | Rtcc not set | Yes |
| `10` | Device busy | Yes |
| `11` | Unknown error | Yes |
| `"Timeout"` | (none) | Yes |

## `wait_for_completion`

`POST /v2/products/{id}/state` accepts an optional `wait_for_completion` (default
`true`). It's passed on to the Gateway, which by default waits until the product reports
it has reached the new state. Set it to `false` for a **brain without input sensors**,
which can't report an open or closed state. It doesn't change the HTTP response: you
always get the `event_id` straight away.

## Writing a client

1. **Connect to realtime before sending commands**, with the same API token, so you
   can't miss the result.
2. **Keep the pending `event_id`s** in a map, and resolve each on the `lot_change` or
   `gateway_error` that carries it. The result can arrive before your HTTP call returns:
   buffer results you haven't matched yet.
3. **Ignore `event_id`s you didn't send.** Your connection also receives operations
   started by others (the Connect app, other users) and automatic ones, such as Parklio
   setting a device's clock.
4. **Give up client-side** after a timeout of your choice, and re-read the product with
   [`GET /v2/products/{id}`](/api/endpoints/products-find-one) to see where it ended up.
5. **One operation per product at a time.** A second command while the first is
   pending gets `214`.

There's no REST endpoint to look up a result by `event_id`, and no replay: if you were
disconnected when the event was sent, re-read the product's state over REST.

A working example is in the [Quickstart](/get-started/quickstart).
