# API reference (/reference)



Base URL `https://api.finic.ai`. Send an API key as `Authorization: Bearer gsk_…` ([Authentication](/authentication)) and JSON bodies (`Content-Type: application/json`); the score endpoints also take `multipart/form-data`. Every response has an `X-Request-Id` header.

| Endpoint                                      | Request                 | What it does                                 |
| --------------------------------------------- | ----------------------- | -------------------------------------------- |
| [`score`](/reference/score)                   | `POST /v1/score`        | Scores one record from 0 to 1000             |
| [`create_score`](/reference/create-score)     | `POST /v1/scores`       | Creates a score from labeled examples        |
| [`list_scores`](/reference/list-scores)       | `GET /v1/scores`        | Lists your scores, with what each predicts   |
| [`calibrate`](/reference/calibrate)           | `POST /v1/calibrate`    | Sends outcomes, to improve a score           |
| [`get_job_status`](/reference/get-job-status) | `GET /v1/jobs/{job_id}` | Whether creating or calibrating has finished |
| [`decide`](/reference/decide)                 | `POST /v1/decide`       | Answers your questions about a case          |

## Errors [#errors]

A failed request returns an error status and this JSON:

```json
{ "error": { "type": "invalid_request", "message": "\" Yes\" appears twice in the criteria.", "param": "questions.q.criteria" } }
```

`type` is one of the types below: branch on it. `message` is for people and can change, so don't parse it. `param` is the dotted path to the field at fault, or `null`. Validation stops at the first problem.

| HTTP  | `type`                    | When                                                                                                                   | Retry                                                                                           |
| ----- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `400` | `invalid_request`         | Malformed JSON, or the request breaks a rule of its endpoint                                                           | No. Fix the request                                                                             |
| `400` | `schema_mismatch`         | A record sent to [`score`](/reference/score) has fields that differ from its score's [schema](/concepts/scores#schema) | No. Fix the record, or send `skip_schema_validation` or `update_schema`                         |
| `401` | `authentication_error`    | Missing or invalid API key, including a revoked key                                                                    | No. Check the key                                                                               |
| `402` | `insufficient_credit`     | The organization's credit is used up (decide and score calls)                                                          | No. An admin adds credit in the Finic Portal under **Settings → Billing**                       |
| `404` | `model_not_found`         | Unknown `model` in a decide request                                                                                    | No. Use `grayson-1`                                                                             |
| `404` | `score_not_found`         | No score with that `model_id` in your organization                                                                     | No. Check the `model_id`                                                                        |
| `404` | `job_not_found`           | No job with that id in your organization                                                                               | No. Check the `job_id`                                                                          |
| `404` | `invalid_request`         | A path or method the API doesn't have                                                                                  | No. Fix the URL                                                                                 |
| `409` | `score_not_ready`         | The score is still being created, or creating it failed                                                                | After its [job](/reference/get-job-status) completes. If the job failed, create the score again |
| `409` | `duplicate_score_id`      | The score already has a call with this `score_id`                                                                      | No. Use a new `score_id` for each record                                                        |
| `409` | `calibration_in_progress` | The score is already being calibrated                                                                                  | After that calibration's job completes                                                          |
| `413` | `request_too_large`       | A body over 2 MB, a decide request over 32,768 tokens, or a record too large to score                                  | No. Send less                                                                                   |
| `429` | `rate_limited`            | Over the request rate, or a daily limit (100 new scores per organization, 10 calibrations per score)                   | The request rate: yes, with backoff. A daily limit: after it resets                             |
| `500` | `internal_error`          | Something failed on Finic's side                                                                                       | Yes                                                                                             |
| `503` | `overloaded`              | Temporarily at capacity, the decision didn't finish within 30 seconds, or creating scores isn't available right now    | Yes, with backoff                                                                               |

* **Out of credit,** decide and score calls get `402` before their data is checked; creating and calibrating never do.
* **A `502` or `504`** from the network edge, during a deployment or an outage, has no JSON body or `X-Request-Id`: treat it like `503`.
* **`202` from `create_score` and `calibrate`** means accepted, with the work running in a job. A failed job reports its error at [`get_job_status`](/reference/get-job-status), in the same shape without `param`.
* **A `429`** for the request rate says `Too many requests for this organization: the limit is 5 requests per second across its API keys and the playground.` and clears in moments. A daily limit's (`This organization has created 100 scores in the last 24 hours, the limit.`) doesn't, so retrying soon fails the same way.

Some `invalid_request` errors:

| Cause                                                            | `param`                   | `message`                                                                                                     |
| ---------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `data` of only whitespace                                        | `data`                    | `data must not be blank`                                                                                      |
| A decide request with neither `data` nor `context`               | `data`                    | `data is required: the JSON object, JSON array or text the questions are about. Free text can go in context.` |
| A question name starting with a digit                            | `questions.1st`           | `invalid name '1st': must match ^[A-Za-z_][A-Za-z0-9_]{0,63}$`                                                |
| Two options whose descriptions differ only in case or whitespace | `questions.q.criteria`    | `" Yes" appears twice in the criteria.`                                                                       |
| A `score` question with one level                                | `questions.risk.criteria` | `A score's criteria must list 2 to 32 levels as text, lowest first.`                                          |
| A noul's criteria with a key other than `true` or `false`        | `questions.q.criteria`    | `A noul's criteria can only describe "true" and "false", each as text.`                                       |
| A choice's `base_rate` shares that don't add up to 1             | `questions.q.base_rate`   | `A choice's base_rate shares must add up to 1.`                                                               |
| An unknown top-level field, such as `temperature`                | `null`                    | `Unknown field "temperature".`                                                                                |
| An unknown field inside a question                               | `questions.q.temperature` | `Extra inputs are not permitted`                                                                              |
| A score call without `score_id`                                  | `score_id`                | `score_id is required: your unique id for this record's score, used later to calibrate.`                      |
| A score call whose `data` holds several records                  | `data`                    | `data must hold one record; got 3. Send one score call per record.`                                           |
| Outcomes whose `score_id`s match no score call                   | `data`                    | `None of the score_ids match a call to this score. Send the score_id you sent to POST /v1/score.`             |

### Retrying [#retrying]

* **`decide`** is safe to retry; a retry of a request that succeeded makes the decision again.
* **`score`** with the same `score_id` scores a record once: if the first call succeeded, the retry gets `409 duplicate_score_id`, without the score.
* **`create_score`** retried after a timeout can create a second score, counted toward the daily limit.
* **`calibrate`** retried resends the same outcomes, which replace themselves; while the first call's job runs, it gets `409 calibration_in_progress`.
* **`get_job_status`** is always safe.

Retry `429` (except a daily limit), `500`, `502`, `503`, `504`, network errors and timeouts with exponential backoff and jitter; don't retry other `4xx` without changing the request. Set client timeouts above 30 seconds, the longest a decision can run.

```python
import random
import time

import requests

RETRYABLE = {429, 500, 502, 503, 504}


def post(path: str, body: dict, api_key: str, attempts: int = 5) -> dict:
    for attempt in range(attempts):
        last = attempt == attempts - 1
        try:
            response = requests.post(
                f"https://api.finic.ai{path}",  # "/v1/decide" or "/v1/score"
                headers={"Authorization": f"Bearer {api_key}"},
                json=body,
                timeout=60,
            )
        except (requests.ConnectionError, requests.Timeout):
            if last:
                raise
        else:
            if response.status_code not in RETRYABLE or last:
                break
        time.sleep(min(8.0, 0.5 * 2**attempt) * random.uniform(0.5, 1.0))

    if not response.ok:
        request_id = response.headers.get("X-Request-Id")
        try:
            error = response.json()["error"]
        except ValueError:  # an edge 502/504 has no JSON body
            raise RuntimeError(f"{response.status_code} (request {request_id})")
        raise RuntimeError(f"{response.status_code} {error['type']}: {error['message']} (request {request_id})")
    return response.json()
```

For help, email [support@finic.ai](mailto:support@finic.ai) with the `X-Request-Id`. Support sees a request's time, key, endpoint, status and error type, not its content: if the problem depends on the content, describe it or send a redacted example.

## Limits [#limits]

| Limit        | Value                                                |
| ------------ | ---------------------------------------------------- |
| Request body | 2 MB                                                 |
| Request rate | 5 per second per organization, in bursts of up to 10 |

### Rate limits [#rate-limits]

The request rate covers all of an organization's API keys, agent sign-ins and playground runs, for decide, score, create and calibrate calls; checking a job doesn't count. Requests over it get `429 rate_limited`, with no `Retry-After` header: retry with exponential backoff ([Retrying](#retrying)). For higher volume, contact [support@finic.ai](mailto:support@finic.ai).

### Scores [#scores]

| Limit                       | Value                                                                         |
| --------------------------- | ----------------------------------------------------------------------------- |
| Rows to create a score      | At least 100, with at least 20 where `is_bad` is true and 20 where it's false |
| Rows used                   | Up to 2,000; more are sampled down, keeping the mix of outcomes               |
| A row, or a record to score | 60,000 bytes as compact JSON                                                  |
| `description`               | 4,000 characters                                                              |
| `score_id`                  | 200 characters, unique within the score                                       |
| New scores                  | 100 per organization per 24 hours                                             |
| Calibrations                | 10 per score per 24 hours, one at a time                                      |
| Creating a score            | About 5 to 10 minutes                                                         |

Over a daily limit, requests get `429 rate_limited` until the 24 hours pass.

### Decide [#decide]

| Limit                                                         | Value           |
| ------------------------------------------------------------- | --------------- |
| Tokens per request (`data` + `context` + questions + markers) | 32,768          |
| Questions per request                                         | 1–128           |
| Options per `choice` or `multi_choice`                        | 2–255           |
| Levels per `score` question                                   | 2–32            |
| Question and option key length                                | 1–64 characters |

Over a limit, a request gets `400 invalid_request`, or `413 request_too_large` for tokens and body size.

### How tokens are counted [#how-tokens-are-counted]

`usage.input_tokens` counts everything the model reads:

* **`data` and `context`:** a string as sent, an object or array as compact JSON.
* **A `noul`, `choice` or `score` question:** its instructions and criteria, plus 2 marker tokens per question and 1 per option or level.
* **A `multi_choice`:** each option is asked separately, so each costs the instructions, the list of all options (with 16 or fewer), the option and 2 markers. One with 8 options reads its instructions 8 times.

Question names and option keys aren't read, so they don't count. To size requests, send a representative one and read `usage.input_tokens`; see [Data & context](/concepts/data#size) for fitting a long history.
