> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API patterns

> Authentication, idempotency, asynchronous state, resources, and retries in the hiloop API.

This page explains conventions used across the hiloop REST API. For exact routes and schemas, use
the [API reference](/api-reference).

## Base URL

Hosted API:

```text theme={null}
https://api.hiloop.ai
```

Self-hosted, BYOC, and on-prem deployments use deployment-specific API URLs:

```sh theme={null}
export HILOOP_API_URL="https://api.hiloop.example.com"
```

## Authentication

Send API keys as bearer tokens:

```sh theme={null}
curl -sS "${HILOOP_API_URL}/v1/whoami" \
  -H "Authorization: Bearer ${HILOOP_API_KEY}"
```

The organization is resolved from the credential. Do not include an organization id in request
bodies as an authority source.

## Organizations and scope

Your account is an **[organization](/concepts/glossary#organization)**, and it is the isolation
boundary: projects, sandboxes, runs, and telemetry all live in it. The edge stamps the caller's
organization from their credential, so a request can only ever touch its own organization's data.

Every request acts at one of two **[scopes](/concepts/glossary#scope)**:

* **Organization scope** — the default, and where the whole day-to-day surface lives: sandboxes,
  telemetry, projects, annotations, members, and organization-wide automation.
* **Sandbox scope** — a per-sandbox credential, further confined to its own run lineage. It can read
  its own sandbox and snapshot lineage and act only on itself; it cannot create, update, or delete
  sandboxes, or touch siblings.

From the CLI, `hiloop whoami` prints your resolved principal (its kind, id, email, and key name)
and organization.

API keys carry a fixed scope from the moment you mint them — mint a key with
[the scope](/guides/managing-api-keys) it needs.

## Deleting a project

`DELETE /v1/projects/{id}` deletes a project within your organization. By default a project that still has
resources — sandboxes, runs, scoped API keys, or scoped secrets — **cannot** be deleted:
the call returns a `conflict`, and you must remove those resources first.

To delete a project **and everything in it** in one call, set `cascade` to `true`. The cascade purges
the project's supported records, then removes the project. It is scoped to your organization, so it can
only ever touch your own data, and it is irreversible.

What a cascade never does is tear down a sandbox that still exists: delete the project's remaining
sandboxes first (`DELETE /v1/sandboxes/{id}` — the normal delete, which tears down the sandbox's
compute), then cascade.

One guard keeps a cascade honest and refuses the whole call atomically: every sandbox in the
project must already be deleted. While one is not, the call returns a
`conflict` with error code `sandboxes_not_deleted` — delete the remaining sandboxes, then retry.

```sh theme={null}
curl -sS -X DELETE "${HILOOP_API_URL}/v1/projects/${PROJECT_ID}?cascade=true" \
  -H "Authorization: Bearer ${HILOOP_API_KEY}"
```

From the CLI, the same delete is `hiloop projects delete <id|slug>` (add `--cascade` for the
cascade; it asks for confirmation unless you pass `--yes`).

The response reports what the delete actually removed, per resource, so scripts and audit logs can
verify the scope of the purge:

```json theme={null}
{
  "runs_deleted": "12",
  "volumes_deleted": "3",
  "secrets_deleted": "2",
  "api_keys_deleted": "1"
}
```

Counts are rendered as JSON strings (they are 64-bit integers). A non-cascading delete only ever
removes a project with no dependents, so its counts are all zero.

## Idempotency

An idempotency key is **optional** and applies to create-style mutations: create sandbox, snapshot
create, exec, and secret rotation. The SDKs generate one automatically. Direct API calls may omit it, but every
unkeyed call is a fresh mutation and must not be retried after an ambiguous failure. Pass your own
key when you want a retry to be safe across processes: reusing the same key with the same body
replays the original result, while reusing it with a different body is rejected with
`idempotency_conflict` (HTTP 409) — a key names one logical request, never a family of them.

The `hiloop` CLI takes the same key as `--idempotency-key` on these commands, and a key turns on
automatic retries for the request itself: an ambiguous failure — a timeout, a dropped connection,
a 5xx — is retried up to 3 times with the same key before the error surfaces. Without a key the
CLI never retries a mutation, because each attempt would be a fresh one.

Delete and the stop/start lifecycle transitions need no key: they are idempotent by sandbox id, so
repeating one converges on the same desired state rather than stacking up work.

To pass your own key, send it in the `idempotency-key` header:

```sh theme={null}
curl -sS -X POST "${HILOOP_API_URL}/v1/sandboxes" \
  -H "Authorization: Bearer ${HILOOP_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{
    "name": "experiment-a",
    "image": "ghcr.io/acme/agent-base:latest",
    "resources": { "cpu_millis": 50, "memory_mb": 128 }
  }'
```

## Asynchronous state

Sandbox mutations are asynchronous, but there is no separate operation resource to poll. A create,
update, or delete returns as soon as the request is accepted, and you poll **the sandbox itself**
until its observed `state` reaches the outcome you asked for:

```sh theme={null}
curl -sS "${HILOOP_API_URL}/v1/sandboxes/${SANDBOX_ID}" \
  -H "Authorization: Bearer ${HILOOP_API_KEY}"
```

Observed states are `requested`, `reserved`, `materializing`, `ready`, `running`, `stopped`,
`terminating`, `failed`, `terminated`, `quarantined`, and `attention`. The state you read is
evidence of what the runtime is doing, not a record of what was asked for. A sandbox that reached
`failed` or `quarantined` carries a `state_reason` explaining why; lifecycle states that need no
explanation omit it.

The CLI polls for you: `hiloop sandbox create` returns once the sandbox is running.

## Resource identifiers

Resource IDs are opaque. Do not parse them for meaning.

Recommended application state:

```json theme={null}
{
  "project_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
  "sandbox_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "snapshot_id": "2f1f5f9b-1d7f-4d91-9c91-7a2b2d856f5e",
  "run_id": "01K6Z000000000000000000000",
  "lineage_path": "01H8A.01H8B"
}
```

## Runtime endpoints

The sandbox surface is nine routes over two resources, sandboxes and snapshots:

| Endpoint                       | Method | Purpose                                                                  |
| ------------------------------ | ------ | ------------------------------------------------------------------------ |
| `/v1/sandboxes`                | GET    | List sandboxes.                                                          |
| `/v1/sandboxes`                | POST   | Create sandbox from an image, a snapshot, or the platform default image. |
| `/v1/sandboxes/{id}`           | GET    | Get sandbox.                                                             |
| `/v1/sandboxes/{id}`           | PATCH  | Update lifecycle state (`stopped` or `running`), TTL, or metadata.       |
| `/v1/sandboxes/{id}`           | DELETE | Delete sandbox (hard teardown).                                          |
| `/v1/sandboxes/{id}/exec`      | POST   | Run one buffered command.                                                |
| `/v1/sandboxes/{id}/snapshots` | POST   | Snapshot the sandbox.                                                    |
| `/v1/snapshots`                | GET    | List snapshots.                                                          |
| `/v1/snapshots/{id}`           | DELETE | Delete a snapshot.                                                       |

There is no capability-discovery route. A deployment states what it cannot serve by refusing the
request that needs it, with `unsupported_capability`; see
[resources and capabilities](/sandboxes/resources-capabilities#how-a-deployment-refuses).

## Retry strategy

| Request type                            | Retry?  | Notes                                                                                                                                                            |
| --------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET                                     | Yes     | Use exponential backoff.                                                                                                                                         |
| Create-style mutation                   | Yes     | Pass an idempotency key to make the retry safe across processes.                                                                                                 |
| Delete or lifecycle transition          | Yes     | Idempotent by sandbox id; no key needed.                                                                                                                         |
| Invalid request or permission failure   | No      | Fix request or credential scope.                                                                                                                                 |
| Quota or rate limit (HTTP 429)          | Depends | See [Rate limits and quotas](#rate-limits-and-quotas): retry after `Retry-After` for `rate_limited`; free capacity first for `quota_exceeded`.                   |
| Over capacity (HTTP 503, `unavailable`) | Yes     | The service shed the request before doing any work. Wait for the `Retry-After` header (a second or two), then retry — with an idempotency key the retry is safe. |
| Service error                           | Yes     | Back off and retry where safe.                                                                                                                                   |

## Rate limits and quotas

Per-organization limits are checked when a request is submitted. A request over a limit is rejected
immediately with HTTP 429 and a structured body naming the limit — work is never accepted and then
failed later for capacity reasons.

```json theme={null}
{
  "code": "quota_exceeded",
  "message": "running sandbox limit reached: 10 sandboxes may run at once; stop a running sandbox and retry",
  "details": {
    "quota": {
      "metric": "sandboxes.running",
      "limit": 10,
      "current": 10
    }
  }
}
```

Branch on the stable `code`, never on the message:

| Code             | Meaning                                                                                                                 | What to do                                                                                                                            |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `quota_exceeded` | The organization is at one of its limits. `details.quota.metric` names which. Retrying immediately hits the same limit. | Free capacity against that metric, or ask to raise the limit.                                                                         |
| `rate_limited`   | Requests for this verb are arriving faster than the organization's rate limit.                                          | Wait for the `Retry-After` header (also `details.quota.retry_after_seconds`), then retry — with an idempotency key the retry is safe. |

Every quota rejection uses the one `quota_exceeded` code, whichever limit it was: read
`details.quota.metric` to tell them apart, and a limit added later needs no new code to branch on.
`details.quota` also carries the configured `limit`, the observed `current` usage (concurrency and
storage caps only), and `retry_after_seconds` (rate limits only).

Limited metrics:

| Metric              | Limits                                                                        | Freed by                                            |
| ------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------- |
| `sandboxes.running` | Sandboxes running at once per organization. A stopped sandbox does not count. | Stopping or deleting a sandbox.                     |
| `sandboxes.total`   | Sandboxes held per organization, running and stopped together.                | Deleting a sandbox — stopping one does not free it. |
