Score a record with one POST.
The ax1om Scoring API takes your CRM field values and returns a fit score (0-100) and a conversion likelihood. Two endpoints, Bearer key auth, and a stable versioned contract. Mint a key in the app and make your first call in minutes.
From key to first score.
- Mint a key. In the app, open Settings, API keys, then Create key. The secret (
ax1m_sk_...) is shown once. Save it. API scoring is available on Pro and above. - Grab a model_id. Any trained model in your workspace. It looks like
mdl_abc123. - POST a record. Send the fields you have. You do not need every field the model trained on.
curl -X POST https://api.ax1om.ai/v1/score \
-H "Authorization: Bearer ax1m_sk_your_key" \
-H "Content-Type: application/json" \
-d '{
"model_id": "mdl_abc123",
"records": [
{ "title": "VP Sales", "industry": "SaaS", "employees": 250 }
]
}' You get back a fit score, a conversion likelihood, and your month-to-date usage:
{
"model_id": "mdl_abc123",
"scores": [
{ "score": 82, "conversion_likelihood": 0.823, "enriched": false }
],
"records_scored": 1,
"cache_hits": 0,
"usage": { "current": 151, "limit": 50000, "remaining": 49849, "in_grace": false, "backstop": 100000 }
} Bearer key on every request.
Every call sends your key as a Bearer token. The webhook endpoint also
accepts an X-API-Key header for CRM tools that map headers
more easily. Keys start with ax1m_sk_. Treat the secret
like a password: it is shown once at creation and stored only as a hash.
Authorization: Bearer ax1m_sk_your_key Test keys
A key that starts with ax1m_sk_test_ scores against the
real model but meters into a separate free bucket (1000 records/mo).
It never touches your live quota and never bills. Use it to wire up
and verify an integration before you flip to a live key. Test-key
responses carry "test": true.
curl -X POST https://api.ax1om.ai/v1/score \
-H "Authorization: Bearer ax1m_sk_test_your_test_key" \
-H "Content-Type: application/json" \
-d '{ "model_id": "mdl_abc123", "records": [ { "title": "VP Sales" } ] }' /v1/scoreScore up to 1000 records in one call. Returns one row per input record, in order. Records that match your existing scored data come back enriched with a delta; unmatched records are scored from the fields you send (see Cold records).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
explain | boolean | No | Opt in to per-record drivers and top_reason_1..3 strings. Adds a few ms; off by default. |
model_id | string | Yes | The trained model to score against (mdl_...). |
records | array<object> | Yes | Up to 1000 records. Each is a flat object of your CRM field values. Extra fields are allowed; unknown fields are ignored by the model. |
Response
| Field | Type | Description |
|---|---|---|
cache_hits | integer | How many records matched your existing scored data. |
model_id | string | |
records_scored | integer | Count of records scored in this call. |
scores | array<V1ScoreRow> | |
test | boolean | Present and true only for test keys (ax1m_sk_test_...). Never billed. |
usage | V1Usage |
Score row (each item in scores)
| Field | Type | Description |
|---|---|---|
conversion_likelihood | number (float) | Model conversion likelihood, 0-1 (4 dp). |
crm_id | string | Echoed back when supplied on the input record. |
delta | integer | score minus previous_score, when a previous score exists. |
drivers | array<object> | Per-record feature contributions. Present only when explain=true. |
enriched | boolean | true only when the record matched your existing scored data. false = thin score from the fields you sent (cold record). |
previous_score | integer | Prior score for this crm_id, when one exists. |
score | integer | Fit score, 0-100. |
top_reason_1 | string | null | Top driver string. Present only when explain=true. |
top_reason_2 | string | null | Second driver. Present only when explain=true. |
top_reason_3 | string | null | Third driver. Present only when explain=true. |
warnings | array<string> | Present only when the record trips the recency age guard. Metadata only; the score is not changed. |
/v1/webhooks/score
A single-record variant with a flat response, shaped for Marketo,
Zapier, and other tools that map fields one to one. The record is the
raw JSON body; model_id is a query parameter.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
model_id | query | string | Yes |
authorization | header | string | No |
X-API-Key | header | object | No |
Send the field values as a flat JSON body:
curl -X POST "https://api.ax1om.ai/v1/webhooks/score?model_id=mdl_abc123" \
-H "X-API-Key: ax1m_sk_your_key" \
-H "Content-Type: application/json" \
-d '{ "Title": "VP Sales", "Industry": "SaaS", "LeadSource": "Webinar" }' Response
| Field | Type | Description |
|---|---|---|
age_sentinel | string | null | Recency age-guard note. Always present (null when in-range) for sentinel-trained models only. |
conversion_likelihood | number (float) | |
delta | integer | score minus previous_score, when a previous score exists. |
percentile | integer | Approximate percentile for the single record. |
previous_score | integer | Present when a prior score exists for record_id. |
record_id | string | Echoed crm_id when supplied. |
score | integer | |
test | boolean | Present and true only for test keys. |
tier | "hot" | "warm" | "cold" | |
top_reason_1 | string | null | |
top_reason_2 | string | null | |
top_reason_3 | string | null |
{
"score": 87,
"tier": "hot",
"percentile": 87,
"conversion_likelihood": 0.8732,
"top_reason_1": "VP title (2.3x lift)",
"top_reason_2": "SaaS industry",
"top_reason_3": "Webinar source"
} A new lead still gets a real score.
When you score a record that is not yet in your existing scored data,
ax1om scores it from exactly the fields you send. Nothing is invented
and no account matching happens on the request path. The response
tells you plainly with the enriched flag:
enriched | Meaning |
|---|---|
true | The record matched your existing scored data. It carries a previous_score and delta where available. |
false | A cold record. Scored from the fields in your request only. Send richer input to get a richer score. |
The score is always real, never a placeholder. enriched: false
is a signal about input completeness, not score quality.
One envelope, stable codes.
Every /v1 error returns the same shape. Branch on
error.code, never on the HTTP status or the message text.
The codes are frozen: new ones may be added, existing ones never change.
{
"error": {
"code": "invalid_api_key",
"message": "API key required. Send Authorization: Bearer ax1m_sk_...",
"docs_url": "https://ax1om.ai/docs/api"
}
} | Code | HTTP | When it fires |
|---|---|---|
invalid_api_key | 401 | Missing key, malformed key, or a revoked/unknown key. |
bad_request | 400 / 422 | Malformed body, no records, or a record the model could not featurize. |
record_limit_exceeded | 400 | More than 1000 records in one batch call. |
model_not_found | 404 | The model_id does not exist for your org. |
quota_exceeded | 429 | Scoring is paused for the month: usage passed the included limit and the grace band, and hit the hard backstop. Upgrade or contact support to resume. |
rate_limited | 429 | Too many requests per second for the key. Slow down and retry. |
internal_error | 500 | An unexpected server error. Safe to retry with backoff. |
Two limits, both visible.
A per-key rate limit (default 10 requests/second)
guards against bursts and returns rate_limited when
tripped. A separate monthly metered quota counts the
records you score. The monthly limit is a soft cap: live keys on a
paid plan keep scoring past it in a grace band
(in_grace: true, remaining goes negative)
and only hard-stop with quota_exceeded at the
backstop. Test keys meter into their own free bucket
with a hard cap.
Every batch response includes a usage object so you always
know where you stand this month:
| Field | Type | Description |
|---|---|---|
backstop | integer | null | Hard stop for this billing month. Scoring pauses (429, code quota_exceeded) once usage reaches it. null for test keys. |
current | integer | Records scored so far this billing month, including this call. |
in_grace | boolean | true once the org is scoring past its included monthly limit, in the grace band. |
limit | integer | Monthly included scored-record limit for the plan (test keys meter into a separate free bucket). |
remaining | integer | Records left this month. Goes negative in the grace band once usage passes the included limit. |
Batch up to 1000 records per call to stay well under the rate limit.
Stable by prefix.
The API is versioned by URL prefix (/v1). Additive changes,
new response fields, new optional request fields, and new endpoints ship
within /v1 without a version bump. Breaking changes, renaming
or removing a field, changing a type, or changing an error code, get a new
prefix, never a silent change to /v1.
Deprecated root aliases
The unversioned POST /score and POST /webhooks/score
still work but are deprecated. They return the pre-v1 contract (the
0-1 float is named probability) and carry
Deprecation: true plus a Sunset response
header (RFC 8594). Deprecated surfaces run for a minimum of 90 days.
Sunset date: 2026-10-19. New integrations should use
/v1, where the same value ships as
conversion_likelihood and errors use the stable envelope.
| Use this | Not this (deprecated) | Sunset |
|---|---|---|
POST /v1/score | POST /score | 2026-10-19 |
POST /v1/webhooks/score | POST /webhooks/score | 2026-10-19 |
Mint a key and score your first record.
Wire it up with a test key, verify the shape, then flip to live. No SDK required, just HTTP.