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;
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.
Authenticate
Every call carries a token in the Authorization header:
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. 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:
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:
409conflict. A key identifies an intention; another intention takes another key. - The key goes in the
Idempotency-Keyheader or in theidempotencyKeybody field. If both are sent they must match, otherwise400idempotency_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
409valeur_rendue_une_fois; itsdetailsname 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. |
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.
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:
{
"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.
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:
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.
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.