---
title: API & SDK Issues
description: >-
  Troubleshoot API authentication, rate limits, WebSocket/SSE setup, and SDK
  usage on the platform.
tags:
  - troubleshooting
  - api
---

 Most API failures are a key sent in the wrong shape, a key that has expired or been revoked, or a burst that hit a rate limit. Check your request against the examples below before you file a ticket.

Every REST route reads your identity from one of three credentials: a Clerk session (the browser), a verified Clerk JWT, or an `ivy_` API key. The route then checks that the job, project or file belongs to you, and applies a rate limit for its route class. The sections below follow that order.

The examples use `https://www.ivybiosciences.com`, the host the `ivybloom` CLI talks to by default. The full route reference is in [Jobs](/docs/api/rest-jobs), [Rate limits](/docs/api/rate-limits) and [SSE streaming](/docs/api/realtime-sse).

## 401 Unauthorized

A `401` means the server could not verify who you are, so it rejected the request before it ran any route logic.

**Common causes:**

### Expired API key

- A key can carry an expiry date. Check the key's expiry on the [API keys page](/profile/settings/api-keys), or run `ivybloom apikeys list`.
- You can't renew an expired key. Create a new one and update your application.

### Missing Authorization header

- Send the key as a Bearer token: `Authorization: Bearer ivy_<keyId>_<secret>`. The server also accepts it in an `X-API-Key` or `X-Ivy-Api-Key` header.
- Common mistakes:
  - Missing the `Bearer` prefix: `Authorization: ivy_abc_123` (wrong).
  - Quotes around the token: `Authorization: Bearer "ivy_abc_123"` (wrong).
  - A key that doesn't start with `ivy_`. Every API key starts with `ivy_`, and anything else is read as a session token.

### Chat streams from the CLI

- A chat stream needs an API key. If a request carries only a CLI client id, the server refuses it with `401 CLI_API_KEY_REQUIRED`. Run `ivybloom auth login --api-key ivy_…` and retry. See [CLI API keys](/docs/cli/api-keys).

**Debugging checklist:**
1. Print the exact `Authorization` header your code sends. Redact the secret, but check the shape.
2. Confirm the key has the shape `ivy_<keyId>_<secret>`.
3. Rule out your application by repeating the request with `curl`:
```
curl -H "Authorization: Bearer ivy_YOUR_KEY" https://www.ivybiosciences.com/api/v1/jobs
```

## 403 Forbidden

A `403` means the server knows who you are, but you aren't allowed to perform this action.

**Common causes:**

### Resource not yours

- Each job, project and file is scoped to its owner. Reading one that belongs to another user or organization returns `403` (or `404`, so the route doesn't confirm that it exists).
- A shared resource is readable only after its owner has shared it with you or published a share link.

### Organization role restrictions

- In an organization, your role decides what you can change. If you can read a project but can't submit jobs to it, ask an org admin to check your role.

### Plan limits

- Plans differ in credits, concurrent jobs and AI reasoning tier, not in API access. Every account can create an `ivy_` API key, including the free Seed plan. Compare plans on [Pricing](/pricing).

## 429 Rate Limited

A `429` means you've sent more requests than the route's rate limit allows.

### How limits are applied

- Each route belongs to a rate-limit class, and each class has its own per-minute budget. Authenticated callers are keyed on the user, and anonymous callers are keyed on the client IP. Per-endpoint figures are on [Rate limits](/docs/api/rate-limits).
- The `429` body carries the error code `RATE_LIMITED` and a `retryAfter` value in seconds.

### Reading rate limit headers

| Header | Meaning |
|--------|---------|
| `Retry-After` | Seconds to wait before retrying. Sent on `429` responses. |
| `X-RateLimit-Limit` | Maximum requests in the current window, on routes that report it. |
| `X-RateLimit-Remaining` | Requests left before the limit, on routes that report it. |
| `X-RateLimit-Reset` | When the window resets, on routes that report it. |

Only `Retry-After` is sent on every `429`, so write your client against that header.

### Backoff strategy

Back off exponentially when you receive a `429`:

1. Read the `Retry-After` header value (in seconds).
2. Wait that many seconds before retrying.
3. If the retry also returns `429`, double the wait time.
4. Cap the maximum wait at 60 seconds.
5. After 5 consecutive `429` responses, stop retrying and log the failure.

**Do not:**
- Retry immediately in a tight loop.
- Retry without reading `Retry-After`.
- Open parallel connections to get around the limit. Authenticated limits are keyed on your user, not on the connection.

## WebSocket and SSE setup

Job progress comes from the Modal orchestrator. The browser receives it over an SSE proxy on this app, so it never connects to Modal directly.

### SSE (Server-Sent Events) connection

```
GET /api/v1/jobs/{jobId}/events
Authorization: Bearer ivy_YOUR_KEY
Accept: text/event-stream
```

- The route checks that you can access the job before it opens the stream.
- SSE is one-way (server to client). Send commands with ordinary HTTP requests.
- The server sends a heartbeat about every 15 seconds and closes the stream after about 5 minutes. Reconnect if the job is still running.
- The route doesn't replay missed events. After a reconnect, fetch `GET /api/v1/jobs/{jobId}` to catch up on the current state.

### Guest SSE

Chats started without an account run under a guest id, and their jobs stream from a separate route:

```
GET /api/v1/jobs/{jobId}/guest-events?guest_id=guest_…
Accept: text/event-stream
```

- The route needs no API key. The orchestrator checks that the `guest_id` owns the job and closes the stream on a mismatch.
- This route is for guest sessions only. A public share link doesn't open a job stream.

### WebSocket

There is no public WebSocket endpoint. The job WebSocket is internal to the compute backend, and external clients can't connect to it — use the SSE route above instead. See [WebSocket Protocol](/docs/api/realtime-websocket) if you're migrating code that opened a job WebSocket directly.

## SDK version compatibility

The `@pleach/*` packages are the pleach agent runtime, and they are pre-1.0 (`0.x`). The REST API doesn't need them: any HTTP client with an `ivy_` key can call it, and the [ivybloom CLI](/docs/cli) is the supported command-line client.

### Common SDK issues

- **Type errors after an update.** `0.x` releases can change types between minor versions, so pin an exact version and read the package changelog before you upgrade.
- **Import path changes.** The packages use subpath exports (for example `@pleach/core/types`). If an import path stops resolving after an update, check the `exports` field in the package's `package.json`.

### Verifying your SDK version

```bash
npm list @pleach/core
```

## CORS errors

CORS errors occur when a browser page on another origin calls the platform API.

**Symptom:** the browser console shows `Access to fetch at 'https://www.ivybiosciences.com/api/...' from origin 'https://your-app.com' has been blocked by CORS policy`.

### Hosted platform

- The API allows browser calls only from the platform's own origins (`https://www.ivybiosciences.com` and `https://accounts.ivybiosciences.com`).
- A custom frontend on another domain can't call the API from the browser. Call it from your backend instead, which also keeps your API key off the client.

### Self-hosted instances

- The allowed origins are a fixed list in `src/middleware/helpers/corsHelpers.ts` and `src/middleware/handlers/routeHandlers.ts`, not an environment variable. Add your frontend origin to both lists and redeploy.
- Don't allow `*` in production, because that lets any website call the API with your users' cookies.

### Preflight requests

- Browsers send an `OPTIONS` preflight before some API calls (POST, PUT, DELETE, or any call with custom headers).
- If the preflight returns a non-2xx status or no CORS headers, the browser blocks the real request.
- When debugging, inspect the `OPTIONS` request in the Network tab, not just the main request.

## Timeout errors

### Client-side timeouts

- Job submission returns as soon as the job is queued. The job then runs asynchronously, so the submit request doesn't need a long timeout.
- When polling, use a short per-request timeout (about 10 seconds) and poll every 5-10 seconds instead of holding one long request open.

### Polling pattern for long-running jobs

Don't wait synchronously for a job to finish. Poll it instead:

1. Submit the job with `POST /api/v1/jobs`. The response carries the job id.
2. Poll `GET /api/v1/jobs/{jobId}` for its status.
3. Stop when the status is terminal: `COMPLETED` (or `SUCCESS` on some tools), `FAILURE`, or `CANCELLED`.
4. Fetch results with `ivybloom jobs results JOB_ID`, or through the routes listed in [Artifacts & storage](/docs/api/rest-artifacts).

You can also stream status over SSE (above) instead of polling.

## Common request patterns

### Submitting a job

```bash
curl -X POST https://www.ivybiosciences.com/api/v1/jobs \
  -H "Authorization: Bearer ivy_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job_type": "esmfold",
    "parameters": {
      "protein_sequence": "MKTAYIAKQRQISFVKSHFSRQ"
    },
    "project_id": "YOUR_PROJECT_ID"
  }'
```

The CLI equivalent is `ivybloom run esmfold protein_sequence=MKTAYIAKQRQISFVKSHFSRQ`.

### Checking job status

```bash
curl https://www.ivybiosciences.com/api/v1/jobs/JOB_ID \
  -H "Authorization: Bearer ivy_YOUR_KEY"
```

## Ticket checklist

If you need to contact support about an API issue:

- The endpoint URL and HTTP method
- The response status code and body (redact your API key)
- Any `Retry-After` or `X-RateLimit-*` headers on the response
- Your CLI version (`ivybloom version`) or SDK version (`npm list @pleach/core`)
- Whether the issue reproduces with `curl` (this rules out your SDK or HTTP library)
- Timestamps and your organization slug
