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

# API conventions

> Create an API key in the console, then call the Managed Agents REST API or the authenticated MCP endpoint: base URL, authentication, asynchronous starts, idempotency, pagination, errors, rate limits, and request ids.

You reach Managed Agents from code over REST or MCP, and both use the same API key:

1. **Create an API key** in the [Recursion console](https://recursion.labelbox.com) under **Settings › API keys**. See [API keys](/recursion/api-keys).
2. **Call the REST API** at `https://api.recursion.labelbox.com/managed-agents/v1` with the key as a bearer token. This page covers its rules.
3. **Or connect an MCP client** to the authenticated MCP endpoint at `https://api.recursion.labelbox.com/mcp` with the same key. See [Connect to the Recursion MCP](/recursion/ai-coding-agents).

Every operation follows the same rules for authentication, retries, paging, and errors. Read this page once, then use the generated **Endpoints** reference as the authority for each operation's exact schema, error alternatives, headers, and retry strategy.

## Base URL

Every operation lives under one base path:

```text theme={"theme":"css-variables"}
https://api.recursion.labelbox.com/managed-agents/v1
```

Requests and responses are JSON. Most fields use `snake_case`. A few responses, such as the model list, use `camelCase`, so follow each operation's schema in **Endpoints**.

## Authentication

Send an API key as a bearer token on every request:

```http theme={"theme":"css-variables"}
Authorization: Bearer rma_...
```

Create the key in the console under **Settings › API keys**. See [API keys](/recursion/api-keys) for scopes, expiry, and safe storage.

| Key scope | Extra header | Where requests act |
| - | - | - |
| Organization | None. You may send `x-organization-id`, but it must name the key's organization. | The key's organization. |
| Tenant | `x-organization-id: <organization id>` or `x-organization-id: default`. Required on every request. | The organization the header names, if you belong to it. |

A key acts as you, with the role you hold in that organization at the moment of the request. If an admin changes your role, your keys follow on the next request. Once the organization is resolved, the response carries `recursion-organization-id` and `recursion-tenant-id` headers that name the scope the request ran in.

| Response | Cause |
| - | - |
| `401 unauthorized` | The key is missing, malformed, revoked, or expired, it belongs to a person who left the tenant, or its organization was archived. |
| `400 invalid_request` | A tenant-scoped key sent no `x-organization-id`. |
| `404 not_found` | `x-organization-id` names an organization you can't reach, an archived one, or a different organization than an organization-scoped key's own. |
| `403 forbidden` | Your role in the organization doesn't allow the operation. See [Organizations and roles](/recursion/organizations-and-roles). |

## REST client setup

Export your key, then send it as a bearer token. Every page shows each request as cURL, and **Endpoints** has each operation's exact schema. See [Python and other languages](#python-and-other-languages) to translate the same requests to an HTTP client.

```bash theme={"theme":"css-variables"}
export RECURSION_API_KEY='rma_...'
```

<Tabs>
  <Tab title="cURL">
    ```bash theme={"theme":"css-variables"}
    curl 'https://api.recursion.labelbox.com/managed-agents/v1/models' \
      -H "Authorization: Bearer $RECURSION_API_KEY"
    ```
  </Tab>
</Tabs>

A successful call returns the models you can use in agents:

The values below illustrate the response shape; use a returned `modelId` when creating an agent.

```json theme={"theme":"css-variables"}
{
  "items": [
    {
      "modelId": "<model-id>",
      "displayName": "<display-name>",
      "family": "<provider-route>",
      "supportsImageInput": true
    }
  ]
}
```

### Tenant-scoped keys

A tenant-scoped key must name the organization on every request. Use an organization id, or `default` for your tenant's default organization.

<Tabs>
  <Tab title="cURL">
    ```bash theme={"theme":"css-variables"}
    curl 'https://api.recursion.labelbox.com/managed-agents/v1/models' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H 'x-organization-id: default'
    ```
  </Tab>
</Tabs>

### Call shape

Call `https://api.recursion.labelbox.com/managed-agents/v1/<path>`. Send JSON bodies with `Content-Type: application/json`.

Send ordinary request bodies as JSON. A body in a media type the API doesn't accept returns `415 unsupported_media_type`. Two kinds of upload use `multipart/form-data` instead: skill bundle uploads and file uploads with `uploadFile`. See [Skills](/recursion/skills) and [Files](/recursion/files).

### Python and other languages

Send the same REST requests the cURL samples show from any HTTP client, with the bearer header on each one. For example, with `httpx`:

```python theme={"theme":"css-variables"}
import os

import httpx

api = httpx.Client(
    base_url="https://api.recursion.labelbox.com/managed-agents/v1",
    headers={"Authorization": f"Bearer {os.environ['RECURSION_API_KEY']}"},
)
models = api.get("/models").json()
```

Translate any cURL sample the same way: the path follows `/managed-agents/v1`, `-d` becomes a JSON body, and each `-H` becomes a header, such as `Idempotency-Key` on `startSession`.

Use a client such as `httpx` or `requests`. Requests sent with the defaults of Python's built-in `urllib` may be refused.

## Asynchronous session starts

`startSession` returns `202 Accepted` as soon as the session is recorded. It doesn't wait for the sandbox or the agent.

```json theme={"theme":"css-variables"}
{
  "session_id": "b7e1c9a4-3d62-4f15-8a07-5c2e9f6d1b38",
  "status_path": "/managed-agents/v1/sessions/b7e1c9a4-3d62-4f15-8a07-5c2e9f6d1b38"
}
```

Follow the session with `getSession` or by [streaming its events](/recursion/events). The session record is authoritative. The event stream is written separately and can lag it, so an empty event list doesn't mean the session is still starting.

What success means depends on the work:

| Work | Done when |
| - | - |
| Finished turn | `execution_state` is `idle` and `stop_reason` says why. `end_turn` means the agent finished its turn, not that its work is correct. |
| Failed session | `status` is `failed`. Read `failure.code`, `failure.category`, and `failure.retryable`. See [Errors](/recursion/errors#session-failure-codes). |

## Idempotent mutations

Mutations with declared retry semantics publish their strategy in the API contract. This inventory is generated from that same contract and includes only operations published by the standalone API:

The operations currently in each strategy are:

* `key-required`: `createAutomation`, `createEnvironmentSetupRun`, `createEventSource`, `runAutomation`, `startSession`
* `key-supported`: `createAgent`, `createEnvironment`, `createVault`, `createVaultCredential`, `sendSessionEvents`, `updateAgentMemorySettings`, `updateEnvironment`
* `retry-unsafe`: `completeIntegrationInstall`, `createAgentVersion`, `createSkill`, `createSkillVersion`, `deleteAutomation`, `deleteEventSource`, `openAgentAnalyst`, `openAssistant`, `pauseAutomation`, `pauseEventSource`, `publishSkillBundle`, `publishSkillVersionBundle`, `replaceAutomation`, `replaceEventSource`, `resumeAutomation`, `resumeEventSource`, `startIntegrationInstall`, `submitAssistantInteractionDecision`, `updateVault`, `uploadFile`

A `key-required` operation rejects a missing `Idempotency-Key`. A `key-supported` operation accepts the header optionally. Use a stable key for each logical mutation so an ambiguous network retry cannot repeat its effect.

| Retry | Result |
| - | - |
| Same key, exact method, escaped path, raw query, and raw body bytes | The original successful response is replayed with `Idempotency-Replayed: true`. |
| Same key with any different request identity bytes | `409 idempotency_conflict`. |
| Same key while the first request is still running | `409 idempotency_in_progress` with `Retry-After`. |
| The key cannot be read or reserved | `503 idempotency_unavailable`. The request did not run and may be retried with the same key. |

Keys hold 1 to 256 visible ASCII characters, sent as exactly one header value. A malformed or repeated header returns `400 invalid_request`. Keys share one namespace per organization and across keyed operations, so use a different key for each logical mutation. A completed key is remembered for about 24 hours, and an abandoned in-progress key can be reused after about 1 hour.

Send `Idempotency-Key` as a request header. Retry with the same request target and body bytes: JSON whitespace and key order, query ordering and escaping, and path escaping are significant.

For `createVault`, pass the optional `Idempotency-Key` header. The retired body field `idempotency_key` is rejected. The key also determines the vault id, so an unchanged vault remains recoverable after the response receipt expires. If that vault was changed or deleted, reuse conflicts and a replacement requires a new key.

A `naturally-idempotent` operation is safe to repeat by its own resource semantics. Do not automatically retry a `retry-unsafe` operation after an ambiguous transport failure: inspect the affected resource or operation state first. An undeclared operation has no replay guarantee. Sending `Idempotency-Key` to any of these operations does not add one.

## Pagination

Paged list operations return a continuation token. Each operation documents its own default and maximum page size.

| Field | Use |
| - | - |
| `limit` | Maximum items to return in one page. |
| `page_token` | The previous response's `next_page_token`. Keep the same filters and `limit`. |
| `next_page_token` | Present when more items exist. Stop when it's absent or empty. |
| `after_event_id` | Continue `listSessionEvents`. Pass that response's `next_page_token` here. |

A page can hold fewer items than `limit` and still have a next token, because event pages also have a size budget. A session-list token is valid for one hour and only with the filters and `limit` it was issued for. A token that's no longer valid returns `400 invalid_request` with `details.field` set to `page_token`, so restart the list without it.

## Errors

Every `4xx` and `5xx` response is a flat object with required `code` and `message` fields and optional `details`:

```json theme={"theme":"css-variables"}
{
  "code": "invalid_request",
  "message": "agent_id is required",
  "details": {
    "field": "agent_id",
    "requestId": "0b6f2c1d-9a4e-4b7f-8c3d-5e6a7b8c9d0e"
  }
}
```

| Field | Use |
| - | - |
| `code` | Required. Stable and machine-readable. Branch on this. |
| `message` | Required. Human-readable and changeable. Don't match on it. |
| `details` | Optional code-specific context, measured values, limits, or next actions. |
| `details.field` | The request field, query parameter, or header at fault, when there is one. |
| `details.issues` | Boundary-validation failures as path-prefixed messages, when present. |
| `details.requestId` | The request id. Include it when you report a problem. |
| `details.retryable` | Server advice about transience: `true` means transient, `false` means non-transient, and absence gives no advice. Automatic replay is allowed only when this value is not `false` and the operation's retry strategy permits it. |

Check the HTTP status and `code` on every non-`2xx` response. Use the generated **Endpoints** reference for an operation's exact status and code alternatives. See [Errors](/recursion/errors) for common recovery guidance.

## Rate limits

Requests are limited to 300 per 60 seconds for each of your allowances: all your API keys share one, in every tenant and organization, and your console use has its own, so a busy script doesn't lock you out of the console. Event streams are also limited in how many can be open at once.

| Response | Meaning | What to do |
| - | - | - |
| `429 rate_limit_exceeded` | You sent too many requests in a short window. | If replay is safe, wait for `Retry-After` and retry. |
| `503 service_unavailable` on a stream | The service is at stream capacity. | Close streams you no longer need, then retry with backoff. |

`Retry-After` supplies timing, not replay permission. Honor it only when the operation's generated retry strategy permits replay and `details.retryable` is not `false`. When a permitted retry has no `Retry-After`, back off exponentially with jitter, starting around one second. See [Limits](/recursion/limits) for every other limit.

## Request ids

Every response carries an `x-request-id` header, and error bodies repeat it as `details.requestId`. You can send your own `x-request-id` of up to 128 letters, digits, `_`, `.`, `:`, or `-`, and it's echoed back so you can match it to your own logs.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" href="/recursion/quickstart">
    Create an environment and an agent, start a session, and read the reply.
  </Card>

  <Card title="Errors" href="/recursion/errors">
    Look up every error code with its cause, fix, and retry guidance.
  </Card>

  <Card title="Limits" href="/recursion/limits">
    Find every size, count, and time limit in one place.
  </Card>

  <Card title="Troubleshooting" href="/recursion/troubleshooting">
    Match a symptom to its cause and fix.
  </Card>
</CardGroup>
