# Errors, Rate Limits & Quotas

## Authentication

Every `/v1` request must carry your API key as a **bearer token**:

```bash
curl https://api.fruitstand.dev/v1/funds/SPY \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Create and manage keys from your account in this portal. Keep keys server-side — treat them like
passwords.

## Status codes

| Status | Meaning | When |
|---|---|---|
| `200` | OK | Success. |
| `401` | Unauthorized | Missing or invalid API key. |
| `403` | Forbidden | Valid key, but no active subscription/plan on it. Pick a plan on [Pricing](/pricing). |
| `404` | Not Found | Unknown fund `code` (single-fund endpoints only — see batch behavior below). |
| `422` | Unprocessable Entity | Invalid parameter, or more than 100 codes in a batch request. |
| `429` | Too Many Requests | Per-minute burst or monthly quota exceeded (see below). |
| `503` | Service Unavailable | Data temporarily stale/empty, or the service is briefly unavailable. |

## Error response shapes

**Gateway policy errors** (`401`, `403`, `429`) follow [RFC 7807](https://www.rfc-editor.org/rfc/rfc9457)
problem+json:

```json
{
  "type": "https://httpproblems.com/http-status/429",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "You have exceeded your rate limit.",
  "instance": "/v1/funds/SPY"
}
```

**Application errors** (`404`, `422`, `503`) return a simpler body:

```json
{ "detail": "Fund 'XYZ' not found." }
```

## Rate limits & quotas

Two independent limits apply per plan — a per-minute **burst** and a monthly **quota**:

| Plan | Burst (per minute) | Monthly quota |
|---|---|---|
| Free | 100 / min | 250 requests |
| Developer | 100 / min | 1,000 requests |
| Pro | 250 / min | 2,500 requests |
| Business | 500 / min | 10,000 requests |

- Exceeding either returns **`429`**. Back off and retry after a short wait for burst; a quota
  `429` clears at your next monthly cycle (or upgrade your plan).
- **Each API call counts as one request** — including a `/batch` call, no matter how many codes it
  carries. Batching is the most quota-efficient way to pull many funds (see below).
- **Caching is allowed on every plan** ([Licensing](/licensing)) — cache results to keep your
  request volume well under quota.

## Batch endpoints

`POST /v1/trailing-returns/batch` and `POST /v1/calendar-returns/batch` accept up to **100 codes**
per call:

- More than 100 codes → **`422`**.
- **Unknown or missing codes are silently omitted** from the response array — a batch never
  returns `404`. Compare the returned `code`s against what you asked for to detect misses.

```bash
curl https://api.fruitstand.dev/v1/trailing-returns/batch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"codes": ["SPY", "QQQ", "NOTAREALTICKER"]}'
# → returns rows for SPY and QQQ only
```

## Pagination

`GET /v1/funds` uses keyset (cursor) pagination:

- `limit` — page size, **default 100, max 1000** (values above 1000 are capped).
- The response's `next_cursor` is an opaque value; pass it back as `?cursor=` to get the next
  page. When `next_cursor` is `null`, you've reached the end.

```bash
# first page
curl "https://api.fruitstand.dev/v1/funds?type=ETF&limit=500" -H "Authorization: Bearer YOUR_API_KEY"
# next page
curl "https://api.fruitstand.dev/v1/funds?type=ETF&limit=500&cursor=SPY" -H "Authorization: Bearer YOUR_API_KEY"
```
