# Operations and idempotency

> How VMYard accepts a change, carries it out, reports the truth about the result, and makes retries safe.

Every change to a machine (create, start, stop, restart, delete, running a command, writing a file, exposing a port, installing an SSH key, changing internet access) is an **operation**. The API answers `202 Accepted` right away and a worker carries out the change in the background.

```json title="202 Accepted"
{
  "operation_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "machine_id": "0b6f6c55-2f1d-4f0e-9a51-6f0f4cba7a10",
  "status": "pending",
  "operation_url": "/v1/operations/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
  "requested_action": "create"
}
```

## Following an operation

Poll `GET /v1/operations/{id}` until `status` is terminal, or stream `GET /v1/operations/{id}/events` as server-sent events. The event stream replays the persisted history first, then live events; send `Last-Event-ID` to resume after a disconnect.

| Status | Meaning |
| --- | --- |
| `pending` | Accepted and queued. |
| `running` | A worker is carrying it out. |
| `succeeded` | Done, and VMYard observed the requested result on the machine's host. |
| `failed` | It definitely did not happen. `error_code` says why. |
| `uncertain` | The outcome could not be confirmed yet. VMYard retries it with the exact same request until it settles. |
| `correlation_miss` | The work finished but the observed machine did not match what was asked for. Treat it as not done. |

`succeeded`, `failed`, and `correlation_miss` are terminal. The SDKs and `vy … --wait` poll for you (250 ms, growing to 2 s, for up to 10 minutes) and raise an error for anything but `succeeded`. They raise on `uncertain` too; replaying the same request with the same idempotency key is safe and picks the operation up again. A wait that times out does not cancel the operation.

> [!IMPORTANT]
> A worker finishing is not the same as the change happening. An operation only succeeds when VMYard sees the expected state on the host: a running machine with a new boot after a restart, a machine that is gone after a delete. If `completed_but_unchanged` is `true`, the machine did not change; don't report it as done.

## Ordering

Operations on one machine run one at a time, in the order they were accepted. A long command on one machine never delays another machine.

## Idempotency keys

Send a unique `Idempotency-Key` header with every request that creates an operation. The key is scoped to your organization.

- Repeating a request with the **same key and the same body** returns the original operation instead of doing the work again. This is how you recover from a timeout or a dropped connection: retry with the same key.
- Using a key again with a **different body** is refused with `409 idempotency_conflict`. For creates this includes the name, auto-stop, idle sleep, and setup script.
- A new key is a new request.

```sh
KEY=$(uuidgen)
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/stop" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" -d '{}'
# Safe to repeat with the same $KEY: you get the same operation back.
```

The SDKs generate a UUID key for every mutation and reuse it when they retry. Pass your own (`idempotencyKey` / `idempotency_key`) when you need to retry across process restarts.

Settings changes (`PATCH /v1/machines/{id}`), port access tokens, environments, webhooks, API keys, billing, and sign-in routes are not operations and take no idempotency key; they answer synchronously. Upgrading an environment creates operations, but its keys are derived from the version and the machine, so repeating the request replays them.

## Retries

- Retry reads freely.
- Retry a mutation only with the same idempotency key and body.
- The SDKs retry reads on connection errors and `408`, `429`, `502`, `503`, and `504`; they retry mutations only on `429` and `503`, always with the same key, and honour `Retry-After`.

## Operation results

A terminal operation keeps its result. Exec operations carry `exec_result` (exit code, base64 output, and `timed_out`, `cancelled`, `truncated`). A completed file read carries `file_ranges`. A `stop` carries `stop_reason`: `api` when a caller asked for it, or `auto_stop`, `billing`, `idle_sleep`, `suspension`, or `abuse` when VMYard stopped the machine itself ([auto-stop](/docs/concepts/auto-stop), an exhausted balance, [idle sleep](/docs/concepts/idle-sleep), a [suspension](/docs/guides/billing#suspended-organizations) of the organization, or the guard against sustained full CPU use on trials); other kinds have `null`. See [Running commands](/docs/guides/commands) and [Files](/docs/guides/files).
