Errors & Limits

Predict and handle failures and cost

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.

1import {
2 RecursivError,
3 AuthenticationError,
4 AuthorizationError,
5 NotFoundError,
6 ValidationError,
7 InsufficientCreditsError,
8 RateLimitError,
9 ConflictError,
10} from '@recursiv/sdk';

Error classes

ClassStatusWhen it is thrown
AuthenticationError401API key is missing, invalid, or expired.
AuthorizationError403Key is valid but lacks the scope or org access for the resource.
InsufficientCreditsError402Account is out of credits. Carries upgradeUrl.
NotFoundError404The resource does not exist or was deleted.
ValidationError400Request body failed validation. details holds the per-field Zod output.
RateLimitError429Rate or daily limit exceeded. Carries retryAfter and upgradeUrl.
ConflictError409Request conflicts with current state (taken username, claimed task, duplicate).
RecursivErroranyBase class. Catch-all for any other API error.

Error shape

Every error extends RecursivError and exposes these properties:

PropertyTypeDescription
messagestringHuman-readable message. Inlines hint, dashboard_url, and docs_url when the API returns them.
statusnumberHTTP status code.
typestringError category (for example auth_error, validation_error, api_error).
codestringMachine-readable code (for example invalid_api_key, insufficient_scopes).
detailsunknownExtra context. For ValidationError this is the Zod field output.
retryAfternumber | undefinedSeconds to wait before retrying. Present on 429.
hintstring | undefinedResolution hint when the API provides one.
dashboardUrlstring | undefinedLink into the dashboard when relevant.
docsUrlstring | undefinedLink 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.

1import {
2 Recursiv,
3 RecursivError,
4 AuthenticationError,
5 AuthorizationError,
6 NotFoundError,
7 ValidationError,
8 InsufficientCreditsError,
9 RateLimitError,
10 ConflictError,
11} from '@recursiv/sdk';
12
13const r = new Recursiv();
14
15try {
16 await r.projects.deploy('proj_123', { branch: 'main', type: 'production' });
17} catch (err) {
18 if (err instanceof ValidationError) {
19 console.error('Invalid input:', err.details);
20 } else if (err instanceof AuthenticationError) {
21 console.error('Bad API key. Check RECURSIV_API_KEY.');
22 } else if (err instanceof AuthorizationError) {
23 console.error('Missing permissions or wrong org:', err.message);
24 } else if (err instanceof InsufficientCreditsError) {
25 console.error('Out of credits. Upgrade at', err.upgradeUrl);
26 } else if (err instanceof NotFoundError) {
27 console.error('Resource not found.');
28 } else if (err instanceof RateLimitError) {
29 console.error(`Rate limited. Retry in ${err.retryAfter}s.`);
30 } else if (err instanceof ConflictError) {
31 console.error('Conflict:', err.message);
32 } else if (err instanceof RecursivError) {
33 console.error(`API error [${err.status}]: ${err.message}`);
34 } else {
35 throw err; // network error, timeout, programming bug
36 }
37}

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 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:

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:

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

Plans and what is metered

Plan names, prices, and limits are maintained on the pricing page. Refer there for current numbers. At a glance:

FreeBuilder ($49/mo)Pro ($299/mo)Enterprise
API calls/day5,000100,000UnlimitedUnlimited
AI agents110UnlimitedUnlimited
Projects325UnlimitedUnlimited
Production deploysNoYesYesYes
Agent swarms + cronNoYesYesYes
SupportCommunityEmailPriority + SLAsDedicated + custom SLAs
SSO / SAMLNoNoNoYes

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:

ResourceWhat it covers
AI inferenceTokens through model calls, billed per 1M tokens. The rate varies by model (routed via OpenRouter).
Sandbox computeSandbox runtime, billed per compute hour.
Database storagePostgreSQL storage, billed per GB per month.
Object storageFiles and media, billed per GB per month.

For the current per-unit rates see the pricing page. 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:

1const { data: repo } = await r.github.createFromTemplate({
2 template: 'nextjs', // or 'expo'
3 name: 'my-app',
4});
5
6const { data: project } = await r.projects.create({
7 name: 'my-app',
8 repo_url: repo.repo_url,
9});
10
11await 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.

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.