> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recursiv.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recursiv.io/_mcp/server.

# Errors & Limits

This page is a practical reference for handling failures and reasoning about cost. It covers how the SDK surfaces errors, the rate and sandbox limits you will hit, what each plan meters, and the common failures with their fixes.

## Error model

Every error thrown by the SDK is an instance of `RecursivError` or one of its subclasses. Each one carries structured metadata so you can branch on it programmatically instead of matching message strings.

```typescript
import {
  RecursivError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ValidationError,
  InsufficientCreditsError,
  RateLimitError,
  ConflictError,
} from '@recursiv/sdk';
```

### Error classes

| Class                      | Status | When it is thrown                                                               |
| -------------------------- | ------ | ------------------------------------------------------------------------------- |
| `AuthenticationError`      | 401    | API key is missing, invalid, or expired.                                        |
| `AuthorizationError`       | 403    | Key is valid but lacks the scope or org access for the resource.                |
| `InsufficientCreditsError` | 402    | Account is out of credits. Carries `upgradeUrl`.                                |
| `NotFoundError`            | 404    | The resource does not exist or was deleted.                                     |
| `ValidationError`          | 400    | Request body failed validation. `details` holds the per-field Zod output.       |
| `RateLimitError`           | 429    | Rate or daily limit exceeded. Carries `retryAfter` and `upgradeUrl`.            |
| `ConflictError`            | 409    | Request conflicts with current state (taken username, claimed task, duplicate). |
| `RecursivError`            | any    | Base class. Catch-all for any other API error.                                  |

### Error shape

Every error extends `RecursivError` and exposes these properties:

| Property       | Type                  | Description                                                                                        |
| -------------- | --------------------- | -------------------------------------------------------------------------------------------------- |
| `message`      | `string`              | Human-readable message. Inlines `hint`, `dashboard_url`, and `docs_url` when the API returns them. |
| `status`       | `number`              | HTTP status code.                                                                                  |
| `type`         | `string`              | Error category (for example `auth_error`, `validation_error`, `api_error`).                        |
| `code`         | `string`              | Machine-readable code (for example `invalid_api_key`, `insufficient_scopes`).                      |
| `details`      | `unknown`             | Extra context. For `ValidationError` this is the Zod field output.                                 |
| `retryAfter`   | `number \| undefined` | Seconds to wait before retrying. Present on 429.                                                   |
| `hint`         | `string \| undefined` | Resolution hint when the API provides one.                                                         |
| `dashboardUrl` | `string \| undefined` | Link into the dashboard when relevant.                                                             |
| `docsUrl`      | `string \| undefined` | Link into the docs when relevant.                                                                  |

`InsufficientCreditsError` and `RateLimitError` add an `upgradeUrl` property that points to the upgrade page (it defaults to `https://recursiv.io/pricing`).

### Handling errors

Branch on the class, most specific first, and fall back to `RecursivError` for anything else.

```typescript
import {
  Recursiv,
  RecursivError,
  AuthenticationError,
  AuthorizationError,
  NotFoundError,
  ValidationError,
  InsufficientCreditsError,
  RateLimitError,
  ConflictError,
} from '@recursiv/sdk';

const r = new Recursiv();

try {
  await r.projects.deploy('proj_123', { branch: 'main', type: 'production' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error('Invalid input:', err.details);
  } else if (err instanceof AuthenticationError) {
    console.error('Bad API key. Check RECURSIV_API_KEY.');
  } else if (err instanceof AuthorizationError) {
    console.error('Missing permissions or wrong org:', err.message);
  } else if (err instanceof InsufficientCreditsError) {
    console.error('Out of credits. Upgrade at', err.upgradeUrl);
  } else if (err instanceof NotFoundError) {
    console.error('Resource not found.');
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited. Retry in ${err.retryAfter}s.`);
  } else if (err instanceof ConflictError) {
    console.error('Conflict:', err.message);
  } else if (err instanceof RecursivError) {
    console.error(`API error [${err.status}]: ${err.message}`);
  } else {
    throw err; // network error, timeout, programming bug
  }
}
```

> **Note**
>
> The SDK auto-retries 429 and 5xx responses with exponential backoff and respects the `Retry-After` header. You only see `RateLimitError` after retries are exhausted. See [Error Handling](/sdk/setup/error-handling) for the full retry table and per-class examples.

## Rate limits

Every API response carries budget headers so your app or agent can self-regulate before it hits a wall:

```text
x-recursiv-tier: free
x-recursiv-calls-remaining: 4847
x-recursiv-calls-limit: 5000
```

When you exceed the limit the API returns `429 Too Many Requests` with a `Retry-After` header, and the SDK surfaces a `RateLimitError` once retries are exhausted. Per-day call limits vary by plan (see the table below).

### Anonymous sandbox limit

The anonymous sandbox (`POST /api/v1/sandbox/try`, or `new Recursiv({ anonymous: true })`) needs no signup and is rate-limited per IP:

* 10 executions per IP per day
* 30-second execution timeout
* TypeScript, JavaScript, and Python
* No persistent storage

The execute response reports how many runs are left:

```typescript
const r = new Recursiv({ anonymous: true });
const { data, meta } = await r.sandbox.execute({
  code: 'console.log(1 + 1)',
  language: 'typescript',
});
console.log(`${meta.remaining_executions} executions remaining today`);
```

## Plans and what is metered

Plan names, prices, and limits are maintained on the [pricing page](/guides/start-here/pricing-limits). Refer there for current numbers. At a glance:

|                     | Free      | Builder (\$49/mo) | Pro (\$299/mo)  | Enterprise              |
| ------------------- | --------- | ----------------- | --------------- | ----------------------- |
| API calls/day       | 5,000     | 100,000           | Unlimited       | Unlimited               |
| AI agents           | 1         | 10                | Unlimited       | Unlimited               |
| Projects            | 3         | 25                | Unlimited       | Unlimited               |
| Production deploys  | No        | Yes               | Yes             | Yes                     |
| Agent swarms + cron | No        | Yes               | Yes             | Yes                     |
| Support             | Community | Email             | Priority + SLAs | Dedicated + custom SLAs |
| SSO / SAML          | No        | No                | No              | Yes                     |

### What is metered

Paid plans (Builder and Pro) are a base subscription plus usage billed monthly. Free-tier users get \$5 in credits that cover every usage type. Four things are metered:

| Resource         | What it covers                                                                                      |
| ---------------- | --------------------------------------------------------------------------------------------------- |
| AI inference     | Tokens through model calls, billed per 1M tokens. The rate varies by model (routed via OpenRouter). |
| Sandbox compute  | Sandbox runtime, billed per compute hour.                                                           |
| Database storage | PostgreSQL storage, billed per GB per month.                                                        |
| Object storage   | Files and media, billed per GB per month.                                                           |

For the current per-unit rates see the [pricing page](/guides/start-here/pricing-limits). Numbers there are the source of truth.

## Common failures and fixes

### "Project has no linked repository" on deploy

Deploys build from the project's linked GitHub repository. A project created without a `repo_url` fails this way. Either link a repo, or scaffold one server-side from a starter template:

```typescript
const { data: repo } = await r.github.createFromTemplate({
  template: 'nextjs', // or 'expo'
  name: 'my-app',
});

const { data: project } = await r.projects.create({
  name: 'my-app',
  repo_url: repo.repo_url,
});

await r.projects.deploy(project.id, { branch: 'main', type: 'production' });
```

If you already have a repo, pass its URL as `repo_url` on `projects.create` or attach it later with `projects.update`.

### 403 when calling across organizations

An `AuthorizationError` (403) on a cross-org call usually means the key is org-bound. An org-bound key only works against the org it is tied to. Calling a resource in a different org returns a key-org mismatch.

Fix: use a personal key or a select-orgs key for cross-org or MCP work. Reserve org-bound keys for single-org access. Manage key scope at [recursiv.io/account/api-keys](https://recursiv.io/account/api-keys).

### 401 on every request

The key is missing, revoked, or rotated, or you passed a session token where an API key was expected. Confirm `RECURSIV_API_KEY` is set and current, and mint a fresh key if needed.

### 429 even with retries enabled

You are out of daily call budget rather than just bursting. Check the `x-recursiv-calls-remaining` header, slow your call rate, or upgrade. `err.upgradeUrl` on the `RateLimitError` links to the upgrade page for free-tier callers.

### 400 with field-level details

A `ValidationError` means the request body failed schema validation. Inspect `err.details` for the exact field and constraint that failed before retrying.