# Errors and rate limits

The error format, what each status means, the per-plan request budget, and how to back off before you are refused.

Source: https://sourceloop.ai/help/api/errors/

---

## The error format

Errors are RFC 9457 problem documents, served as `application/problem+json`:

```json
{
  "type": "https://api.sourceloop.ai/errors/insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This key cannot read deals.",
  "remediation": "Add the deals:read scope to the key, or use a key that has it.",
  "instance": "req_8f21c0a4"
}
```

Two fields are worth building against:

- **`remediation`** says what to do next, in a sentence. It is non-standard and deliberate: an error that explains itself costs one round trip, an opaque one costs three. It is also why an AI agent calling this API can usually correct itself without asking you.
- **`instance`** is the request id, also returned in the `Sourceloop-Request-Id` header on every response. Quote it in support requests and we can find the exact call.

## What each error means

| `type` | Status | What happened |
|---|---|---|
| `invalid-request` | 400 | A parameter is missing, malformed, or contradicts another |
| `unknown-metric` | 400 | That metric does not exist. `GET /v1/schema` lists the ones that do |
| `unknown-dimension` | 400 | Same, for a breakdown dimension |
| `unknown-filter-operator` | 400 | The filter operator is not supported for that dimension |
| `invalid-window` | 400 | The period could not be parsed, or `from` is after `to` |
| `window-too-large` | 400 | The requested range exceeds what the endpoint will scan |
| `unsupported-combination` | 400 | Each parameter is valid, but not together |
| `website-not-found` | 404 | No website matches, or the key does not cover it |
| `insufficient-scope` | 403 | The key is valid but lacks a scope. The body names it |
| `rate-limit-exceeded` | 429 | Too many requests this minute. Wait `Retry-After` seconds |

Where a value was rejected against a fixed set, the problem document also carries an `allowed` array listing the valid values, so you rarely need to open this page.

## Rate limits

Counted **per workspace, not per key**. A second key does not buy a second budget.

| Plan | Sustained requests per minute |
|---|---|
| Free | 60 |
| Pro | 120 |
| Business | 600 |
| Agency | 1,200 |

Every response carries the current budget, so a well-behaved client slows down before it is refused rather than after:

```
RateLimit-Limit: 600
RateLimit-Remaining: 574
RateLimit-Reset: 41
```

`RateLimit-Reset` is seconds until the window rolls over. Exceeding the limit returns `429` with `Retry-After` in seconds. Retry after that many seconds, not immediately, and not with a tight loop.

Analytics reads are additionally capped at four concurrent queries per workspace. One wide window beats many narrow ones: asking for 90 days once is both faster and cheaper for you than 90 requests for a day each.

## Retrying safely

Retry `429` and `5xx`. Do not retry `4xx` other than `429`, because the request itself is what needs changing.

Writes accept an `Idempotency-Key` header. Send the same key with a retried write and the original result is returned instead of the write happening twice, which matters when a network timeout leaves you unsure whether the first attempt landed.

```bash
curl -s -X POST "https://app.sourceloop.ai/api/v1/events" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY" \
  -H "Idempotency-Key: signup-8f21c0a4" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"event_name":"signup_completed","email":"jane@acme.com"}]}'
```

Batch sends are partial-success: one malformed event does not discard the batch, and the response reports accepted and rejected counts separately.