API reference · v1

One POST in.
One entity ID out.

JSON over HTTPS, bearer auth, idempotency keys, cursor pagination, dated versions. There are fourteen endpoints and you will mostly use two of them.

Base URLhttps://api.spotit.ai/v1
AuthAuthorization: Bearer spk_live_••••
Version2026-06-01
SpecOpenAPI 3.1 · /openapi.json
Getting started

Quickstart

Create a key, send a string, get an ID. The rest of this page documents the parameters around those three steps.

@spotit/node 1.6.2
import Spotit from "@spotit/node";

const key = process.env.SPOTIT_KEY;
const spotit = new Spotit({ apiKey: key });

const { match, entity } = await spotit.resolve({
  q: "nordwind logistik gmbh hamburg",
  jurisdiction: ["DE"],
});

if (match.decision === "auto_accept") {
  console.log(entity.id, match.confidence);
}
  1. 1
    Create a key

    No card. spk_test_ reads the full graph and is never billed; spk_live_ bills. Start with the resolve and read scopes.

    Get an API key
  2. 2
    Send a string

    Any of the five clients, or plain curl. One field in, and the response already carries the entity ID you will key everything else on.

  3. 3
    Store the ID, not the name

    Put entity_id on your record and join on it from then on. Names change; the ID does not.

Conventions

Four things that hold everywhere

Auth, idempotency, pagination and versioning behave the same on every endpoint, so you only learn them once.

Keys

spk_live_ bills and writes; spk_test_ reads the full graph, caps at 500 calls a day and is never billed. Scopes are resolve, read, search, watch and admin; thresholds, region pins and jurisdiction limits attach to the key too.

Idempotency

Send Idempotency-Key on any POST. We store the response for 24 hours and replay it byte-for-byte; reusing a key with a different body is a 409.

Pagination

Cursor-based, never offset. A list response carries next_cursor until it does not; pass it back as cursor. Cursors are stable across inserts.

Versioning

Dated versions pinned per key. Override per request with Spotit-Version: 2026-06-01. Breaking changes ship under a new date and the previous one runs for twelve months.

GET /v1/events?cursor=…cursor pagination
{
  "data": [
    { "event_id": "evt_01JXR4T8M2C9KD",
      "type": "entity.officer_changed",
      "entity_id": "ent_01JR8K3F5T2QW9" }
  ],
  "has_more": true,
  "next_cursor": "ev_1788524412_8c31f0"
}
response headersevery request
Spotit-Version: 2026-06-01
X-Request-Id: req_8c31f0a92e
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1788524412
X-Credits-Charged: 1
X-Region: eu-central-1
Endpoints

The endpoint index

Credits are charged per unit of work. Lineage, history and the source catalogue are free on every plan.

EndpointDescriptionCreditsp50
POST/v1/resolveResolve one messy input to a canonical entity138 ms
POST/v1/resolve/batchResolve a job of up to 10,000 records (1,000 on Developer)1 / record
GET/v1/entities/{id}Read the full record, optionally as_of a date0.221 ms
GET/v1/entities/{id}/lineageSource document behind each field027 ms
GET/v1/entities/{id}/historyRegister events for the entity, 2009 onward034 ms
GET/v1/entities/{id}/relationsParents, subsidiaries, branches, brands0.229 ms
GET/v1/searchStructured discovery; 100 results per page1 / page86 ms
POST/v1/matchLink two record sets to each other1 / pair54 ms
GET/v1/geo/{id}Communes, districts, NUTS/LAU rollups0.218 ms
POST/v1/subscriptionsSubscribe an entity list or saved query044 ms
GET/v1/eventsPoll change events from a cursor031 ms
GET/v1/sourcesRegistry catalogue and ingest cadence012 ms
GET/v1/jobs/{id}Poll a batch job and fetch its results file09 ms
POST/v1/subscriptions/{id}/replayRe-deliver a window of events038 ms
POST /v1/resolve

Resolve

Takes one messy input and returns one entity, a calibrated confidence and a decision. Add include=explain while you are tuning; drop it in production to save 4 ms of serialisation.

ParameterTypeNotes
qrequiredstringThe messy input. Free text, domain, email domain, VAT, LEI, DUNS, register number, EUID, brand or former name.
typestringOne of legal_entity, branch, brand, place. Narrows the candidate set; omit to search all four.
jurisdictionstring[]ISO 3166-1 alpha-2 codes. Cuts latency roughly in half when you know the country.
hintsobjectdomain, postal_code, city, vat, registration_number, incorporated_year. Treated as evidence; they raise a candidate's score but never exclude one.
thresholdnumberOverrides the key's auto_accept threshold for this call. Default 0.95; the review floor sits 0.15 below it and anything under the floor is a no_match.
includestring[]alternatives · explain · lineage · relations · financials. Each adds fields, not credits.
as_ofstringISO 8601 date. Resolve against the register state that day; defaults to today.
strictbooleanReturn 422 instead of a 200 with decision: no_match. Default false.
POST /v1/resolveheaders
Host: api.spotit.ai
Authorization: Bearer spk_live_8f2c...
Content-Type: application/json
Idempotency-Key: 5f3a90e2-2c41-4c0e-9a77-2d1bd4e6a3c9
POST /v1/resolvebody
{
  "q": "nordw1nd logistik g.m.b.h., am sandtorkai",
  "type": "legal_entity",
  "jurisdiction": ["DE"],
  "hints": {
    "domain": "nordwind-logistik.de",
    "postal_code": "20457"
  },
  "threshold": 0.95,
  "include": ["alternatives", "explain"],
  "as_of": "2026-09-05"
}
200 OK41 ms · eu-central-1
{
  "request_id": "req_8c31f0a92e",
  "latency_ms": 41,
  "match": {
    "entity_id": "ent_01JR8K3F5T2QW9",
    "confidence": 0.987,
    "decision": "auto_accept",
    "matched_on": ["name", "domain", "postal_code"]
  },
  "entity": {
    "id": "ent_01JR8K3F5T2QW9",
    "name": "Nordwind Logistik GmbH",
    "status": "active",
    "jurisdiction": "DE",
    "registry": { "register": "HRB", "number": "148902" }
  },
  "alternatives": [
    { "entity_id": "ent_01JQ9V2H7YB4KC",
      "name": "Nordwind Holding AG",
      "confidence": 0.612 }
  ]
}
response fields
FieldTypeNotes
match.entity_idstringCanonical ID. Stable across renames, mergers and re-registrations.
match.confidencenumber0–1, calibrated per jurisdiction. A 0.91 is right about 91% of the time.
match.decisionenumauto_accept · review · no_match. Derived from confidence against the key's threshold; a no_match still returns 200 unless you send strict=true.
match.matched_onstring[]Which fields agreed. Useful for logging why a match was accepted.
explain.scorersobjectPer-scorer contribution. Only present with include=explain.
explain.conflictsstring[]Fields that disagreed with the winning candidate.
entityobjectThe record itself. Same shape as GET /v1/entities/{id}.
alternativesarrayRunners-up, max 10, sorted descending. Only with include=alternatives.
request_idstringQuote this in support tickets; it is indexed in our logs for 30 days.
include=explainwhy it matched
"explain": {
  "candidates_blocked": 71,
  "candidates_scored": 12,
  "scorers": {
    "lexical":      0.91,
    "embedding":    0.96,
    "registry_key": null,
    "geo":          0.98
  },
  "conflicts": []
}
Branch on the decision

decision is a band on the confidence axis, set by the threshold on your key. Branch on the band rather than on a number you had to pick yourself. A no_match returns 200 by default and a 422 when you send strict=true; neither is billed.

no_matchreviewauto_accept0.500.600.700.800.901.00threshold 0.95, per keyfloor = threshold − 0.15confidence
Core object

The entity object

Everything else on this page either produces this object, points at a field inside it, or tells you when one of those fields changed.

fields
GroupFieldsNotes
idstringStable across renames, mergers and re-registrations.
typeenumlegal_entity · branch · brand · place.
namestringCurrent registered name. former_names carries the rest, with dates.
statusenumactive · liquidation · dissolved · merged · struck_off.
registryobjectauthority, register, number, incorporated_on.
identifiersobjectlei, vat, euid, duns, national ids. Nulls where none is issued.
addressesarrayRegistered seat and any filed operating addresses, with LAU codes.
classificationobjectNACE 2.1, SIC and the national code, each with its source.
financialsobject38 fields from filed accounts. Present for 61% of entities.
relationsobjectparent, subsidiaries, branches, brands, officers (counts, not names).
geoobjectcommune, district, NUTS and LAU, as geo_ ids.
lineagearrayField, source document, published_at, observed_at. With include=lineage.
GET /v1/entities/{id}abridged
{
  "id": "ent_01JR8K3F5T2QW9",
  "type": "legal_entity",
  "name": "Nordwind Logistik GmbH",
  "status": "active",
  "registry": {
    "authority": "Amtsgericht Hamburg",
    "register": "HRB", "number": "148902",
    "incorporated_on": "2011-03-14"
  },
  "identifiers": {
    "lei": "529900NWLG7K2XQF4T81",
    "vat": "DE815402337",
    "euid": "DEK1101R_HRB148902",
    "duns": null
  },
  "former_names": [
    { "name": "Nordwind Spedition GmbH",
      "until": "2019-04-02" }
  ],
  "classification": { "nace": ["52.29", "49.41"] },
  "financials": {
    "revenue_eur": 98400000,
    "headcount": 412, "fiscal_year": 2024
  },
  "relations": {
    "parent": "ent_01JQ9V2H7YB4KC",
    "branches": 4, "officers": 3
  },
  "geo": { "commune": "geo_01JN2R7X8VD4KQ" }
}
One object, four reads

?include= adds lineage, relations or financials to the same object; ?as_of= rewinds every field to a date; /history returns the events that moved them. Nothing changes shape.

POST /v1/resolve/batch

Batch

Up to 10,000 records per request on Team, 50,000 on Business, 1,000 on Developer — processed at roughly 340 records per second. Poll the job or hand us a callback URL; results are a newline-delimited JSON file valid for 7 days.

POST /v1/resolve/batchrequest and 202
POST /v1/resolve/batch
{
  "records": [
    { "id": "crm-8812", "q": "Nordwind Logistik Gmbh" },
    { "id": "crm-8813", "q": "nordwind-logistik.de" },
    { "id": "crm-8814", "q": "DE815402337" }
  ],
  "threshold": 0.90,
  "callback_url": "https://hooks.acme.example/jobs"
}

202 Accepted
{ "job_id": "job_01JYD3K8P2M6TQ",
  "record_count": 3,
  "status": "queued",
  "poll": "/v1/jobs/job_01JYD3K8P2M6TQ" }
job lifecycle
queuedAccepted, waiting for a worker
runningPartial results readable from the cursor
completeresults_url is a signed NDJSON link, 7-day TTL
failedNothing billed; error carries the failing record ids
expiredOlder than 7 days; re-submit to regenerate
CSV in, CSV out

POST /v1/resolve/batch also accepts multipart/form-data. Send a CSV with a header row, name the input column, get back the same file with entity_id, confidence and decision appended.

Lineage

Lineage and point-in-time reads

Two mechanics do most of the work in regulated and diligence workflows: ?include=lineage attaches the source document behind each field, and ?as_of= rewinds the whole record.

GET /v1/entities/{id}/lineage?field=financials.revenue_eur
{
  "field": "financials.revenue_eur",
  "value": 98400000,
  "source": {
    "authority": "Bundesanzeiger",
    "document_id": "src_01JV3D8K1P6NQZ",
    "document_type": "annual_accounts",
    "fiscal_year": 2024,
    "published_at": "2025-11-08",
    "url": "https://bundesanzeiger.de/..."
  },
  "observed_at": "2025-11-08T04:12:09Z",
  "extraction": {
    "method": "structured_xbrl",
    "confidence": 1.0
  },
  "supersedes": "src_01JC2R6P8B3TMV"
}
GET /v1/entities/{id}?as_of=2018-01-01
{
  "id": "ent_01JR8K3F5T2QW9",
  "as_of": "2018-01-01",
  "name": "Nordwind Spedition GmbH",
  "status": "active",
  "financials": {
    "revenue_eur": 41200000,
    "headcount": 168,
    "fiscal_year": 2016
  }
}
Webhooks

Webhook delivery

Every delivery carries a Spotit-Signature header. Verify it, reject anything older than five minutes, and remember that two secrets are live during a rotation.

POST https://hooks.acme.example/watchdelivered
Spotit-Signature: t=1773197251,v1=8f3c2a...,v1=c07be1...
Content-Type: application/json

{
  "event_id": "evt_01JXR4T8M2C9KD",
  "type": "entity.officer_changed",
  "occurred_at": "2026-03-11T00:00:00Z",
  "observed_at": "2026-03-11T04:47:31Z",
  "entity_id": "ent_01JR8K3F5T2QW9",
  "subscription_id": "sub_01JH9F2Q7XB4NM",
  "change": {
    "field": "relations.officers",
    "before": { "count": 2 },
    "after":  { "count": 3 }
  },
  "source": {
    "authority": "Amtsgericht Hamburg",
    "document": "src_01JXQ0W5R8T2LP"
  }
}
delivery semantics
GuaranteeAt-least-once — make your handler idempotent on event_id
Retries1s, 5s, 30s, 5m, 1h, 6h, then dead-letter
Timeout5 s to a 2xx, or it counts as a failure
ReplayPOST /v1/subscriptions/{id}/replay?from=…&to=…
OrderingNot guaranteed — compare occurred_at, not arrival
BatchingUp to 50 events per POST when a burst backs up
Dead letterDrain it from the subscription’s dead-letter queue
SecretsTwo live during a rotation; retire the old one when ready
verify a delivery
import { createHmac, timingSafeEqual } from "crypto";

// secrets is an array: during a rotation, two are live.
export function verify(body, header, secrets) {
  const parts = header.split(",").map(s => s.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || Math.abs(Date.now() / 1000 - +t) > 300) return false;

  return secrets.some(secret => {
    const want = Buffer.from(
      createHmac("sha256", secret).update(t + "." + body).digest("hex"),
      "hex",
    );
    return sigs.some(sig => {
      const got = Buffer.from(sig, "hex");
      return got.length === want.length && timingSafeEqual(got, want);
    });
  });
}
POST /v1/subscriptionscreate one
{
  "name": "DACH industrials — insolvency watch",
  "query": {
    "jurisdiction": ["DE", "AT"],
    "nace": ["28.4", "28.9"],
    "revenue_eur": { "gte": 10e6 }
  },
  "events": [
    "entity.insolvency_filed",
    "entity.status_changed",
    "query.entered"
  ],
  "destination": {
    "url": "https://hooks.acme.example/watch",
    "secret_id": "whsec_01JR2M8F4K9TXQ"
  }
}
MCP & tool schemas

MCP and tool schemas

Seven tools over MCP, inheriting the scopes and thresholds of the key you connect with. The tool descriptions are written for a model to read, and every result carries the citation fields your evaluator needs.

hosted · streamable HTTP
# Claude Code, Cursor, Zed, any MCP client
claude mcp add --transport http spotit \
  https://mcp.spotit.ai/mcp \
  --header "Authorization: Bearer $KEY"

# or run it locally
npx @spotit/mcp --key $SPOTIT_KEY
Scopes live on the key

A read-only key exposes only the read tools. Rate limits, thresholds, region pinning and jurisdiction scopes all follow the key — an agent cannot talk its way past them.

the seven tools
spotit_resolve_entityMessy input → entity ID + confidence
spotit_get_entityFull record, optionally as_of a date
spotit_searchStructured filters: sector, geo, size, form
spotit_eventsRegistry events for a set of entities
spotit_lineageSource document behind any field
spotit_relationsParents, subsidiaries, branches, brands
spotit_geoCommunes, districts, NUTS/LAU rollups
what the model sees
Every resultCarries entity_id, source and observed_at, so groundedness is gradeable without a second retrieval pass.
No matchReturns the reasons rather than a plausible guess.
RefusalBelow the floor it returns the reasons, not the nearest plausible company.
SDKs

Client libraries

Every client is generated from the OpenAPI document, so a field that exists in the API exists in your editor's autocomplete the day it ships.

TypeScript
@spotit/node
v1.6.2
Python
spotit
v1.4.0
Go
github.com/spotit-ai/spotit-go
v0.9.1
Java
ai.spotit:spotit-java
v0.7.4
RustCommunity
spotit-rs
v0.3.0
OpenAPI 3.1Spec
api.spotit.ai/openapi.json
v2026-06-01
Limits

Rate limits

X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are always present. A 429 also carries Retry-After in whole seconds.

PlanSustainedBurstBatch rowsConcurrent jobsWebhook endpoints
Developer10 rps201,00011
Team100 rps30010,000510
Business500 rps1,50050,0002550
EnterpriseNegotiatedBulk filesUnlimitedUnlimited
Failure modes

Errors

Every error carries a stable machine code, the offending parameter, a documentation URL and the request ID our support team will ask for.

StatusTypeWhen
400invalid_request_errorMalformed body, unknown field, or a jurisdiction outside the coverage set.
401authentication_errorMissing, malformed or revoked key. Check the spk_live_ / spk_test_ prefix.
402credit_errorMonthly credits spent and the key has a hard cap set.
403permission_errorThe key's scopes do not include this endpoint or this region.
404not_found_errorEntity ID does not exist, or was merged — the response carries merged_into.
409conflict_errorIdempotency-Key reused with a different body inside the 24 h window.
422no_match_errorOnly with strict=true. The default returns 200 and decision: no_match.
429rate_limit_errorOver the plan's rps or burst. Retry-After is always set.
500api_errorOur fault. Safe to retry; the request_id is already in our logs.
503region_errorPinned region is degraded. Retry without a pin to fail over.
400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "code": "jurisdiction_unknown",
    "message": "Jurisdiction 'XX' is not covered.",
    "param": "jurisdiction",
    "doc_url": "spotit.ai/docs/errors#jurisdiction",
    "request_id": "req_2b9d41c7f0"
  }
}
422 No match
{
  "error": {
    "type": "no_match_error",
    "code": "below_floor",
    "message": "Best candidate 0.61 < floor 0.80",
    "reasons": ["no_key", "geo_conflict"],
    "best": "ent_01JQ8B4X2VT7RC",
    "billed": false,
    "doc_url": "spotit.ai/docs/errors#below_floor",
    "request_id": "req_5a1e93b7c2"
  }
}
429 Rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1788524412

{
  "error": {
    "type": "rate_limit_error",
    "code": "rps_exceeded",
    "doc_url": "spotit.ai/docs/errors#rate_limit",
    "request_id": "req_c40b8d1f6e"
  }
}
Regions & residency

Regions and residency

Entity IDs are global. Where a request is processed and where its logs land is a per-key setting, and failover never crosses a residency boundary: EU keys fail over within the EU, US and APAC keys return 503 rather than leave their region.

RegionHostp50 resolveFailoverPlans
eu-central-1Frankfurt38 mseu-west-1All
eu-west-1Dublin41 mseu-central-1Team+
us-east-1N. Virginia44 msTeam+
ap-southeast-1Singapore58 msBusiness+
Ecosystem

Connectors

The integrations we build, document and support ourselves. The remaining ninety-odd are community-maintained against the OpenAPI spec and listed in the catalogue.

The whole spec is public.

Read the OpenAPI document, generate a client, and call the sandbox before you talk to anyone here. Test keys resolve against the full graph with a 500-call daily cap.

Endpoints
14
Spec
OpenAPI 3.1
Version
2026-06-01
Breaking changes
dated, 12-mo notice