API reference
The Finic API's six endpoints, how requests authenticate, the errors they return and the limits they run under.
Base URL https://api.finic.ai. Send an API key as Authorization: Bearer gsk_… (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 | POST /v1/score | Scores one record from 0 to 1000 |
create_score | POST /v1/scores | Creates a score from labeled examples |
list_scores | GET /v1/scores | Lists your scores, with what each predicts |
calibrate | POST /v1/calibrate | Sends outcomes, to improve a score |
get_job_status | GET /v1/jobs/{job_id} | Whether creating or calibrating has finished |
decide | POST /v1/decide | Answers your questions about a case |
Errors
A failed request returns an error status and this 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 has fields that differ from its score's 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 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
402before their data is checked; creating and calibrating never do. - A
502or504from the network edge, during a deployment or an outage, has no JSON body orX-Request-Id: treat it like503. 202fromcreate_scoreandcalibratemeans accepted, with the work running in a job. A failed job reports its error atget_job_status, in the same shape withoutparam.- A
429for the request rate saysToo 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_ids 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
decideis safe to retry; a retry of a request that succeeded makes the decision again.scorewith the samescore_idscores a record once: if the first call succeeded, the retry gets409 duplicate_score_id, without the score.create_scoreretried after a timeout can create a second score, counted toward the daily limit.calibrateretried resends the same outcomes, which replace themselves; while the first call's job runs, it gets409 calibration_in_progress.get_job_statusis 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.
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 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
| Limit | Value |
|---|---|
| Request body | 2 MB |
| Request rate | 5 per second per organization, in bursts of up to 10 |
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). For higher volume, contact support@finic.ai.
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
| 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
usage.input_tokens counts everything the model reads:
dataandcontext: a string as sent, an object or array as compact JSON.- A
noul,choiceorscorequestion: 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 for fitting a long history.