# Getting started

The Quova API is a REST API over HTTPS that returns JSON. This page covers what every endpoint has in common, and the API reference has the parameters, fields and credit cost of each one.

## Your first request

Create a key in the dashboard, then ask for a week of prices for Apple and BHP. Every request goes to `https://api.quova.io`.

```shell
curl "https://api.quova.io/v1/prices/daily?symbols=NASDAQ:AAPL,ASX:BHP&from=2026-06-01&to=2026-06-05&currency=USD" \
  -H "Authorization: Bearer $QUOVA_API_KEY"
```

You get back the bars for each symbol along with what it resolved to and what the call cost. A symbol we can't answer for gets an entry in `errors` instead of failing the whole call.

Response 

```json
{
  "data": [
    {
      "symbol": "NASDAQ:AAPL",
      "instrument": {
        "id": "ins_34VInjBb4l9Qz9gdXnDar",
        "symbol": "NASDAQ:AAPL",
        "name": "Apple Inc.",
        "type": "equity",
        "mic": "XNAS",
        "currency": "USD",
        "country": "US"
      },
      "asOf": "2026-06-05T00:00:00Z",
      "currency": "USD",
      "adjust": "splits",
      "bars": [
        {
          "date": "2026-06-01",
          "open": 309.625,
          "high": 310.94,
          "low": 305.02,
          "close": 306.31,
          "volume": 48849933
        },
        {
          "date": "2026-06-02",
          "open": 307.46,
          "high": 315.45,
          "low": 306.685,
          "close": 315.2,
          "volume": 44534716
        },
        {
          "date": "2026-06-03",
          "open": 314.175,
          "high": 316.94,
          "low": 308.85,
          "close": 310.26,
          "volume": 50836705
        },
        {
          "date": "2026-06-04",
          "open": 313.23,
          "high": 313.54,
          "low": 309.65,
          "close": 311.23,
          "volume": 44869134
        },
        {
          "date": "2026-06-05",
          "open": 312.86,
          "high": 315.17,
          "low": 307.15,
          "close": 307.34,
          "volume": 65310502
        }
      ]
    },
    {
      "symbol": "ASX:BHP",
      "instrument": {
        "id": "ins_34VMvft6kZEXThtzFGcAL",
        "symbol": "ASX:BHP",
        "name": "BHP Group Ltd",
        "type": "equity",
        "mic": "XASX",
        "currency": "AUD",
        "country": "AU"
      },
      "asOf": "2026-06-05T00:00:00Z",
      "currency": "USD",
      "adjust": "splits",
      "bars": [
        {
          "date": "2026-06-01",
          "open": 44.582805,
          "high": 45.040725,
          "low": 44.4575925,
          "close": 44.70444,
          "volume": 6338357
        },
        {
          "date": "2026-06-02",
          "open": 45.50852,
          "high": 45.637724,
          "low": 44.89839,
          "close": 45.486986,
          "volume": 7621289
        },
        {
          "date": "2026-06-03",
          "open": 46.01124,
          "high": 46.360512,
          "low": 45.897192,
          "close": 46.267848,
          "volume": 8405596
        },
        {
          "date": "2026-06-04",
          "open": 45.57987,
          "high": 45.6512,
          "low": 44.488521,
          "close": 44.79524,
          "volume": 7871447
        },
        {
          "date": "2026-06-05",
          "open": 43.8199,
          "high": 43.967845,
          "low": 42.953365,
          "close": 43.14358,
          "volume": 7385673
        }
      ]
    }
  ],
  "errors": [],
  "meta": {
    "credits": 2,
    "requestId": "req_34VoxwgsJXbOecNLR74B7"
  }
}
```

## Authentication

Send your key in the `Authorization` header as a bearer token. Keys start with `qv_` and are shown once, when you create them, because we only store a hash.

Header 

```shell
Authorization: Bearer qv_...
```

Keys never go in the query string, since URLs end up in logs. A key can call every endpoint or a fixed list of operation IDs, and it can have its own credit budget, rate limit, IP allowlist and expiry date.

## Responses

Data endpoints share one envelope. `data` has an entry for each symbol you asked for, in the order you sent them. `errors` lists the symbols that failed, and `meta` says what the call cost along with its request ID.

Every field is always present, and a value we don't know is `null` rather than zero. Each entry has an `asOf` timestamp for the point in time its data describes. That's the newest record's date for history, when a live value was observed, and when we last checked a source for reference data.

Timestamps are ISO 8601 in UTC, and daily data uses the exchange's local trading date. Fields and parameters are camelCase while enum values and error codes are snake\_case. An unknown query parameter is rejected with a 400, so a typo can't quietly change what you get back.

## Identifiers

Every instrument is referenced as `namespace:value`, and any of these work wherever a symbol is expected.

| Kind                 | Example                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| Listed security      | NASDAQ:AAPL, LSE:VOD, XETRA:SAP or ASX:VAS. The MIC works too, as in XASX:VAS                                |
| Open identifiers     | isin:US0378331005 or figi:BBG000B9XRY4                                                                       |
| Licensed identifiers | cusip:037833100 and sedol:... are accepted as input but never returned                                       |
| Broker contract ID   | conid:265598, the ID Interactive Brokers gives a contract. Accepted as input but never returned              |
| Unlisted fund        | apir:BTA0419AU                                                                                               |
| Listed option        | OCC:AAPL250117C00150000 for a US option, with or without the OSI spaces, or ASX:BHPS49                       |
| FX pair              | FX:EURUSD                                                                                                    |
| Crypto               | CRYPTO:BTC for the most liquid match, or SOLANA:<contract> for an exact token                                |
| NFT                  | ETHEREUM:<contract>/<token id> for one NFT, or the bare contract for its collection. SOLANA:<mint> on Solana |
| Index, rate, future  | INDEX:AXJO, RATE:AU10Y, FUT:CL1                                                                              |
| Quova ID             | ins\_7Hq2kP3xY9vLmN4bR8sT1u. Store this one, it survives renames and ticker changes                          |

A bare ticker like `BHP` resolves to its home listing. Each entry in the response includes the `instrument` your symbol resolved to, so you can always check.

Tickers get reused, so a listing that has ended comes back with its last trading day, like `ASX:CCL@2021-05-11`, and sending that symbol always gets you the same instrument. To look a ticker up as it was on a given day, put the day in the symbol, as in `ASX:CCL@2021-01-04`, or send `listedOn=2021-01-04` to date every symbol in the call.

## Errors

When a whole request fails you get an error status and an RFC 9457 `application/problem+json` body. Branch on `code`, which never changes, rather than on the title or detail, which we may reword.

400 Bad Request 

```json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "The request has invalid parameters",
  "code": "invalid_request",
  "requestId": "req_34Voy9DCjEWXnyooRZsd8",
  "errors": [
    {
      "location": "query.from",
      "message": "must be a date like 2026-09-01"
    }
  ]
}
```

| Status | Code                      | Meaning                                                                                                   |
| ------ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| 400    | invalid\_request          | A parameter is missing or malformed. errors says which one and why                                        |
| 400    | invalid\_cursor           | The cursor isn't valid. Start again without one                                                           |
| 401    | missing\_api\_key         | No Authorization header was sent                                                                          |
| 401    | invalid\_api\_key         | The key doesn't exist, was revoked or isn't sent as a bearer token                                        |
| 401    | key\_expired              | The key is past its expiry date                                                                           |
| 402    | trial\_credits\_exhausted | The trial's credits ran out on this request, so the trial has ended. Choose a plan in billing to carry on |
| 402    | trial\_ended              | The trial has ended and the API is paused until you choose a paid plan or the free plan in billing        |
| 402    | credits\_exhausted        | Your plan's credits for this period are used up. Retry-After says when they reset                         |
| 402    | spend\_cap\_reached       | Your organisation hit its spend cap for this billing period                                               |
| 402    | payment\_failed           | Stripe couldn't collect a payment after retrying. Update your card in billing                             |
| 402    | no\_active\_plan          | This organisation needs a paid plan, since its creator's one free organisation is another one             |
| 403    | plan\_upgrade\_required   | Your plan doesn't include this endpoint, or dates that far back                                           |
| 403    | endpoint\_not\_allowed    | The key isn't allowed to call this endpoint                                                               |
| 403    | ip\_not\_allowed          | The request came from an IP outside the key's allowlist                                                   |
| 403    | org\_suspended            | The organisation is suspended                                                                             |
| 404    | not\_found                | The path or the item you asked for doesn't exist                                                          |
| 429    | key\_rate\_limited        | The key went over its own rate limit                                                                      |
| 429    | org\_rate\_limited        | Your organisation went over its plan's rate limit                                                         |
| 429    | credit\_budget\_exhausted | The key used up its credit budget for the day, week or month                                              |
| 500    | internal\_error           | Something went wrong on our side                                                                          |
| 503    | data\_pending             | We're still fetching this data. Retry after the Retry-After header                                        |

Inside a batch, a symbol that fails gets its own entry in `errors` with one of these codes, and you aren't billed for it.

| Code                   | Meaning                                                                         |
| ---------------------- | ------------------------------------------------------------------------------- |
| invalid\_symbol        | The identifier isn't in a format we recognise                                   |
| instrument\_not\_found | Nothing matches the identifier                                                  |
| not\_supported         | The dataset doesn't apply to this instrument, like dividends on a currency pair |
| data\_unavailable      | None of our sources have this data                                              |
| data\_pending          | We're fetching it now. Ask again shortly                                        |
| data\_failed           | Every source failed for this item. Try again later                              |

## Credits

Every endpoint has a weight in credits, shown in the API reference and in the `x-credits` field of the OpenAPI spec. Most endpoints charge the weight for each instrument that resolves, and the rest charge per call, page or series. History length doesn't change the price.

`meta.credits` is what the call cost. The `Credits-Remaining` header is what's left under the tightest limit on your key. That's the key's own budget, the trial cap, your plan's monthly pool or your spend cap, whichever runs out first, and it reads 0 once you're into overage.

Response headers 

```shell
HTTP/2 200
Content-Type: application/json
Credits-Remaining: 4,998
```

The [pricing page](https://quova.io/pricing/index.md#compare) lists the cost of every endpoint.

## Rate limits

Each plan has a requests-per-minute limit that every key in the organisation shares. You can give a key a lower limit of its own.

| Plan    | Requests a minute |
| ------- | ----------------- |
| Trial   | 60                |
| Free    | 10                |
| Starter | 300               |
| Growth  | 1,000             |
| Scale   | 3,000             |

Going over returns a 429 with `key_rate_limited` or `org_rate_limited` and a `Retry-After` header in seconds. Wait that long before you retry.

The Free plan also keeps history to the last 5 years. Its responses carry a `History-From` header with the first date it serves, leaving out `from` starts there, and asking for an earlier date returns a 403 with `plan_upgrade_required`.

## Batch requests

Endpoints that take `symbols` accept up to 100 comma-separated identifiers, and one symbol or fifty use the same call. Results come back in the order you sent them.

If a symbol fails it goes into `errors` and the rest of the call still succeeds. Only items that return data are billed, and a symbol sent twice in one call, like `ASX:VAS,XASX:VAS`, is billed once.

## Fresh data

We keep everything we fetch and refresh it when someone reads it after it has gone stale, so most reads come from our cache. The first request for an instrument we've never seen waits while we fetch it, which makes it slower than the ones after it.

Add `fresh=true` to skip the cache and get the latest data during your call. Items refreshed this way are billed at 2x. If we can't refresh an item, it falls back to our cache and costs the normal price.

## CSV

Add `format=csv` to any data endpoint to get `text/csv` instead of JSON. The body only has data rows, so the `Batch-Errors` header tells you how many inputs failed.

## Pagination

List endpoints return a page at a time. Set the page size with `limit`, then send `meta.nextCursor` back as `cursor` to get the next page. On the last page `nextCursor` is null. There's no offset pagination.
