# Requests and responses

> Base URL, authentication, company IDs, the response envelope, errors, rate limits and paging.

## Base URL and authentication

```text
https://belfort-data-api.vercel.app
```

Every route is a `GET`. Send your key in the `X-API-Key` header on every route except `/v1/status`, which needs none:

```bash
curl -H "X-API-Key: $BELFORT_API_KEY" https://belfort-data-api.vercel.app/v1/companies/MSFT
```

## Naming a company

Company routes take an `{id}` in the path. It can be:

- a ticker, in any case. `BRK.B` and `BRK-B` reach the same company.
- a CIK, the SEC's company number, with or without leading zeros.

A filer with no ticker still works by CIK. A ticker reaches exactly one CIK; for the few companies that reorganized under a new CIK, see [Company](/datasets/company/#reorganized-companies).

## The response envelope

Every successful answer has the same two parts:

```json
{
  "data": "...",
  "meta": { "snapshot_id": "...", "as_of": "...", "next_cursor": null }
}
```

- `data` is the answer: one object, or a list.
- `meta.snapshot_id` names the load of data that answered you.
- `meta.as_of` is when the source data was published. Quote it with any figure you use.
- `meta.next_cursor` is the key to the next page, or `null` on the last page and on routes that don't page.

Values are as filed. **A blank in a filing is `null`, never 0.**

## Errors

Errors come back as `{ "error": { "code": "...", "message": "..." } }`. The message says what to fix.

| HTTP | `code` | What happened |
|---|---|---|
| 400 | `bad_request` | The request is malformed, or has a query parameter the route doesn't take. |
| 401 | `unauthorized` | The key is missing, unknown or revoked. |
| 404 | `not_found` | No company matches this ticker or CIK. |
| 429 | `rate_limited` | Your key is over its per-minute limit. Wait the seconds in `Retry-After`. |
| 500 | `internal` | Something failed on our side. |
| 503 | `internal` | The database can't be reached, or this dataset isn't loaded yet. Wait the seconds in `Retry-After`. |

Unknown query parameters are refused on purpose, so a typo like `frequncy=annual` fails loudly instead of quietly returning everything.

## Rate limits

Each key has a per-minute limit. The limit is counted by each server instance on its own, so it isn't a hard global cap: a 429 means the instance you reached is over your limit, and another instance may still answer. Please treat your agreed limit as the rate you send. Usage is counted across all instances.

## Paging

Routes that return long lists page them. Pass `limit` to set the page size and, for the next page, pass `meta.next_cursor` back as `cursor` with the same filters. Stop when `next_cursor` is `null`.

```bash
curl -H "X-API-Key: $BELFORT_API_KEY" \
  "https://belfort-data-api.vercel.app/v1/companies/AAPL/statements?statement=IS&limit=4"

# then, with the cursor from that answer:
curl -H "X-API-Key: $BELFORT_API_KEY" \
  "https://belfort-data-api.vercel.app/v1/companies/AAPL/statements?statement=IS&limit=4&cursor=NEXT_CURSOR"
```

A page can hold fewer items than `limit` even when more remain, so always follow `next_cursor` rather than counting items. A cursor only works with the filters it was issued for.

| Route | `limit` | Default | Pages with `cursor` |
|---|---|---|---|
| `/v1/companies/{id}/statements` | 1–20 | 4 | Yes |
| `/v1/companies/{id}/financials` | 1–100 | 40 | Yes |
| `/v1/companies/{id}/segments` | 1–50 | 20 | Yes |
| `/v1/companies/{id}/holders` | 1–100 | 25 | Yes |
| `/v1/companies/{id}/insider-trades` | 1–50 | 20 | Yes |
| `/v1/companies/{id}/form13f` | 1–50 | 20 | Yes |
| `/v1/companies/{id}/filings` | 1–50 | 20 | Yes |
| `/v1/companies/{id}/earnings-releases` | 1–100 | 20 | Yes |
| `/v1/companies/{id}/ownership` | 1–100 | 20 | No |

Earnings releases also take `from` and `to`, inclusive filed-date bounds in `YYYY-MM-DD` form.

`/v1/status`, `/v1/companies/{id}`, `/v1/companies/{id}/metrics` and `/v1/macro/{series}` return everything in one answer.

## Freshness

`/v1/status` reports, for each dataset, when its source was published (`as_of`), when we loaded it (`loaded_at`) and its age in hours. A dataset that isn't loaded yet reads `null`, and its route answers 503 until it is. How often each source updates is on [Where the data comes from](/data-sources/#how-fresh-it-is).

---
Source: https://docs.belfortagent.xyz/requests-and-responses/
