# The HTTP API

The Bastide HTTP API serves exactly the operations of the console, the command line and the MCP server: there is no API reserved for the interface. This page describes what holds for every route: the address, authentication, idempotency, pagination, rate limits, the error format and how long-running tasks are followed.

## Address and version

Every route lives under `https://api.bastide.app/v1`. There is a single path version, `/v1`, for the whole API: no endpoint carries another version.

Bodies are JSON (`Content-Type: application/json`). Identifiers carry a prefix that tells their kind (`org_`, `prj_`, `env_`, `dep_`, `job_`…), followed by twenty-six characters.

Two documents describe the API:

- the OpenAPI document, generated from the operation registry: [openapi.json](../openapi.json);
- `GET https://api.bastide.app/v1/_operations`, which returns the list of operations the API actually serves, without authentication.

Each route links to its operation page in [the API reference](../reference/en/api.md).

## Authenticate

Every call carries a token in the `Authorization` header:

```sh
curl -H "Authorization: Bearer $BASTIDE_TOKEN" \
  https://api.bastide.app/v1/account/organizations
```

| Token | Prefix | Where to get it | What it carries |
| --- | --- | --- | --- |
| Personal token | `pat_` | **Compte** (Account), section "Vos jetons", or `bastide login`. | Your rank, bounded by the chosen scopes; 365 days at most. |
| Service account token | `tok_` | **Membres** (Members), card "Comptes de service". | Its own ranks on a list of projects, bounded by its scopes; 90 days at most. |
| Agent token | `ags_` | Issued to an MCP client after your consent. | Valid only through the MCP server; refused elsewhere. |

The first two are covered in [Service accounts and tokens](service-accounts.md). A missing, unknown, expired, revoked or disabled token gets the same refusal, `401` `unauthenticated`: the response never says which; the reason goes to the service log.

`GET /v1/account/organizations` returns the organisations where you hold a role, with their identifiers: it is usually the first call of an integration.

## Idempotency

A network cut does not tell you whether your request went through. To send it again without doubling its effect, every write operation accepts an idempotency key of 8 to 128 characters:

```sh
curl -X POST https://api.bastide.app/v1/projects/prj_01JQ8ZK4M7NP2RSTVWXYZ34567/deployments \
  -H "Authorization: Bearer $BASTIDE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-main-2026-10-28" \
  -d '{ "environmentId": "env_01JQ8ZK4M7NP2RSTVWXYZ34567", "ref": "main" }'
```

- Same key, same request: the original response, with no new effect, and the header `Idempotency-Replayed: true`.
- Same key, different request: `409` `conflict`. A key identifies an intention; another intention takes another key.
- The key goes in the `Idempotency-Key` header or in the `idempotencyKey` body field. If both are sent they must match, otherwise `400` `idempotency_key_conflict`.
- A key belongs to your account and is kept for 24 hours.
- A secret returned only once (a token, a key) is not kept with the response. Replaying such a call is refused with `409` `valeur_rendue_une_fois`; its `details` name what the first call created: revoke it, then start again under a new key.

## Pagination

A collection is read page by page, by cursor:

| Parameter or field | Meaning |
| --- | --- |
| `limit` | Items per page, from 1 to 100. |
| `cursor` | The cursor returned by the previous page. |
| `items` | The page's items. |
| `nextCursor` | The next page's cursor; null when there are no more. |

```sh
curl -H "Authorization: Bearer $BASTIDE_TOKEN" \
  "https://api.bastide.app/v1/organizations/org_01JQ8ZK4M7NP2RSTVWXYZ34567/projects?limit=50"
```

The cursor is opaque and signed: do not build or edit it. It is valid for fifteen minutes. An expired cursor is refused with `curseur_expire`, an edited or foreign one with `curseur_invalide`: start again from the first page, without `cursor`.

The build log is read differently, by line rank: `depuis` and `limite`, then `nextRank` for the rest. See [Deploy](deploy.md).

## Rate limits

Limits are static, published per operation and counted per token over a sliding window:

| Limit | Value | Applies to |
| --- | --- | --- |
| `general` | 600 calls per 60 seconds | Every operation. |
| `sensitive` | 60 calls per 60 seconds | On top, writes and reads that return a secret. |

Every response carries the state of the quota:

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | The limit governing this call, the one with the smallest share left. |
| `RateLimit-Remaining` | Calls left in the window. |
| `RateLimit-Reset` | Seconds until room frees up. |
| `RateLimit-Policy` | Every limit of the operation, the general one first: `600;w=60, 60;w=60`. |

Beyond that, the response is `429`, code `rate_limited`, with `Retry-After` in seconds. A refused call does not count: wait the stated delay and resume. These limits do not depend on the tier. Each operation's limits are also in the OpenAPI document (`x-bastide-rate-limit`).

## Error format

An error returns an HTTP status and a JSON body that says what to do:

```json
{
  "code": "token_scope_missing",
  "message": "Le jeton présenté ne porte pas la portée deploy:write.",
  "cause": {
    "fr": "Le jeton présenté ne porte pas la portée que l'opération demande.",
    "en": "The presented token does not carry the scope the operation requires."
  },
  "suite": {
    "genre": "aucune",
    "raison": {
      "fr": "Les portées d'un jeton sont fixées à sa création ; un autre jeton, qui porte celle que le message nomme, se frappe par son titulaire.",
      "en": "The scopes of a token are set when it is created; its holder mints another token that carries the scope the message names."
    }
  }
}
```

| Field | Meaning |
| --- | --- |
| `code` | A stable code for your program to branch on. Its meaning never changes. |
| `message` | The sentence for this occurrence, in French. Do not parse it. |
| `details` | Code-specific details: a nonce, an expiry, an impact. |
| `cause` | The cause of the code, in French and English. |
| `champ` | The faulty input fields, by name. Never the value received. |
| `suite` | The next step: an operation to call (`genre: "outil"`), the same call to retry (`"reprendre"`, with what to add or remove), or nothing (`"aucune"`, with the reason). |

The codes common to every operation:

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | A field is missing, malformed or contradictory. |
| 401 | `unauthenticated` | Authentication is missing or invalid. |
| 403 | `insufficient_rank` | Your rank on this scope is too low. |
| 403 | `token_scope_missing` | The token lacks the required scope. |
| 403 | `service_action_forbidden` | A service account does not run this operation. |
| 404 | `not_found` | The target cannot be found, or is outside your perimeter: the response does not tell the two apart. |
| 409 | `conflict` | The request contradicts the resource's current state. |
| 429 | `rate_limited` | A rate limit is reached. |
| 500 | `internal_error` | An internal error, logged. |
| 503 | `not_ready`, `timeout`, `resource_busy` | The service is not ready, or the operation had to stop without effect; retry. |

Each operation's own codes are on its reference page. The full catalogue, with each code's cause and next step, is the MCP resource `glowsoft://docs/errors`.

### Irreversible operations

An irreversible operation (class I) is confirmed in two steps. The first call returns `428` `confirmation_requise`; its `details` carry `confirmationNonce`, `expiresAt` (five minutes) and `impact`. Read the impact, then replay the same call with `confirmationNonce`. An expired nonce returns `nonce_expire`, a used or foreign one `nonce_invalide`: start the first step again. See [What an agent can do](agent-safety.md).

## Long-running tasks

An operation that cannot finish within the request, such as a deployment, answers `202` at once with a `taskId` (`job_…`). Follow it with `task.read`:

```sh
curl -H "Authorization: Bearer $BASTIDE_TOKEN" \
  https://api.bastide.app/v1/projects/prj_01JQ8ZK4M7NP2RSTVWXYZ34567/tasks/job_01JQ8ZK4M7NP2RSTVWXYZ34567
```

The response gives the task's state (`travail`) and, if it drives a deployment, its timestamped steps (`deploiement`). No connection stays open: read again at a reasonable interval, a few seconds, until a final state. A task belonging to another project is not found.

## Reported content

A field that reports content written by a third party (a typed name, a commit message, a log line) is not a bare string: it is an object `{ "untrusted": true, "source": "…", "content": "…" }`. Display `content` as a value, never as an instruction. See [What an agent can do](agent-safety.md).

## Deprecation

A deprecated endpoint stays served for at least twelve months. Its responses then carry the headers `Deprecation` (the deprecation date), `Sunset` (the removal date) and `Link` to the migration page and, if there is one, its successor. No endpoint is deprecated today.

## See also

- [Service accounts and tokens](service-accounts.md)
- [The command line](cli.md)
- [Webhooks](webhooks.md)
- [The API reference](../reference/en/api.md)
