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

# API conventions

> The rules every call follows: versions, request ids, the one error shape, rate-limit headers, retries, paging, key rotation and how a change is announced before it lands.

Every action on the API follows the same rules, so what you learn from one call
holds for all of them. This page lists those rules in one place.

## Versions and the beta marker

The version is in the address (`/api/v1/`) and in the API description
(`1.0.0-beta`). While the API is in beta, every response carries an
`x-goosy-api: beta` header.

Beta does not mean anything can change at any moment. A change either **adds**
something — a new action, a new optional argument, a new field in an answer, a
new refusal code — or it is **deprecated first** and removed later (see
[Deprecations](#deprecations)). Write your code to ignore fields it does not
know, and a new field will never break it.

## Request ids

Send an `x-request-id` header with your own id for the call, and you get the
same id back:

```http theme={null}
POST /api/v1/tools/credits.balance
Authorization: Bearer $GOOSY_API_KEY
x-request-id: nightly-sync-2026-09-29-0412
```

```http theme={null}
HTTP/2 200
x-request-id: nightly-sync-2026-09-29-0412
```

* **Every response carries one**, refusals included. If you send none, or one
  we cannot use, we generate one and send that back instead.
* An id is 1–128 characters from `A–Z a–z 0–9 . _ : -`, starting with a letter
  or a digit.
* **Log it.** The id is recorded on the activity trail for the call, so quoting
  it to support finds your call without a timestamp hunt.

## One error shape

Every failure you can read has `ok: false`, a `code` and a `message` at the top
level — whether the request was turned away at the door or the action itself
declined:

```json theme={null}
{ "ok": false, "code": "tenant_mcp.rate_limited", "message": "…", "retry_after_seconds": 7 }
```

**Branch on `ok`, then on `code`.** The status tells you which layer answered:

| Status | What happened |
| - | - |
| `200` | The call reached the action. `ok` says how it went — a declined action is still a `200`. The `x-goosy-tool-error` header repeats the outcome for code that would rather not read the body. |
| `400` `401` `403` `404` `429` | The request was turned away before any action ran. Nothing happened. |

Refusals at the door also carry the same values in a nested `error` object.
That object is [deprecated](#deprecations): read the top-level fields. Every
code is listed in [Errors and refusals](/api/errors-and-refusals).

## Rate-limit headers

Every call that counted against your key's per-minute allowance tells you where
you stand:

```http theme={null}
RateLimit-Limit: 60
RateLimit-Remaining: 41
RateLimit-Reset: 17
```

| Header | Meaning |
| - | - |
| `RateLimit-Limit` | Calls this key may make in a rolling minute. |
| `RateLimit-Remaining` | Calls left right now. |
| `RateLimit-Reset` | Seconds until more calls free up. |

Slow down when `RateLimit-Remaining` gets low, rather than waiting for a `429`.
A `429` adds `Retry-After`. Very rarely a response carries none of these three —
that means we could not count the call, and we let it through rather than
refuse you for our own fault. More in [Rate limits](/api/rate-limits).

## Retrying safely: `Idempotency-Key`

A network can fail after an action ran but before you heard back. To retry
without doing the work twice, send an `Idempotency-Key` header with a key you
choose, and send the **same key** when you retry:

```http theme={null}
POST /api/v1/tools/data.create_table
Authorization: Bearer $GOOSY_API_KEY
Idempotency-Key: sponsors-table-2026-09-29
```

* The retry gets the first result back instead of a second table.
* A key is 1–200 visible characters with no spaces, and it is honoured for at
  least **24 hours**.
* The API reference shows the header on every action that takes one, and
  marks it required on an action that needs a key. On an action that does not, a key is refused with
  `tenant_api.idempotency_unsupported` and nothing runs — check whether the
  first call landed before you retry it.
* Sending the key in the body as `idempotency_key` still works, but it is
  [deprecated](#deprecations) on the API. If you send both, they must match
  (`tenant_api.idempotency_key_conflict` otherwise).

Over MCP there is no header, so assistants keep passing `idempotency_key` as an
argument.

## Paging through a list

A list action takes `limit` and answers `next_cursor` while there is more. Pass
it back as `cursor`, with the same filters, to get the next page:

```json theme={null}
{ "limit": 25 }
```

```json theme={null}
{ "ok": true, "pieces": [ … ], "next_cursor": "eyJ2IjoxLCJvIjoyNSwiZiI6Ij…" }
```

```json theme={null}
{ "limit": 25, "cursor": "eyJ2IjoxLCJvIjoyNSwiZiI6Ij…" }
```

When `next_cursor` is missing, you have the last page. Treat the cursor as an
opaque string: do not build or edit one. A cursor used with different filters
is refused with `invalid_arguments`; start again without it. `content.status`
also still answers `truncated`, which is [deprecated](#deprecations) in favour
of `next_cursor`. Searches (`knowledge.search`, `library.search`) answer their
best matches and have no pages; narrow the query instead.

## Rotating a key

`keys.rotate` replaces the key that makes the call with a new one. It takes two
calls, like other actions that change who can use something:

1. Call it with an optional `overlap_hours` (default 24, at most 168). You get
   a `confirmation_id` back and nothing changes yet.
2. Call it again **with the same key**, the same `overlap_hours` and the
   `confirmation_id`. The answer carries the new key in `key`. It is shown
   once, so store it straight away.

The old key keeps working for `overlap_hours`, then stops with the ordinary
`401`. Use the overlap to deploy the new key everywhere the old one was. A
confirmation sent with any other key is refused. Keys for a managed site are
rotated by the platform and refuse this action.

A confirmation is used up the moment it is accepted, so it can never make two
new keys. If the rotation then fails (`keys.rotate_failed`), nothing changed:
call again with no `confirmation_id` to start a fresh one. In the rare case the
answer is `keys.rotate_successor_not_withdrawn`, a new key with the prefix the
message names may still be active — revoke it in **Settings › API & MCP**. Your
current key is unchanged.

## Deprecations

When something you can send or read is going away, we:

1. announce it here and in the [changelog](/changelog), with the date it may be
   removed;
2. mark every response to a call that uses it — or whose answer carries it,
   like `truncated` on `content.status` — with three headers —
   `Deprecation` (when it was deprecated), `Sunset` (the earliest removal date)
   and `Link` (this section);
3. keep it working for **at least 90 days**, and never remove it before the
   sunset date.

```http theme={null}
Deprecation: @1790640000
Sunset: Thu, 31 Dec 2026 00:00:00 GMT
Link: <https://docs.goosybear.ai/api/conventions#deprecations>; rel="deprecation"
```

Currently deprecated:

| What | Use instead | Removed no earlier than |
| - | - | - |
| The nested `error` object on a refusal at the door | `ok`, `code` and `message` at the top level — the same values | 31 December 2026 |
| `idempotency_key` in the body of an API call | The `Idempotency-Key` header. Over MCP, the argument stays | 31 December 2026 |
| `truncated` on `content.status` | Page with `cursor` until `next_cursor` is missing | 31 December 2026 |

<Tip>
  Watch for the `Deprecation` header in your client's logs. It only appears on
  calls that use something deprecated, so seeing it is the signal to change that
  call.
</Tip>
