---
title: "Company entity resolution REST API"
summary: "Every /v1 endpoint with what it costs, worked payloads including an abstention, the error model, limits and idempotency. Each endpoint and error code links its anchor on the page."
canonical_url: https://entitybind.com/api
last_updated: 2026-10-12
copy: full
---

# Company entity resolution REST API

> Every /v1 endpoint with what it costs, worked payloads including an abstention, the error model, limits and idempotency. Each endpoint and error code links its anchor on the page.

Canonical page: https://entitybind.com/api
Last updated: 2026-10-12

## One call in, one company out — or the question that would settle it.

You are wiring company matching into something that has to be defended afterwards, and the rows that will hurt you are the ones the matcher was not sure about. `POST /v1/resolve` takes a name, a [company number](https://entitybind.com/identifiers/company-number), an [LEI](https://entitybind.com/identifiers/lei), a domain or a scrap of an address, and returns one company record with a citation on every field, a `match_confidence`, and a `decision_id` that reopens the evidence at a URL. When the evidence will not carry a match it returns `unresolved` instead, naming the column that would settle it, at zero credits. 4 endpoints can charge. Every read, receipt and replay is free.

- Endpoints: 41
- That can charge: 4
- An abstention: 0 credits
- Confidence basis: Per population
- Subscriptions: Available in Billing

The ask: A first run reads a copy of your CRM, MDM or supplier export and hands back what resolved, which of your existing matches we would not have made, and what we would not decide. [Start free →](https://app.entitybind.com/app?signup=1)

Self-service. Create your workspace online and verify your email to start matching company records. Free starts with 300 credits and no card. Paid monthly plans are available in workspace Billing; VAT is added where applicable. Usage is tracked without automatic overage charges. [Trust and security](https://entitybind.com/trust)

## The company entity resolution API, and who it is for

One endpoint decides which registered company a row of yours points at, and hands back the evidence for the decision.

Entity Bind is a company entity resolution API. You post a record you already hold, a company name with whatever else the row carries, and `POST /v1/resolve` answers with the one company it belongs to: a citation on every field, a `match_confidence`, and a `decision_id` that reopens the whole evidence trail at a URL. Where the evidence will not carry a match, the same call returns `unresolved` with a ranked shortlist and the one column of yours that would have settled it. It is built for data, CRM and revenue operations teams who have to explain, months later, why two rows became one company.

That is three jobs in practice, and one endpoint answers all three. A data team deduplicating a warehouse table wants the clusters stated in its own keys rather than in ours. A CRM administrator after a migration or a merger wants to know which account rows are the same company and, more usefully, which merges to refuse. A revenue operations lead wants the duplicate accounts found without a wrong merge quietly welding two customers into one. `client_ref` is what carries your key through the pipeline, so every answer joins back to the row it came from.

- **it accepts**: At least one identifying field: a name, a website or domain, an email, a phone number, a registration number, an LEI, a VAT number, a D-U-N-S, an ISIN, a ticker, or previous and trading names. Country and postal code do not count as identifying on their own; they narrow the shortlist.
- **it returns**: One company record with a citation on every field, a `match_confidence` with the population it was measured over beside it, and a `decision_id`. Or `unresolved`, with the shortlist, a typed reason and a review id.
- **it will not do**: Write to your CRM, ERP, MDM or warehouse. You get result sets, and applying them stays inside your own change control.
- **it is not for**: Entity Bind resolves companies and organisations only. It does not resolve people, vessels, bank-account ownership, sanctions exposure, emissions or geolocation, and its results may not be used for eligibility decisions about individuals, including employment, credit and tenancy.

## Built for

Engineers wiring company identity into a master somebody has to defend — an MDM or stewardship queue, a CRM after a migration or a merger, a supplier file before a payment run, counterparty onboarding that has to survive an audit. And for agents: the same pipeline is on [the MCP server](https://entitybind.com/mcp), with the same key, the same tenancy and the same credit rule.

## Wrong for

Buying breadth as the product: we hold a fraction of an incumbent’s records and are not building a global company graph. If you already run a licensed data feed or an MDM platform, run this beside it and let it check the matches they propose. Entity Bind resolves companies and organisations only. It does not resolve people, vessels, bank-account ownership, sanctions exposure, emissions or geolocation, and its results may not be used for eligibility decisions about individuals, including employment, credit and tenancy.

## Your first call

One header, one JSON body, one endpoint. With a key, the call below is the whole integration for a single record.

### Authentication

A bearer token on every request: `Authorization: Bearer rk_…`. The same credential works here and on [the MCP server](https://entitybind.com/mcp), against the same tenant, meter and rate-limit buckets.

### Base URL

The hosted base URL is https://app.entitybind.com. Set ENTITYBIND_BASE_URL to that origin. Self-hosted deployments use their own configured base URL.

#### How you get a key, and how it rotates

two paragraphs

The full-pipeline evaluation is arranged separately and is not activated by signup. Request it for your workspace; we confirm availability, sources and the start date before activation. The request for the planned 1,000-credit evaluation goes through /contact#evaluation. Live keys are ones you create yourself, in your Entity Bind workspace: read or resolve scope, each raw key shown once, and listed and revoked in the same place. Create your workspace online and verify your email. The hosted base URL is https://app.entitybind.com; configured sources are listed in your workspace and the API reference. Paid plans are available through Stripe in workspace Billing; registry checks follow your active plan. Only the key's SHA-256 digest is stored, so a lost key is replaced rather than recovered.

In your Entity Bind workspace, rotating a key is create, switch, revoke: make the new key, move the integration onto it, then revoke the old one. Through the admin API's rotate endpoint, rotation is a window rather than a swap: both credentials authenticate for the configured grace period, so a deploy can overlap. A grace period of zero is the leak path, and is a decision you make on purpose.

POST /v1/resolve · the first call:

```bash
curl -sS -X POST "$ENTITYBIND_BASE_URL/v1/resolve" \
  -H "Authorization: Bearer $ENTITYBIND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "HSBC Holdings plc",
        "registration_number": "00617987",
        "jurisdiction": "GB",
        "country": "GB",
        "depth": "research",
        "client_ref": "identity-check"
      }'
```

That resolves HSBC Holdings plc; check it against [the Companies House record](https://find-and-update.company-information.service.gov.uk/company/00617987), then try companies you choose yourself. The Acme payloads below are fictional: they show the schema, not a recording of that call. Calling from an agent? [The plain-text quickstart](https://entitybind.com/api/quickstart.txt) has the calls and the access requirements.

POST /v1/resolve · request:

```json
{
  "name": "Acme Widgets Ltd",
  "domain": "acmewidgets.co.uk",
  "country": "GB",
  "address": {
    "line1": "12 Kingsway",
    "city": "London",
    "postal_code": "WC2B 6UN",
    "country": "GB"
  },
  "trading_names": ["Acme Widgets"],
  "depth": "research",
  "enrich": true,
  "client_ref": "crm-4417"
}
```

Back comes a match reached by the probabilistic stage: no model call, no registry call, one credit. Read `decision`, then `match_confidence` with the basis beside it, then `decision_id` and `usage.credits_charged`.

#### The whole 200 response

121 lines · 6 field notes

POST /v1/resolve · 200:

```json
{
  "record_id": "rec_1f4c2b9d6a7e4f0b8c3d5e6f7a8b9c0d",
  "client_ref": "crm-4417",
  "decision": "match",
  "stage": "probabilistic",
  "confidence": 0.999,
  "match_confidence": 0.9755,
  "match_confidence_basis": "calibrated_probability",
  "entity": {
    "entity_id": "ent_8a5f3c11d94b47c2b0e6a1f7c3d29e40",
    "record": {
      "identity.legal_name": "ACME WIDGETS LIMITED",
      "identity.trading_names": ["Acme Widgets"],
      "identity.legal_form": "private-limited-company",
      "registration.company_number": "09876543",
      "registration.jurisdiction": "GB",
      "registration.lei": "213800QVXQAZLPZH1234",
      "registration.status": "active",
      "registration.incorporated_on": "2016-02-11",
      "digital.domain": "acmewidgets.co.uk",
      "digital.website": "https://www.acmewidgets.co.uk",
      "contact.phone": "+442071234567",
      "location.registered_address": "12 Kingsway, London, WC2B 6UN",
      "location.country": "GB",
      "identity.identifiers": {
        "lei": "213800QVXQAZLPZH1234",
        "company_number": "09876543",
        "jurisdiction": "GB"
      }
    },
    "provenance": {
      "identity.legal_name": {
        "source_id": "companies_house",
        "source_url": "https://find-and-update.company-information.service.gov.uk/company/09876543",
        "retrieved_at": "2026-08-30T09:14:02Z",
        "source_as_of": "2026-08-29",
        "confidence": 0.98,
        "licence": "open",
        "first_seen_at": "2026-05-02T11:20:44Z",
        "last_checked_at": "2026-08-30T09:14:02Z",
        "checks": 7,
        "valid_from": null,
        "valid_to": null,
        "age_days": 2.12,
        "licence_permits_resale": true,
        "resellable": false
      },
      "digital.domain": {
        "source_id": "customer",
        "source_url": null,
        "retrieved_at": "2026-09-01T10:02:55Z",
        "source_as_of": null,
        "confidence": 0.9,
        "licence": "open",
        "first_seen_at": "2026-09-01T10:02:55Z",
        "last_checked_at": "2026-09-01T10:02:55Z",
        "checks": 1,
        "valid_from": null,
        "valid_to": null,
        "age_days": 0.0,
        "licence_permits_resale": true,
        "resellable": false
      },
      "identity.identifiers": {
        "source_id": "identifier_crosswalk",
        "source_url": "https://api.gleif.org/api/v1/lei-records/213800QVXQAZLPZH1234",
        "retrieved_at": "2026-09-01T10:02:56Z",
        "source_as_of": null,
        "confidence": 0.95,
        "licence": "open",
        "first_seen_at": "2026-08-30T09:14:02Z",
        "last_checked_at": "2026-08-30T09:14:02Z",
        "checks": 3,
        "valid_from": null,
        "valid_to": null,
        "age_days": 2.12,
        "licence_permits_resale": true,
        "resellable": false
      }
    },
    "member_record_ids": [
      "rec_0b91d7e34c2a41f6851b7d09e2c4a6f3",
      "rec_1f4c2b9d6a7e4f0b8c3d5e6f7a8b9c0d"
    ],
    "created_at": "2026-05-02T11:20:44Z",
    "updated_at": "2026-09-01T10:02:56Z",
    "last_checked_at": "2026-09-01T10:02:55Z",
    "stalest_field_age_days": 2.12
  },
  "candidates_considered": 1,
  "candidates": [
    {
      "entity_id": "ent_8a5f3c11d94b47c2b0e6a1f7c3d29e40",
      "external": false,
      "legal_name": "ACME WIDGETS LIMITED",
      "domain": "acmewidgets.co.uk",
      "legal_status": "active",
      "probability": 0.999,
      "adjudicator_decision": null,
      "adjudicator_confidence": null
    }
  ],
  "clarification": null,
  "more_input_needed": false,
  "depth_capped": false,
  "entity_created": false,
  "replayed": false,
  "escalation_unavailable": false,
  "no_identifying_signal": false,
  "pipeline_version": "2026.09.0",
  "decision_id": "dec_5c7e0a2b8d1f42a39e6b4c5d7f8a9b01",
  "review_id": null,
  "correlation_id": "req_9d3f7c1a4b2e48c5b6a7d8e9f0a1b2c3",
  "usage": {
    "credits_charged": 1,
    "credits_remaining": 4871,
    "deep_attempts_remaining": 2988,
    "plan": "growth",
    "enforced": false
  }
}
```

- `match_confidence · match_confidence_basis`: The one comparable quantity, and what it rests on. Threshold on this. The basis names the population the number was measured over, so a calibrated_probability and an identifier_rule are not being read as if they were the same evidence.
- `confidence`: Deprecated, and in the payload only so existing clients do not break. A measurement retired it: it is not comparable across stages and it is not a filter. Read match_confidence instead.
- `entity.record · entity.provenance`: Keyed identically, so every fact in the golden record has a citation under the same key: source, URL, retrieval time, licence, first seen, last checked, and how many times it has been confirmed.
- `stalest_field_age_days`: The age of the entity's oldest field, not its newest. One freshly confirmed value cannot make a stale record look current, which is the failure mode of every last_updated column.
- `decision_id`: The receipt. It redeems the features, the weighted contributions, the thresholds the call was judged under, the cited evidence and a replay of the score, free, at GET /v1/decisions/{id}.
- `usage.credits_charged`: What this call cost, in the response that cost it. One credit here, because the probabilistic stage decided and nothing had to be adjudicated or researched.

### What the request accepts

Unknown fields are rejected rather than silently ignored. A typo in a field name is a 422 on the first call rather than a column that quietly never arrived, which is the failure that costs a week.

#### Every field a resolve accepts

25 fields · 3 groups

##### Identifying

At least one is required.

`name` · `website` · `domain` · `email` · `phone` · `registration_number` · `lei` · `trading_names` · `previous_names` · `vat_number` · `duns` · `isin` · `ticker`

A request with none of these is rejected with a 422 invalid_record before anything runs. Country and postal code are not on this list on purpose: they qualify an identity rather than being one.

##### Qualifying

Optional, and each one narrows the shortlist.

`country` · `address` · `jurisdiction` · `incorporation_date` · `legal_form` · `exchange` · `parent_name` · `context` · `extra`

context is up to 1,000 characters of plain language. A town, trading address or business description can help retrieval and ranking, but vague business context alone has no established match-quality benchmark. address is structured; put city and postal_code inside it. Country names and codes are normalised without implying registry coverage. extra takes up to 50 key/value pairs.

##### Control

Optional, and these are what a cost-conscious integration sets.

`depth` · `enrich` · `client_ref`

depth is probabilistic, adjudication or research, defaulting to research. enrich defaults to true. client_ref is up to 200 characters and is echoed back verbatim, so the response joins to your file on your key.

### Do not threshold on confidence

`confidence` is deprecated and stays in the payload only so existing clients do not break. The measurement that retired it: the lowest value any binding response ever returned was 0.9000, so a `confidence >= 0.85` rule admitted all 791 bindings and 254 of the 382 abstentions with them. Threshold on `match_confidence`, and read `match_confidence_basis` for the population it was measured over. The deprecation is dated in the [changelog](https://entitybind.com/changelog#2026-08-13-match-confidence).

The bar underneath both fields is the acceptance floor, 0.90, enforced in code. It is an acceptance contract, not calibration evidence: it says what we refuse to return, not how often an accepted match is right. Read it with the coverage it costs.

The call above needs a key. Create your workspace online, verify your email and create a resolve-scoped key in API keys. Free is capped at shallow matching; paid plans activate greater depth. We write nothing back to your systems, and an abstention costs nothing. [Start free →](https://app.entitybind.com/app?signup=1)

## Every endpoint, and what each one costs

41 routes in 5 groups. 4 of them decide something and can charge; every read, receipt, queue, export and reference collection is free.

### The data plane (4 endpoints)

| Endpoint | What it does | Cost |
| --- | --- | --- |
| [POST /v1/resolve](https://entitybind.com/api#post-v1-resolve) | One record in, one bound entity out — or an abstention, with the shortlist behind it. | 1 credit shallow · 5 deep · 0 abstained |
| [POST /v1/match](https://entitybind.com/api#post-v1-match) | Compare two records without storing either: is this the same company as that one? | 1 credit shallow · 5 deep · 0 abstained |
| [POST /v1/batch](https://entitybind.com/api#post-v1-batch) | Up to 500 records in one request, order-independent within the batch. | 1 credit shallow · 5 deep · 0 abstained |
| [POST /v1/crosswalk](https://entitybind.com/api#post-v1-crosswalk) | Resolve one identifier or a name, then return every identifier system the company is known by. | 1 credit shallow · 5 deep · 0 abstained |

#### What each one does, in detail

4 notes

The four endpoints that decide something, and the only four that can charge. Each is priced by the stage that reached the answer, and only on success. An abstention is free at every stage, including the ones that spent the most getting there.

- `POST /v1/resolve`: Mints a record id, creates or joins an entity, and writes a decision receipt that reopens at GET /v1/decisions/{id}. An abstention returns the ranked shortlist, a review id and the same receipt. Rate-limit class RESOLVE.
- `POST /v1/match`: Persists nothing, so it mints no record id, no entity id and no receipt — there is nothing to fetch afterwards, and nothing lands in your catalogue. Same credit rule as resolve. Rate-limit class MATCH.
- `POST /v1/batch`: Idempotency-Key aware, one ledger row per outcome rather than one summed row, and it reports the pairs it withheld between your own rows. It is not a queue: batches run one at a time, so a 300,000-row file is an overnight window to plan rather than a throughput figure. Rate-limit class BATCH.
- `POST /v1/crosswalk`: Billed as the resolve it is; the identifier projection is free. Each identifier is separately cited, the systems asked that came back empty are listed as checked_and_empty, and one hop of ownership is walked. Accepts max_hops 1–10 and as_of.

### Entities, records and crosswalk reads (11 endpoints)

| Endpoint | What it does | Cost |
| --- | --- | --- |
| [GET /v1/crosswalk/{entity_id}](https://entitybind.com/api#get-v1-crosswalk-entity-id) | Read the identifier map back for an entity you already own. | Free |
| [GET /v1/entities](https://entitybind.com/api#get-v1-entities) | List the catalogue, newest first. | Free |
| [GET /v1/entities/{entity_id}](https://entitybind.com/api#get-v1-entities-entity-id) | One golden record with per-field provenance. | Free |
| [GET /v1/entities/search](https://entitybind.com/api#get-v1-entities-search) | Triage the catalogue by name or domain. | Free |
| [GET /v1/entities/export](https://entitybind.com/api#get-v1-entities-export) | An NDJSON stream of the whole catalogue. | Free |
| [POST /v1/entities/reconcile](https://entitybind.com/api#post-v1-entities-reconcile) | Repair dangling entity_id foreign keys by exact identifier equality. | Free |
| [DELETE /v1/entities/{entity_id}](https://entitybind.com/api#delete-v1-entities-entity-id) | Erasure that cascades to everything the entity reaches. | Free |
| [POST /v1/entities/{entity_id}/detach](https://entitybind.com/api#post-v1-entities-entity-id-detach) | The audited single-record split, with a receipt on both sides. | Free |
| [GET /v1/records/unbound](https://entitybind.com/api#get-v1-records-unbound) | Every submission no entity claims. | Free |
| [GET /v1/records/by-ref/{client_ref}](https://entitybind.com/api#get-v1-records-by-ref-client-ref) | Map your own key for a row to the ids it produced. | Free |
| [DELETE /v1/records/{record_id}](https://entitybind.com/api#delete-v1-records-record-id) | Erase one submission. | Free |

#### What each one does, in detail

11 notes

Reading back what you already own, repairing it, and erasing it. All free: you paid for the decision, and reading its result is not a second decision. The catalogue is yours to read, export and delete.

- `GET /v1/crosswalk/{entity_id}`: A projection of a decision you have already paid for, so it is not a second decision.
- `GET /v1/entities`: Paged, at a maximum page size of 200. Tenant-scoped like every read here: an id belonging to another tenant answers 404 rather than 403, so ids cannot be probed.
- `GET /v1/entities/{entity_id}`: Field-level provenance includes supplier, supplier record and licence reference when supplied, plus the entity's own last_checked_at and stalest_field_age_days — because one freshly confirmed field should not make a stale record look current.
- `GET /v1/entities/search`: Returns three fields and no more — entity_id, legal_name, domain — so a free read cannot be turned into a free enrichment endpoint.
- `GET /v1/entities/export`: Portability, in the GDPR article 20 sense. Declared rate-limit class BATCH rather than READ: it walks the whole catalogue, and a limit sized for a primary-key lookup would not describe what the call costs.
- `POST /v1/entities/reconcile`: Precedence LEI, then registration number, then domain — never a name and never a score. Input order is preserved and your client_ref echoed, and an identifier held by two entities comes back as ambiguous_entity_ids rather than as a guess.
- `DELETE /v1/entities/{entity_id}`: Member records, open review items, replay bindings and cached idempotent responses all go. Settled review items are redacted rather than deleted, and decisions survive on purpose: they are the retained audit evidence that the erasure itself was authorised.
- `POST /v1/entities/{entity_id}/detach`: Requires a review-scoped credential, so an ingestion key cannot do it. Not a general unmerge: no arbitrary cluster split, no rollback to a historical shape, and six preconditions that fail closed with a 409 before any write happens.
- `GET /v1/records/unbound`: What abstained, and what replayed. Entity erasure cannot reach these by definition, which is why they are enumerable and erasable on their own.
- `GET /v1/records/by-ref/{client_ref}`: record_id, decision_id, entity_id and review_id for a client_ref you sent, so the join back into your file is on your identifiers rather than ours.
- `DELETE /v1/records/{record_id}`: For the rows that never bound to an entity, and for a row you sent by mistake.

### Receipts, review and labels (9 endpoints)

| Endpoint | What it does | Cost |
| --- | --- | --- |
| [GET /v1/decisions/{decision_id}](https://entitybind.com/api#get-v1-decisions-decision-id) | The receipt: features, weighted contributions, thresholds, cited evidence and a replay of the score. | Free |
| [GET /v1/decisions](https://entitybind.com/api#get-v1-decisions) | The same receipts as a population, newest first. | Free |
| [GET /v1/review-queue](https://entitybind.com/api#get-v1-review-queue) | The abstention queue. | Free |
| [GET /v1/review-queue/{review_id}](https://entitybind.com/api#get-v1-review-queue-review-id) | One queued item, with its shortlist and the decision that queued it. | Free |
| [POST /v1/review-queue/{review_id}/decision](https://entitybind.com/api#post-v1-review-queue-review-id-decision) | Settle one item. | Free |
| [POST /v1/review-queue/decisions](https://entitybind.com/api#post-v1-review-queue-decisions) | Settle up to 200 items in one call. | Free |
| [POST /v1/labels](https://entitybind.com/api#post-v1-labels) | Append an independently attributable customer or import label. | Free |
| [GET /v1/labels](https://entitybind.com/api#get-v1-labels) | Read the label log back. | Free |
| [GET /v1/labels/export](https://entitybind.com/api#get-v1-labels-export) | A reproducible JSONL label slice. | Free |

#### What each one does, in detail

9 notes

The audit surface. Every decision reopens at a URL with its features, thresholds and evidence, the abstention queue is worked through the same API, and labels append rather than overwrite so a calibration run can be rebuilt from the rows it used.

- `GET /v1/decisions/{decision_id}`: Free. Auditing a decision you have already paid for is not a second decision, so the evidence trail never carries a price. Accepts a resolve credential or a review credential.
- `GET /v1/decisions`: Filterable by pipeline_version, threshold_upper, decided_since and decided_until, and by acting_key_id. This is the remediation read after a policy change, and the four-eyes read of which merges one credential approved.
- `GET /v1/review-queue`: Filtered and ordered in SQL rather than in your client: min_confidence, and sort by oldest_first, newest_first, confidence_desc or confidence_asc.
- `GET /v1/review-queue/{review_id}`: The review id an unresolved response handed back is the id you read here.
- `POST /v1/review-queue/{review_id}/decision`: A verdict must name its reviewer, applies through the merge guard rather than around it, records the acting credential and surface, and settles the record so it stops re-queueing. A second verdict on the same item is a 409.
- `POST /v1/review-queue/decisions`: Per-item results rather than one transaction: a typed failure comes back as a verdict carrying an error_code while the rest proceed, so one bad row cannot roll back an afternoon of review.
- `POST /v1/labels`: Idempotent on source_event_id. A correction appends a revision naming supersedes_label_id, and the original row is never overwritten — a label log you can rewrite is not evidence.
- `GET /v1/labels`: Including the superseded rows, which is the point of appending rather than updating.
- `GET /v1/labels/export`: Filtered by model, prompt, pipeline, source and evidence tier, so a calibration claim can be rebuilt from exactly the rows it was made on.

### Usage and reference data (2 endpoints)

| Endpoint | What it does | Cost |
| --- | --- | --- |
| [GET /v1/usage](https://entitybind.com/api#get-v1-usage) | Your own automated, abstained and human-reviewed rates, each with the range a sample that size supports — a Wilson interval — plus the stage, decision and endpoint mix and the credit spend behind them. | Free |
| [GET /v1/reference/{collection}](https://entitybind.com/api#get-v1-reference-collection) | Every enumerated value, generated from the code that consumes it. | Free |

#### What each one does, in detail

2 notes

Your own meter, and every enumerated value the API will ever return — generated from the constants that charge, throttle and decide, so a client can read them rather than hard-code them.

- `GET /v1/usage`: It carries no per-tenant precision and no NPV under any name, and says so in the payload with a NOT_MEASURED block: review items are created only on abstention, so reviewer verdicts label the unresolved band and nothing else. A precision figure derived from that band would be arithmetic over a population nobody observed.
- `GET /v1/reference/{collection}`: The collections listed below include enrichment availability and caller resale rights. /pricing and /limits are served from the same constants that charge and throttle. Reference reads require a tenant credential and cost no credits.

### Accounts, connectors and operations (15 endpoints)

| Endpoint | What it does | Cost |
| --- | --- | --- |
| [GET /v1/admin/tenants](https://entitybind.com/api#get-v1-admin-tenants) | List tenants. | Free |
| [POST /v1/admin/tenants](https://entitybind.com/api#post-v1-admin-tenants) | Create a tenant. | Free |
| [GET /v1/admin/keys](https://entitybind.com/api#get-v1-admin-keys) | List the credentials issued to a tenant. | Free |
| [POST /v1/admin/keys](https://entitybind.com/api#post-v1-admin-keys) | Mint a credential with exactly one scope. | Free |
| [POST /v1/admin/keys/{key_id}/rotate](https://entitybind.com/api#post-v1-admin-keys-key-id-rotate) | Rotate a credential. | Free |
| [DELETE /v1/admin/keys/{key_id}](https://entitybind.com/api#delete-v1-admin-keys-key-id) | Kill a credential immediately. | Free |
| [POST /v1/admin/oauth/install-tickets](https://entitybind.com/api#post-v1-admin-oauth-install-tickets) | Issue a one-time install ticket for a connector. | Free |
| [GET /v1/admin/oauth/grants](https://entitybind.com/api#get-v1-admin-oauth-grants) | List the connector grants a tenant has live. | Free |
| [DELETE /v1/admin/oauth/grants/{grant_id}](https://entitybind.com/api#delete-v1-admin-oauth-grants-grant-id) | Revoke one connector grant. | Free |
| [GET · POST /v1/oauth/authorize](https://entitybind.com/api#get-post-v1-oauth-authorize) | The connector authorisation endpoint. | Free |
| [POST /v1/oauth/token](https://entitybind.com/api#post-v1-oauth-token) | Exchange an authorisation code for an access token. | Free |
| [POST /v1/oauth/revoke](https://entitybind.com/api#post-v1-oauth-revoke) | Revoke an access token. | Free |
| [GET /healthz](https://entitybind.com/api#get-healthz) | Liveness. Static, and touches nothing. | Free |
| [GET /readyz](https://entitybind.com/api#get-readyz) | Readiness. One SELECT 1. | Free |
| [GET /metrics](https://entitybind.com/api#get-metrics) | Prometheus scrape. | Free |

#### What each one does, in detail

15 notes

The control plane under an admin-scoped credential that can reach no customer data by construction, the OAuth endpoints a connector install uses, and the three unversioned probes.

- `GET /v1/admin/tenants`: Admin scope. The control plane reaches no customer data at all, by construction rather than by policy.
- `POST /v1/admin/tenants`: One tenant is one isolation boundary: every read and write is scoped to it.
- `GET /v1/admin/keys`: Metadata only. Only the SHA-256 digest of a key is stored, and the raw value is printed once at creation and never again.
- `POST /v1/admin/keys`: Scopes are separate rather than layered: a resolve key cannot settle a review item, a review key cannot resolve, and an admin key can do neither. A mismatch is a 403 key_scope_forbidden raised before anything lands on request state.
- `POST /v1/admin/keys/{key_id}/rotate`: A window, not a swap: both values authenticate for the configured grace period so a deploy can overlap. A grace of zero is the leak path, and is a choice you make deliberately.
- `DELETE /v1/admin/keys/{key_id}`: No grace window. This is the one to reach for when a key has leaked.
- `POST /v1/admin/oauth/install-tickets`: The operator-side half of a connector install, so no shared secret travels by email.
- `GET /v1/admin/oauth/grants`: What is installed, on whose authority, and when it was granted.
- `DELETE /v1/admin/oauth/grants/{grant_id}`: Revocation is per grant, so removing one integration does not disturb the others.
- `GET · POST /v1/oauth/authorize`: Excluded from the OpenAPI schema on purpose: form-encoded, browser-facing, and answering with RFC 6749 error bodies rather than the standard envelope, so a client generated from the schema must never call it. The discovery document sits at /.well-known/oauth-authorization-server on the origin root. OAuth is resolve-only — review authority is never minted through a connector grant.
- `POST /v1/oauth/token`: Access tokens authenticate through the same seam as an API key, so everything downstream — tenancy, scopes, metering, rate-limit buckets — behaves identically.
- `POST /v1/oauth/revoke`: The token endpoints and this one are the only unauthenticated non-probe routes, and they are bucketed on the transport peer and the client id rather than on a tenant, because there is no verified tenant yet.
- `GET /healthz`: Outside /v1, because a probe that moves with the API version is not a probe.
- `GET /readyz`: Enough to know the database answers, and deliberately no more than that.
- `GET /metrics`: Requires a metrics-scoped credential. Deployment-wide rather than tenant-scoped, which is why it is its own scope rather than a permission on an admin key.

Scopes are separate, not layered: read · resolve · review · admin · metrics · operator — a mismatch is a 403 `key_scope_forbidden` raised before anything lands on request state

#### What each scope reaches

6 scopes

- `READ`: Reads of what your tenant already holds. It cannot resolve and cannot erase. One of the two scopes you can create in your Entity Bind workspace; RESOLVE is the other.
- `RESOLVE`: The data plane and the reads that hang off it. What an ingestion service holds.
- `REVIEW`: Verdicts, corrections and label reads. It cannot resolve, which is the point: the credential that approves a merge is not the credential that proposed it.
- `ADMIN`: Tenants and credentials. Reaches no customer data by construction.
- `METRICS`: The deployment-wide Prometheus scrape, and nothing else.
- `OPERATOR`: A local cross-tenant workbench scope, mintable only by the local command line. It is never issued over the API.

## What the other three calls look like

Ids, names and timestamps in these samples are illustrative. The field names, the nesting, the enum values and the thresholds are the shipped ones.

### A pairwise decision

Two records you already hold, compared without either of them landing in a catalogue. This is the call for adjudicating a match somebody else proposed — an incumbent feed you already run, a CRM merge suggestion, a spreadsheet a team argued over — and it stores nothing.

POST /v1/match · request:

```json
{
  "left": {
    "name": "Tecknuovo Ltd",
    "domain": "tecknuovo.com",
    "country": "GB"
  },
  "right": {
    "name": "TECKNUOVO LIMITED",
    "registration_number": "09876543",
    "jurisdiction": "GB",
    "domain": "tecknuovo.com"
  },
  "depth": "adjudication"
}
```

POST /v1/match · 200:

```json
{
  "decision": "match",
  "stage": "probabilistic",
  "probability": 0.9832,
  "match_confidence": 0.9755,
  "match_confidence_basis": "calibrated_probability",
  "depth_capped": false,
  "escalation_unavailable": false,
  "no_identifying_signal": false,
  "correlation_id": "req_0c4d8e2b6f1a47539ab8c0d1e2f3a4b5",
  "usage": {
    "credits_charged": 1,
    "credits_remaining": 4870,
    "deep_attempts_remaining": 2988,
    "plan": "growth",
    "enforced": false
  }
}
```

#### What to read in it

4 field notes

- `The fields that are absent`: There is no entity, no decision_id, no review_id and no candidates, because the endpoint persists nothing. Do not describe this call as auditable: there is no receipt to fetch afterwards. If you want the audit trail, resolve instead.
- `depth`: Set once for the pair. A depth on either nested record is ignored, because a pair compared at two different budgets is not one comparison.
- `An adjudicated pair`: Returns "stage": "llm_adjudication" and five credits rather than one. The stage that decided is the stage you pay for.
- `A pair with nothing to compare`: Returns "decision": "unresolved", "probability": null and "no_identifying_signal": true, at zero credits. It is never scored, because answering with the scorer's no-evidence prior would be a confident no_match about two records nobody looked at. Never render that unresolved as a no_match.

### An identifier crosswalk

Resolve on any one identifier, then read back every identifier system the company is known by, each one separately cited, plus the systems that were asked and came back empty, plus one hop of ownership. It is a resolve with a projection on top, so it is billed as a resolve and the projection is free.

POST /v1/crosswalk · request:

```json
{
  "lei": "213800QVXQAZLPZH1234",
  "max_hops": 1,
  "depth": "research",
  "enrich": true
}
```

#### The identifier map, drawn and in full

157 lines · 4 field notes

**Drawing: One company, five identifier schemes, every edge cited.** A resolved entity in the centre, with an edge to each identifier scheme it carries — LEI from GLEIF, company number from a register, CIK and ticker from EDGAR, and a domain — each entry carrying the fact that supplied it. Below them, the two ways a scheme can be absent: not_found, where a wired source had no value for this company, and not_configured, where no wired source supplies the scheme at all.

One company, one entity: `entity_id`. Every identifier beside it is an attribute of this entity.

| Scheme | Cited | What the edge holds |
| --- | --- | --- |
| `registration.lei` | GLEIF | 20 characters, ISO 17442, checksum verified before it is stored. |
| `registration.company_number` | Companies House | Qualified by jurisdiction — a bare number is a different company elsewhere. |
| `registration.cik` | SEC EDGAR | Ten characters, and the join to every filing that company has made. |
| `registration.tickers` | SEC EDGAR | Qualified by exchange, and only from an exchanges fact with the same source. |
| `digital.domain` | Register or observed | The identifier a CRM actually has, and the one most often wrong in it. |

Every edge carries its fact: One entry per scheme and value, each holding the fact that supplied it — so a wrong identifier is traceable to the source that asserted it rather than to us.

And the schemes that came back empty — two different answers:

- `not_found`: A wired source supplies this scheme and had no value here. A fact about the company.
- `not_configured`: No wired source supplies this scheme at all. A fact about us, not about the company.

The crosswalk is a derived attribute, stamped on every path that writes an entity. It cites the source url of the most authoritative contributing fact, and takes the minimum confidence across contributors rather than the best of them. CUSIP and SEDOL are absent by design: their terms prohibit maintaining a database of the identifiers, so the write path would refuse to store one.

POST /v1/crosswalk · 200 · abridged to the identifier map and one hop:

```json
{
  "record_id": "rec_ab19c73e5d0f42b18c6a4e5f70d92b83",
  "client_ref": null,
  "decision": "match",
  "stage": "deterministic",
  "confidence": 0.995,
  "match_confidence": 0.9784,
  "match_confidence_basis": "identifier_rule",
  "crosswalk": {
    "entity_id": "ent_8a5f3c11d94b47c2b0e6a1f7c3d29e40",
    "legal_name": "ACME WIDGETS LIMITED",
    "identifiers": [
      {
        "scheme": "lei",
        "value": "213800QVXQAZLPZH1234",
        "qualifier": null,
        "attribute": "registration.lei",
        "provenance": {
          "source_id": "gleif",
          "source_url": "https://api.gleif.org/api/v1/lei-records/213800QVXQAZLPZH1234",
          "retrieved_at": "2026-08-30T09:14:01Z",
          "source_as_of": "2026-08-29",
          "confidence": 0.98,
          "licence": "open",
          "checks": 6,
          "age_days": 2.12,
          "licence_permits_resale": true,
          "resellable": false
        }
      },
      {
        "scheme": "company_number",
        "value": "09876543",
        "qualifier": "GB",
        "attribute": "registration.company_number",
        "provenance": {
          "source_id": "companies_house",
          "source_url": "https://find-and-update.company-information.service.gov.uk/company/09876543",
          "retrieved_at": "2026-08-30T09:14:02Z",
          "source_as_of": "2026-08-29",
          "confidence": 0.98,
          "licence": "open",
          "checks": 7,
          "age_days": 2.12,
          "licence_permits_resale": true,
          "resellable": false
        }
      },
      {
        "scheme": "cik",
        "value": "0001640147",
        "qualifier": null,
        "attribute": "registration.cik",
        "provenance": {
          "source_id": "sec_edgar",
          "source_url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0001640147",
          "retrieved_at": "2026-08-28T17:41:09Z",
          "source_as_of": "2026-08-28",
          "confidence": 0.97,
          "licence": "open",
          "checks": 3,
          "age_days": 3.8,
          "licence_permits_resale": true,
          "resellable": false
        }
      },
      {
        "scheme": "ticker",
        "value": "ACMW",
        "qualifier": "XNAS",
        "attribute": "registration.tickers",
        "provenance": {
          "source_id": "sec_edgar",
          "source_url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0001640147",
          "retrieved_at": "2026-08-28T17:41:09Z",
          "source_as_of": "2026-08-28",
          "confidence": 0.97,
          "licence": "open",
          "checks": 3,
          "age_days": 3.8,
          "licence_permits_resale": true,
          "resellable": false
        }
      }
    ],
    "checked_and_empty": [
      {
        "scheme": "duns",
        "status": "not_configured",
        "sources": [],
        "detail": "No source in this deployment supplies duns."
      }
    ],
    "ownership": {
      "max_hops": 1,
      "as_of": "2026-09-01",
      "hops_attempted": 1,
      "hops_traversed": 1,
      "direct_parent": {
        "relationship": "direct_parent",
        "scheme": "lei",
        "value": "5493001KJTIIGC8Y1R12",
        "attribute": "hierarchy.direct_parent_lei",
        "provenance": {
          "source_id": "gleif",
          "source_url": "https://api.gleif.org/api/v1/lei-records/213800QVXQAZLPZH1234/direct-parent",
          "retrieved_at": "2026-08-30T09:14:01Z",
          "source_as_of": "2026-08-29",
          "confidence": 0.98,
          "licence": "open",
          "checks": 6,
          "age_days": 2.12,
          "licence_permits_resale": true,
          "resellable": false
        }
      },
      "ultimate_parent": null,
      "corporate_owners": [
        {
          "name": "ACME HOLDINGS LIMITED",
          "natures_of_control": ["ownership-of-shares-75-to-100-percent"],
          "notified_on": "2016-04-06",
          "ceased_on": null,
          "registration_number": "07654321",
          "country_registered": "United Kingdom",
          "legal_form": "Private Limited Company",
          "place_registered": "Companies House"
        }
      ],
      "not_traversed": [
        {
          "step": "beyond_one_hop",
          "reason": "Grandparents and the chain to an ultimate natural person are not walked. That needs recursive resolution with cycle detection and per-hop cost control, which this surface deliberately does not do.",
          "entity_id": null,
          "identifier": null,
          "depth": null
        }
      ]
    }
  },
  "candidates": [],
  "depth_capped": false,
  "entity_created": false,
  "replayed": false,
  "escalation_unavailable": false,
  "pipeline_version": "2026.09.0",
  "decision_id": "dec_bd402e19f7c34a58ae61c07d3f829b45",
  "review_id": null,
  "correlation_id": "req_7f1e6d5c4b3a29180fedcba987654321",
  "usage": {
    "credits_charged": 1,
    "credits_remaining": 4869,
    "deep_attempts_remaining": 2988,
    "plan": "growth",
    "enforced": false
  }
}
```

- `identifiers[].provenance`: One citation per identifier rather than one per response. The LEI comes from GLEIF and the company number from Companies House, and the payload says so per row. The example abbreviates provenance; provider, provider_record_id and licence_reference are also returned when supplied, alongside temporal and resale fields.
- `checked_and_empty`: The negative, which a reference-data load needs as much as the positive. "GLEIF has no LEI for them" is a finding; a missing key is a silence. status distinguishes not_found from not_configured, not_checked and source_unavailable — and there is no DUNS source in this deployment, so DUNS answers not_configured rather than nothing.
- `ownership.not_traversed`: What was not walked, and why, in words. Ownership is UK PSC register plus GLEIF level 2, defaults to one hop and is bounded. An incomplete path here is an absence of coverage, not an absence of owners, and this field is where it says so.
- `crosswalk: null`: Null exactly when decision is unresolved. An abstention hands back candidates instead, because publishing an identifier map for a company we could not identify is the one failure this surface must not have.

### A decision receipt

The decision_id from the resolve above, redeemed. Features, the weighted contributions that moved the score, the thresholds the call was judged under, the evidence that was cited, and a replay of the score from the stored features against today's code.

GET /v1/decisions/{decision_id} · request:

```bash
curl -sS "$ENTITYBIND_BASE_URL/v1/decisions/dec_5c7e0a2b8d1f42a39e6b4c5d7f8a9b01" \
  -H "Authorization: Bearer $ENTITYBIND_API_KEY"
```

#### What a decision id redeems

76 lines · 5 field notes

**Drawing: What a decision id redeems.** Every outcome carries a decision id. Reading it back returns the feature vector that was scored, the thresholds and pipeline version in force at the time, the evidence and the model and prompt versions where one adjudicated, and the score recomputed from the stored features so the probability can be checked rather than believed.

On every outcome: `decision_id`. One opaque id, returned on a match, an abstention and a refusal alike. `GET /v1/decisions/{id}` redeems:

- Features: The feature vector exactly as it was scored — or the named identifier rule, where a rule decided and nothing was scored at all.
- Thresholds: The upper and lower thresholds in force, the 0.90 floor, and the pipeline version — so a decision taken under a bar we have since raised is still readable as what it was.
- Evidence: The citations behind the decision, and the model and prompt version where one adjudicated. A prompt change is a version change, not a silent recalibration.
- Reproduced: The score, recomputed from the stored features and returned beside the one that was recorded. The two are meant to be equal, and you can see that they are.

The point of the last one: A number you can recompute is a different object from a number you are asked to accept.

Refusals are receipted on the same terms as matches: a candidate excluded by a deterministic non-match records its own decision, and so does every abstention. Receipts are queryable as a population as well as one at a time, filtered by pipeline version, threshold and a decision window — which is what makes “show me every decision taken under the threshold we have since raised” a question with an answer.

GET /v1/decisions/{decision_id} · 200:

```json
{
  "decision": {
    "decision_id": "dec_5c7e0a2b8d1f42a39e6b4c5d7f8a9b01",
    "left_record_id": "rec_1f4c2b9d6a7e4f0b8c3d5e6f7a8b9c0d",
    "right_ref": "ent_8a5f3c11d94b47c2b0e6a1f7c3d29e40",
    "decision": "match",
    "stage": "probabilistic",
    "probability": 0.999,
    "match_confidence": 0.9755,
    "match_confidence_basis": "calibrated_probability",
    "features": {
      "name_token_sort": 1.0,
      "name_jaro_winkler": 0.9847,
      "name_present": 1.0,
      "name_idf_overlap": 0.9123,
      "domain_exact": 1.0,
      "domain_present": 1.0,
      "domain_conflict": 0.0,
      "domain_name_affinity": 0.86,
      "regno_match": 0.0,
      "regno_conflict": 0.0,
      "lei_match": 0.0,
      "lei_conflict": 0.0,
      "country_match": 1.0,
      "country_conflict": 0.0,
      "postal_match": 1.0,
      "postal_present": 1.0,
      "address_present": 1.0,
      "address_similarity": 0.87,
      "address_conflict": 0.0
    },
    "top_features": [
      { "feature": "name_token_sort", "value": 1.0, "contribution": 4.2337 },
      { "feature": "postal_match", "value": 1.0, "contribution": 2.9338 },
      { "feature": "domain_exact", "value": 1.0, "contribution": 2.1036 },
      { "feature": "domain_name_affinity", "value": 0.86, "contribution": 1.832746 },
      { "feature": "name_jaro_winkler", "value": 0.9847, "contribution": 1.639526 }
    ],
    "identifier_rule": null,
    "thresholds": {
      "upper": 0.97,
      "lower": 0.35,
      "llm_confidence_floor": 0.5
    },
    "verdict": null,
    "evidence": [
      {
        "source_id": "companies_house",
        "source_kind": "registry",
        "licence": "open",
        "url": "https://find-and-update.company-information.service.gov.uk/search/companies?q=acme+widgets",
        "summary": "2 companies match this query (query: \"acme widgets\", unfiltered search).",
        "retrieved_at": "2026-09-01T10:02:55Z"
      }
    ],
    "pipeline_version": "2026.09.0",
    "reviewer_id": null,
    "reviewer_reason": null,
    "acting_key_id": null,
    "acting_surface": null,
    "decided_at": "2026-09-01T10:02:56Z",
    "replay": {
      "reproduced": true,
      "recomputed_probability": 0.999,
      "pipeline_version_matches": true
    }
  },
  "correlation_id": "req_3b8a1c6d5e4f47029876543210fedcba",
  "usage": {
    "credits_charged": 0,
    "credits_remaining": 4871,
    "deep_attempts_remaining": 2988,
    "plan": "growth",
    "enforced": false
  }
}
```

- `thresholds`: The bar this call was judged against, stored with the call rather than looked up today. A threshold change does not silently reinterpret last quarter's merges, and GET /v1/decisions?threshold_upper= is how you find the ones decided under the old one.
- `replay`: The stored features re-scored against today's code. reproduced: true means the number is not a claim about a run nobody can repeat. On a deterministic identifier merge it is null with pipeline_version_matches true — null because the scorer was never reached, not because the receipt is empty.
- `top_features`: The weighted contributions that actually moved the decision, in order. This is the part a reviewer reads, and the part a regulator asks for.
- `verdict`: Populated on an adjudicated decision with the model id, the prompt version, the rationale and the adjudicator's own confidence — which is an uncalibrated self-report and must never be thresholded on or compared with match_confidence.
- `acting_key_id · acting_surface`: Which credential decided, and over which surface: api, mcp, workbench or maintenance. This is what makes the four-eyes read possible at GET /v1/decisions?acting_key_id=.

The stage named in every response is the stage that decided, and the one you pay for. [The seven stages, and where each one stops](https://entitybind.com/how-it-works)

## What decides your row, and what happens when two identities conflict

Matching here is a cascade rather than a similarity score. Each stage sees only what the stage before it could not settle, and the row stops at the cheapest stage that can settle it.

- **Normalise, then retrieve a shortlist**: Legal forms, punctuation, transliteration and register-specific number shapes are normalised first, so that two spellings of one company can meet at all. Then [rarity-weighted blocking](https://entitybind.com/guides/blocking-and-candidate-generation) pulls a handful of candidates out of the catalogue and the registers, leaning on the rarest words in the name, rather than comparing your row against everything.
- **An agreeing identifier decides on its own**: An agreeing [company number](https://entitybind.com/identifiers/company-number) or [LEI](https://entitybind.com/identifiers/lei), checked for register shape rather than for string equality, settles the row at the deterministic stage. No model call, no egress, one credit. An identifier-rich file therefore never reaches a model, and a file of bare names is the one that pays for adjudication.
- **Otherwise the evidence is weighed, not the strings**: A [Fellegi and Sunter three-region policy](https://entitybind.com/guides/fellegi-sunter-explained) accepts, rejects, or escalates the borderline band to adjudication and then to cited research. Weights are fitted, and the out-of-fold analysis of that fit is published rather than assumed. A score over two strings is a retrieval signal here, never the decision.
- **Nothing under the floor comes back as a match**: A match below 0.90 confidence is unrepresentable. The record comes back as an abstention with a ranked shortlist instead.

The bar is 0.90, enforced in code as MIN_ACCEPTED_MATCH_CONFIDENCE. It is an acceptance contract, not calibration evidence: it says what we refuse to return, not how often an accepted match is right. Read it with the coverage it costs.

### Ambiguous and conflicting company identities

Two rows that look like the same company and are not is the expensive case, and it is the case a similarity score gets wrong quietly. Five rules decide it, and four of the five can end in a refusal rather than a merge.

- **A disagreeing strong identifier is a non-match, not a close call**: Two rows carrying different company numbers are answered as a non-match outright and never reach the merge guard. This is the rule that keeps a parent and its subsidiary, or two branches of a franchise group, apart when their names are nearly identical.
- **Several disagreeing weak signals block the merge**: One weak contradiction is noise. Two or more weak signals agreeing that these are different companies, a domain and a phone number together for instance, is evidence, and the merge is blocked with the attribute that disagreed named. An agreeing strong identifier short-circuits the weak check, which is how the company that relocated and changed every contact detail still resolves.
- **Two candidates the evidence cannot separate are withheld, not picked**: Two candidates within 0.02 of each other, or two carrying the same decisive identifier, and the merge is withheld even above the upper threshold. The record escalates carrying `indistinguishable_candidates` in its features, so the refusal is a fact on the row rather than a silence.
- **A conflict between two field values is settled by precedence, not by recency**: Once records are clustered, [one value per attribute has to win](https://entitybind.com/how-it-works#golden-record). A register and a named human reviewer rank first, then a document the company filed, then what you sent us, then our own cited research, then a paid vendor, then anything we inferred. Nothing inferred outranks its own inputs.
- **Where nothing separates them, the row comes back unresolved**: Back comes the ranked shortlist, a typed reason from a closed vocabulary, the one column of yours that would have settled it, and a review id a person can work. Zero credits, at every stage, including the ones that spent the most getting there.

Four moves here, seven stages underneath them. The cascade is ordered by what a stage costs rather than by how clever it is, so the cheapest thing that can settle your row is the thing that settles it, and a stage only runs on what the stage before it could not decide. [What each of the seven stages costs](https://entitybind.com/how-it-works)

## When it will not answer

[Abstention](https://entitybind.com/glossary/abstention) is an outcome here, not an error and not a null. An integration written without handling it drops the rows that most needed a person.

Three companies share the stem of this name and nothing in the record separates them, so nothing is bound. The response is not a null and not a low-confidence guess: it is a ranked shortlist, the question that would settle it, a review id and a receipt — and it is free.

POST /v1/resolve · request:

```json
{
  "name": "Apex Roofing",
  "country": "GB",
  "depth": "research",
  "client_ref": "crm-8890"
}
```

#### The whole unresolved response

70 lines · 6 field notes

POST /v1/resolve · 200:

```json
{
  "record_id": "rec_7b2e5d3c1a9f48e0b6c2d4f5a7e8b901",
  "client_ref": "crm-8890",
  "decision": "unresolved",
  "stage": "probabilistic",
  "confidence": 0.8123,
  "match_confidence": 0.0,
  "match_confidence_basis": "no_binding",
  "entity": null,
  "candidates_considered": 3,
  "candidates": [
    {
      "entity_id": "ent_c41b7f9028a34d6e9f15b8c2d7e30a64",
      "external": false,
      "legal_name": "APEX ROOFING LIMITED",
      "domain": "apexroofing.co.uk",
      "legal_status": "active",
      "probability": 0.8123,
      "adjudicator_decision": null,
      "adjudicator_confidence": null
    },
    {
      "entity_id": null,
      "external": true,
      "legal_name": "APEX ROOFING (MIDLANDS) LTD",
      "domain": "apexroofingmidlands.co.uk",
      "legal_status": "active",
      "probability": 0.7994,
      "adjudicator_decision": null,
      "adjudicator_confidence": null
    },
    {
      "entity_id": "ent_2d80e6a4f1b74c39a05e7c8b1d2f3a45",
      "external": false,
      "legal_name": "APEX ROOFING SERVICES LTD",
      "domain": "apex-roofing-services.com",
      "legal_status": "dissolved",
      "probability": 0.612,
      "adjudicator_decision": null,
      "adjudicator_confidence": null
    }
  ],
  "clarification": {
    "attribute": "domain",
    "question": "Which website belongs to the company you mean?",
    "options": [
      "apexroofing.co.uk",
      "apexroofingmidlands.co.uk",
      "apex-roofing-services.com"
    ],
    "answer_by": "resubmit the record with your answer in `context`"
  },
  "more_input_needed": true,
  "depth_capped": false,
  "entity_created": false,
  "replayed": false,
  "escalation_unavailable": false,
  "no_identifying_signal": false,
  "pipeline_version": "2026.09.0",
  "decision_id": "dec_a17c93f5be2044d8b6e0c1a2d3f45b67",
  "review_id": "rev_4e8b0d17c9a24f35b1e6072a8c9d3f10",
  "correlation_id": "req_5a2c8e1f7b394d06a8c1e2f3b4d5a6c7",
  "usage": {
    "credits_charged": 0,
    "credits_remaining": 4871,
    "deep_attempts_remaining": 2988,
    "plan": "growth",
    "enforced": false
  }
}
```

- `clarification`: The column that would have decided it, the question in plain language, the options, and how to answer. This is the field that turns an abstention into work somebody can actually do.
- `candidates[].entity_id · external`: A live registry candidate the cascade never stored comes back with entity_id null and external true. An id that would dangle is never handed to a caller, and the guard runs on every row of the shortlist rather than on the winner.
- `match_confidence: 0.0`: Not a 0% match score. match_confidence is the probability that this record is bound to the right company, and this record is bound to nothing. The per-pair belief is in candidates[].probability, where it belongs.
- `adjudicator_decision · adjudicator_confidence`: Null means "not asked", never "asked and unconvinced". The cascade stopped before the paid stages because nothing separated the shortlist.
- `review_id`: The queue item this created. Work it at GET /v1/review-queue/{id} and settle it with a named reviewer; the verdict is recorded against that reviewer and excluded from the automated population every accuracy figure is measured over.
- `usage.credits_charged: 0`: An abstention is free at every stage, including the ones that spent the most getting there. You pay for answers, not attempts.

### False binds: 0 wrong bindings, on every shape of input we recorded

Measured on 317 live GB companies recorded 2026-08-29, from a company number down to a name cut off by a field limit. Zero wrong bindings were observed across the historical six-shape recording and the separate domain fixture. Observed correctness was 1.0000 on accepted outcomes; this does not guarantee an error-free new workload.

Historical results on an internal UK fixture with a preloaded catalogue. The September 5 replay has no full-prompt-verified calls and cannot establish current calibration. No independent customer holdout or global coverage claim.

[Every figure, its population and what it does not show →](https://entitybind.com/accuracy)

### The three ways to handle one

#### 01 Answer the question

Where `more_input_needed` is true, `clarification` names the column that would settle it. Resubmit with the answer in `context`; these rows usually come back resolved.

#### 02 Queue it for a person

The `review_id` is already [a queue item](https://entitybind.com/glossary/review-queue). Work it at `GET /v1/review-queue` and settle it with a named reviewer; the verdict goes through the merge guard, is recorded against that reviewer, and is excluded from the automated population every accuracy figure is measured over.

#### 03 Leave it unbound, on purpose

Nothing obliges you to resolve every row. The record is enumerable at `GET /v1/records/unbound`, and its `decision_id` still redeems the full evidence trail.

#### The flags on every response

6 booleans

- `replayed`: This exact record has been submitted before and the stored binding was returned. Zero credits, no new decision.
- `depth_capped`: The cascade stopped at a ceiling — yours or your plan's — where it would otherwise have escalated. Where that changed the outcome, the outcome is an abstention rather than a cheaper guess.
- `entity_created`: An entity was created in this tenant's catalogue. It may be a confidently matched external-registry identity or a weaker no-candidate mint. Read match_confidence_basis and verify the returned identifiers; creation alone does not prove a correct match.
- `escalation_unavailable`: A deeper stage was warranted and could not run. Read it as a temporary limit on automation, not as evidence about the record.
- `no_identifying_signal`: Nothing in the record survived normalisation as something comparable. It is checked first, and it is free — there was no question to answer.
- `more_input_needed`: The abstention is answerable: clarification names the column that would settle it. This is the flag worth routing on, because these rows come back resolved once the column arrives.

The abstention shape is easier to judge against your own rows than against ours: what resolved, what your existing matches got wrong, and what we would not decide. Upload your CSV in your Free workspace to inspect its results; contact us for an assisted audit of existing matches. We write nothing to your systems. [Start free →](https://app.entitybind.com/app?signup=1)

## How it plugs in, which columns go where, and who reviews the rest

One HTTPS call per record or 500 records per call, a bearer token, and your own key on every row that comes back. Nothing is written to your systems at any point.

- **One record, or five hundred**: `POST /v1/resolve` for a row. `POST /v1/batch` for up to 500 records in one request, order-independent within the batch, with an `Idempotency-Key` header so a retried batch is not a second batch. Batch is not a queue: it runs the records and answers.
- **Your key travels with the row**: `client_ref` takes up to 200 characters and comes back verbatim on every row, so results join to your file on your own identifier rather than on an id of ours. `GET /v1/records/by-ref/{client_ref}` reads the mapping back later, which is what makes a second pass over the same file cheap.
- **A typo in a field name fails on the first call**: Unknown fields are rejected rather than silently ignored, and a request carrying no identifying field at all is a 422 before anything runs. The failure you are being protected from is the column that quietly never arrived, which is the one that costs a week.
- **You can cap what leaves the deployment, per record**: `depth` is a per-record argument even inside a batch, and `depth=probabilistic` forbids egress entirely: no registry candidate pass, no evidence lookup, no adjudication. A capped record still reaches the review queue, so the cap costs automation rather than safety.

### Field mappings

A CRM, MDM or supplier export on the left, and the request field it belongs in on the right. The names on the left are only illustrative; the ones on the right are exact, and a field this table did not name is rejected rather than ignored.

| Your column | Request field | What it does for the match |
| --- | --- | --- |
| `account_id` | `client_ref` | Your key. Echoed back verbatim on every row, up to 200 characters. |
| `account_name` | `name` | The one field almost every export has. On its own it is usually an abstention rather than a match. |
| `website` | `domain` | The cheapest thing you can add to a name. Domain agreement is a decisive digital signal. |
| `company_no` | `registration_number` | Checked for register shape rather than for string equality, and it decides the row on its own. |
| `lei` | `lei` | The other identifier that decides on its own, where a counterparty file carries one. |
| `vat_no` | `vat_number` | Identifying, and worth mapping where a finance or supplier export has it. |
| `billing_country` | `country` | Qualifying, not identifying. It narrows the shortlist and cannot decide a row. |
| `billing_postcode` | `address.postal_code` | Qualifying. The address is structured, so the postcode and the city go inside it. |
| `industry_notes` | `context` | Up to 1,000 characters of plain language, read as caller-supplied evidence rather than as an identity. |

Send the identifying columns and the key you want back, and nothing else. Two of the rows above are qualifying rather than identifying, and a request carrying only those two is rejected before anything runs.

### Review of uncertain matches

The rows the evidence would not carry do not disappear and they are not yours to find: each one is an item on a queue, with the shortlist and the column that would have settled it already attached. Four rules govern who may settle one.

- **The queue is an endpoint, not a screen we own**: `GET /v1/review-queue` lists the abstentions, sortable by confidence and filterable by a minimum. `POST /v1/review-queue/{review_id}/decision` settles one; `POST /v1/review-queue/decisions` settles up to 200 in a call. It is a human-paced surface and it is rate-limited as one. Every read on it is free.
- **Approving a merge needs a different credential**: A review write requires a distinct review credential: a resolve-scoped key can poll the queue and cannot approve anything. A verdict is refused unless it names an accountable reviewer, and the credential and surface that applied it are recorded server-side rather than accepted from the caller.
- **Rejecting one candidate does not approve another**: An alternative binding has to clear the confidence floor and the shared margin guard independently before it is applied, and it persists its own earned stage and confidence. A weak or near-tied alternative leaves the verdict unapplied, so a human no-match cannot launder an unreviewed match into certainty.
- **A reviewer's verdict leaves the automated population**: Every reviewer verdict is stamped as human review and excluded from the automated-merge population every accuracy figure on this site is measured over. That is what keeps the published figures a measurement of the engine rather than of the engine and an analyst together.

## What a call costs, in the call

Every response carries a usage block. The charge is on the response, so you can check it per call against your own ledger.

usage · on every response:

```json
"usage": {
  "credits_charged": 1,
  "credits_remaining": 4871,
  "deep_attempts_remaining": 2988,
  "plan": "growth",
  "enforced": false
}
```

#### What each usage field means

5 fields · where the block is carried

- `credits_charged` (integer): What this call cost. Zero on an abstention, a replay and an input with no identifying signal.
- `credits_remaining` (integer): The balance after this call, so a client can back off before it runs out rather than after.
- `deep_attempts_remaining` (integer): Deep attempts are metered separately from credits, because they are the ones with a model provider at the other end.
- `plan` (string): The plan the rate limits and depth ceiling were applied from.
- `enforced` (boolean): False by default: usage metering runs in shadow mode, recording real figures without enforcing the usage quota. This flag describes usage enforcement, not subscription payment; paid subscriptions are purchased separately in workspace Billing.

The usage block is on every response — resolve, match, batch, crosswalk, entity reads, receipts, review and usage itself — so charge-on-success is something you verify per call rather than something you read on a pricing page. On the MCP surface only the two billable tools carry it, and it is the only place an agent can learn what a call cost, because there are no response headers to read there.

### Paid subscriptions and usage are separate

Paid monthly subscriptions are purchased through Stripe in workspace Billing. Usage creates no automatic usage or overage charge. `enforced` is `false` by default: the ledger records real figures and nothing is deducted. The plans and the credit rule are defined in code — the same constants the meter reads and `GET /v1/reference/pricing` serves. The 5× multiple on a deep decision is a price, not a measured cost ratio. Shadow-mode usage metering does not make a paid subscription free.

### The credit rule, in five lines

- 1 credit(s): A decision reached by the deterministic or probabilistic stage. CPU only, no egress.
- 5 credit(s): A decision that needed LLM adjudication or agentic research.
- 0 credit(s): An abstention, at any stage, including the ones that spent the most getting there.
- 0 credit(s): A byte-identical replay. Per tenant, per exact content, and with no expiry.
- 0 credit(s): An input that normalised to no identifying signal. There was nothing to answer.

Read by the meter and by GET /v1/reference/pricing from one constant set · the no-signal case is checked first, whatever else the row says

### Two cost controls, and what they trade

#### A cap costs automation, not precision

Capping does not lower precision. A capped record abstains, with depth_capped: true, where an uncapped one would have escalated — so what a cap trades away is automation, not correctness. That is the whole reason a cap is safe to set.

#### The three depths, and what each one reaches

3 rungs · egress and cost

depth is a price and latency ceiling, and it is set per record — including per record inside a batch, so a bulk file and the twenty rows you actually care about can run at different budgets in one request. probabilistic is a hard no-egress setting enforced in code at both egress points rather than a preference: it gates the registry candidate pass and the new-entity enrichment gather, not only the model call. enrich=false is the orthogonal control, and turns the registries off at any depth for a caller whose contract forbids the lookup.

| Depth | What runs | Egress | Cost |
| --- | --- | --- | --- |
| probabilistic | Normalisation, narrowing millions of rows to a handful worth comparing (blocking), deterministic identifier rules and probabilistic scoring. Nothing further. | No egress of any kind. No model provider and no registry. | One credit at most. |
| adjudication | The above, plus LLM adjudication of the borderline band. Stops before agentic research. | A model provider, and the registry candidate pass. | One credit, or five if the adjudicator decided it. |
| research | The whole cascade, including agentic registry and web research on the ambiguous tail. The default. | A model provider, the registries and the live web, all cited. | One credit, or five if a deep stage decided it. |

The plan sets credits, deep attempts and the rate-limit multiplier, and clamps a depth you asked for down to one you are entitled to. [Plans, allowances and the licence terms](https://entitybind.com/pricing)

## The error model

One envelope on every failure, a stable machine code from a published vocabulary, and a correlation id on the successes as well.

429 · the envelope, on every failure:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "rate limit exceeded",
    "correlation_id": "req_9d3f7c1a4b2e48c5b6a7d8e9f0a1b2c3"
  }
}
```

#### What the codes will not tell you

4 notes

##### A wrong-tenant id is a 404, never a 403

A 403 confirms that the id exists, which turns every read into an enumeration oracle: you could walk our id space from the outside and learn which of your competitors are customers. So an entity that belongs to another tenant is indistinguishable from one that does not exist.

##### deadline_exceeded is a 504, not a 503

Nothing upstream is down. Our budget for that request ran out, so the caller is being told to ask for less — a lower depth, or a longer allowance — rather than that a dependency failed. Retrying the identical request unchanged will spend the same budget again.

##### 5xx messages are canned

Internal detail never reaches a caller. Every 5xx message is replaced with one of four fixed strings, so a driver error cannot leak SQL, bound parameters or an internal hostname. The correlation_id is what to send us: it is on every response, successful or not.

##### Codes are stable, messages are not

Branch on code. The human message is written for a human reading a log and may be reworded; the code is a published vocabulary you can fetch from GET /v1/reference/error-codes and diff between releases.

### All 33 codes

Branch on the code, not the message. The same vocabulary is fetchable at `GET /v1/reference/error-codes`, from the map that sets the status.

| Code | HTTP | What it means |
| --- | --- | --- |
| [unauthorized](https://entitybind.com/api#error-unauthorized) | 401 | No credential, or one that does not authenticate. |
| [key_scope_forbidden](https://entitybind.com/api#error-key-scope-forbidden) | 403 | A real credential with the wrong scope. Raised before anything lands on request state. |
| [plan_not_permitted](https://entitybind.com/api#error-plan-not-permitted) | 403 | The plan does not carry this capability. |
| [forbidden](https://entitybind.com/api#error-forbidden) | 403 | The status-derived default where no typed code applies. |
| [entity_not_found](https://entitybind.com/api#error-entity-not-found) | 404 | No such entity for this tenant. Also what a wrong-tenant id returns. |
| [record_not_found](https://entitybind.com/api#error-record-not-found) | 404 | No such record for this tenant. |
| [decision_not_found](https://entitybind.com/api#error-decision-not-found) | 404 | No such receipt for this tenant. |
| [review_item_not_found](https://entitybind.com/api#error-review-item-not-found) | 404 | No such review item for this tenant. |
| [tenant_not_found](https://entitybind.com/api#error-tenant-not-found) | 404 | Admin scope only. |
| [api_key_not_found](https://entitybind.com/api#error-api-key-not-found) | 404 | Admin scope only. |
| [oauth_grant_not_found](https://entitybind.com/api#error-oauth-grant-not-found) | 404 | Admin scope only. |
| [not_found](https://entitybind.com/api#error-not-found) | 404 | The status-derived default. |
| [method_not_allowed](https://entitybind.com/api#error-method-not-allowed) | 405 | Wrong verb for a real path. |
| [erased_reference](https://entitybind.com/api#error-erased-reference) | 409 | The thing you named has been erased, and its absence is deliberate. |
| [entity_revision_conflict](https://entitybind.com/api#error-entity-revision-conflict) | 409 | The entity moved under you. Re-read and retry. |
| [idempotency_conflict](https://entitybind.com/api#error-idempotency-conflict) | 409 | The same Idempotency-Key with different content. Permanent, not retryable. |
| [review_already_resolved](https://entitybind.com/api#error-review-already-resolved) | 409 | A second verdict on a settled item, raised before anything is written. |
| [entity_detach_unsafe](https://entitybind.com/api#error-entity-detach-unsafe) | 409 | One of the six detach preconditions does not hold. Nothing was written. |
| [tenant_already_exists](https://entitybind.com/api#error-tenant-already-exists) | 409 | Admin scope only. |
| [payload_too_large](https://entitybind.com/api#error-payload-too-large) | 413 | Over the request byte ceiling. Rejected before parsing. |
| [invalid_record](https://entitybind.com/api#error-invalid-record) | 422 | The record cannot be admitted — no identifying field, an unknown field, or a value out of range. |
| [reviewer_required](https://entitybind.com/api#error-reviewer-required) | 422 | A verdict arrived without naming its reviewer. |
| [rate_limited](https://entitybind.com/api#error-rate-limited) | 429 | The bucket is empty, or the deep-admission gate shed load. Retry-After is set. |
| [internal_error](https://entitybind.com/api#error-internal-error) | 500 | Unhandled. The message is canned text; the correlation id is the way to ask us about it. |
| [licence_violation](https://entitybind.com/api#error-licence-violation) | 500 | The write path refused to store a fact the licence does not permit storing. A bug, and it fails closed. |
| [tenant_violation](https://entitybind.com/api#error-tenant-violation) | 500 | A cross-tenant access was attempted internally and refused. |
| [storage_error](https://entitybind.com/api#error-storage-error) | 500 | The persistence layer failed. No driver detail reaches the caller. |
| [http_error](https://entitybind.com/api#error-http-error) | 500 | The status-derived default. |
| [adjudication_failed](https://entitybind.com/api#error-adjudication-failed) | 502 | The adjudicator was reached and could not answer. |
| [source_unavailable](https://entitybind.com/api#error-source-unavailable) | 503 | An upstream register or source is down. |
| [blocking_index_unavailable](https://entitybind.com/api#error-blocking-index-unavailable) | 503 | Candidate retrieval is unavailable, so nothing would be comparable. |
| [review_label_pending](https://entitybind.com/api#error-review-label-pending) | 503 | A calibration key is waiting on labels. It fails closed rather than guessing. |
| [deadline_exceeded](https://entitybind.com/api#error-deadline-exceeded) | 504 | Our budget for this request ran out. Ask for less depth, or allow longer. |

## Limits, idempotency and batch

Deployment defaults, served by GET /v1/reference/limits from the settings that enforce them. Read them from there; a hard-coded copy goes stale the day a ceiling moves.

### Rate limits

| Class | Per minute | Applies to |
| --- | --- | --- |
| READ | 1,000 / min | Entity, record, receipt, label and reference reads. |
| MATCH | 300 / min | POST /v1/match. |
| RESOLVE | 300 / min | POST /v1/resolve and POST /v1/crosswalk. |
| BATCH | 30 / min | POST /v1/batch, /v1/entities/export and /v1/entities/reconcile. |
| REVIEW | 120 / min | The review queue and the detach correction. |
| ADMIN | 60 / min | The control plane. |
| OAUTH | 30 / min | The connector endpoints, bucketed on the transport peer and client id. |

#### How the buckets actually behave

4 notes · per tenant, per replica

##### Per tenant, not per key

A customer with three keys has bought one allowance. Keying on the credential would let anyone multiply a published limit by minting a second key, which would make the published number meaningless.

##### Headers on success, not only on 429

RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset come back on every response, so a client can slow down before it is refused. Retry-After is added only when a request is actually refused.

##### Token buckets, so burst is real

You can spend up to a minute's quota at once and it refills continuously. A plan multiplier scales every class: 0.2x on Free, 1.0x on Starter and Growth, 3.0x on Scale.

##### Read these from the API, do not hard-code them

Bucket state is in-memory by default, so a published rate limit is accurate at one replica unless a shared limiter store is configured: across a horizontally scaled deployment the effective limit is the published one multiplied by the replica count. That is the reason to read GET /v1/reference/limits at start-up rather than to copy a number off this page.

### Published ceilings

| Limit | Value | Note |
| --- | --- | --- |
| Batch size | 500 records | The deployment default. The schema's absolute ceiling is 5,000, and a larger file is more requests rather than a bigger one. |
| Page size | 200 | On every paged read. |
| Request body | 2,000,000 bytes | Rejected with a 413 before parsing, so an oversized body costs neither of us the parse. |
| Request deadline | 300 seconds | End to end for one resolve or match, or for one whole batch. Nested work cannot reset it. An overrun is a 504 deadline_exceeded. |
| Deep comparisons per batch | 20 | The ambiguous pairs inside one batch that may be adjudicated. Beyond it, the rest abstain rather than the batch running unboundedly. |
| Concurrent deep resolutions | 20 per replica | The deep-admission gate. A request waits 5 seconds for a slot and is then refused with a 429 and Retry-After 30, rather than queueing invisibly behind a 180-second research run. |
| Bulk reviewer verdicts | 200 per call | Per-item results, not a transaction. |
| Idempotency-Key | 200 characters | Header, and /v1/batch only. |

#### Idempotency, in two different shapes

a header on batch, a fingerprint on resolve

##### Idempotency-Key, on /v1/batch only

A header, up to 200 characters. The request hash covers the record content including each record_id, each record's depth and enrich, and the tenant. Identical concurrent callers wait for and replay the sealed winner rather than both running; the same key with different content is a permanent 409 idempotency_conflict. Batch row ids are derived deterministically from the tenant, the key and the row index, so a replay mints exactly the same ids.

##### A content fingerprint, on /v1/resolve, with no header at all

The SHA-256 of the submitted record excluding its server-minted id, computed before the cascade runs. A byte-identical resubmission inherits the stored binding, comes back flagged replayed: true, and is billed at zero. There is no TTL: you pay once per distinct record, ever.

- Per tenant. There is no cross-customer discount, and nobody else's submission makes yours free.
- Per exact content. One corrected postcode is a different fingerprint and a fresh, chargeable resolution.
- It is the binding that replays, not the record you were sent last time. The golden record returned is that entity as it stands today, so it may have gained facts since.

#### Batch semantics

5 rules · it is not a queue

##### Order-independent within the batch

Two phases. Every record resolves through the unchanged cascade against the store as it stood when the batch began, with writes staged so no record sees another's output. Then the intra-batch edges are collected, connected components are computed, and one entity is assembled per component. Shuffle the file and you get the same answer.

##### One ledger row per outcome

Not one summed row for the request. Every row's cost is attributable to the row that caused it, which is what makes a batch invoice checkable rather than a total you have to accept.

##### It reports the pairs it withheld

The response carries withheld_pairs and blocked_pairs alongside outcomes: comparisons between your own rows that the cascade would not make. An outcome says where a record landed, and neither of those landed anywhere — which is exactly the list a data owner wants out of a deduplication run.

##### depth is per record, and the plan clamps it

depth_ceiling on the response echoes the deepest stage any record in that batch was permitted to reach after the plan ceiling was applied. depth_capped on an outcome says a record stopped at a cap; it does not say whose cap it was.

##### It is not a queue

Batches run one at a time, inside the same 300-second request deadline as everything else. A 300,000-row file is an overnight window to plan rather than a throughput number to quote.

## You never have to guess an enum

12 collections, each generated from the code that consumes it. A client holding a hard-coded list of stages, [licences](https://entitybind.com/glossary/licence-tier) or error codes is holding a copy that can go stale — and the day it does, it fails on the row that mattered.

#### The collections, and what each returns

12 paths

- `/v1/reference/jurisdictions`: Every jurisdiction code the resolver recognises.
- `/v1/reference/registries`: The registers behind the catalogue, per jurisdiction.
- `/v1/reference/sources`: Each connector's capabilities, licence, readiness and passively observed health — so a client can tell a source that is empty from one that is down.
- `/v1/reference/enrichment-fields`: Field contracts, supplier capabilities, configured availability, storage and caller resale rights, including explicit unsupported requirements. Availability is not per-company coverage or a freshness guarantee. Free to read with your tenant key; also exposed as MCP get_enrichment_fields.
- `/v1/reference/identifiers`: The identifier schemes the crosswalk can return.
- `/v1/reference/stages`: The cascade stages a decision can be attributed to.
- `/v1/reference/decisions`: The decision vocabulary.
- `/v1/reference/depths`: The depth ladder, and what each rung is permitted to reach.
- `/v1/reference/licences`: The licence tiers, and what each permits.
- `/v1/reference/error-codes`: The whole typed error vocabulary with its HTTP statuses.
- `/v1/reference/pricing`: The credit rule and the plans, from the constants that charge.
- `/v1/reference/limits`: The rate limits and ceilings, from the settings that enforce them.

Every one of these is generated from the code that consumes it rather than maintained beside it. /pricing is served from the same constant set the meter reads, and /limits from the same settings the throttle reads, so the value your client holds and the value the API applies cannot drift into disagreement. Read a vocabulary rather than copying one: a hard-coded list is correct right up until a release adds a value, and then it is a branch nobody wrote.

#### The vocabularies a client will branch on

9 fields

##### `decision`

`match` · `new_entity` · `no_match` · `unresolved`

new_entity is explicitly not a match. Nothing matched, so a new entity was minted.

##### `stage`

`deterministic` · `probabilistic` · `llm_adjudication` · `agentic_research` · `human_review`

The stage that decided, which is also the stage you are billed for.

##### `depth`

`probabilistic` · `adjudication` · `research`

##### `match_confidence_basis`

`calibrated_probability` · `identifier_rule` · `adjudicated` · `no_candidate_matched` · `no_candidate_retrieved` · `inherited_from_binding` · `human_review` · `no_binding`

Each value is a different population, and each carries its own measured bound. Read the basis before you compare two confidences: a no_candidate_retrieved mint is the weakest answer the API gives, and it fails an 0.85 rule on purpose.

##### `licence`

`open` · `licensed` · `pass_through`

##### `source_kind`

`registry` · `filing` · `web` · `commercial` · `customer` · `reviewer` · `inferred`

##### `crosswalk scheme`

`lei` · `company_number` · `jurisdiction` · `cik` · `ticker` · `duns`

duns is in the vocabulary and has no source in this deployment, so it answers not_configured rather than being silently absent.

##### `identifier gap status`

`not_found` · `not_configured` · `not_checked` · `source_unavailable`

Four different silences, told apart. Only the first is a fact about the company.

##### `acting_surface`

`api` · `mcp` · `workbench` · `maintenance`

An agent reaches the same pipeline over the Model Context Protocol — eleven tools, same key, same tenancy, same credit rule — and reads these same collections. [The MCP tools](https://entitybind.com/mcp)

## How match quality is measured, and what is never kept

Two numbers describe a matcher and neither is readable on its own: the share of asserted matches that were right, and the share of real matches it found. A third decides how to read both, which is how much of the file it was willing to decide at all.

### Precision, and the rate that makes it readable

[Precision](https://entitybind.com/glossary/precision) is the share of the matches a system asserted that were correct. It is the number a merge-safety buyer holds a hard line on, because a precision failure is a wrong merge: two different companies welded into one entity, and everything computed over that entity now wrong in a way that is expensive to find and expensive to unwind.

It is also the easiest figure in this field to make meaningless, and no dishonesty is required: a system that decides almost nothing and abstains on the rest reports perfect precision for ever. So precision is never published here without the automation rate beside it, and never as a point estimate where a bound is available.

On the continuous-integration gate of 2026-09-05, automated merge precision was 1.0000 on 357/357 merges at an automation rate of 63.10%.

742 of 1,176 pairs were decided automatically in the September 5 saved gate report. This measures automation, separately from accepted-merge precision.

The published figure is the [Wilson lower bound](https://entitybind.com/glossary/wilson-interval) rather than that point estimate: 98.94%. That is the conservative reading of a run with no errors in it. 357 error-free automated merges support a [95% lower bound](https://entitybind.com/accuracy) there, and proving a 99% lower bound would need at least 381 consecutive clean merges, so this run is 24 short of one. A single error resets that arithmetic to something harder, which is the reason the bound is published and the point estimate is not.

### Recall, and what an abstention costs

[Recall](https://entitybind.com/glossary/recall) is the share of the true matches present in your data that the system actually found, and it is the thing an abstention costs. A recall failure is a missed merge: the same company appears twice, the duplicate persists, and somebody eventually notices. That is a cost rather than a catastrophe, and a later pass with better evidence recovers it.

The asymmetry is deliberate. Recall is spent here to protect precision: a record whose evidence will not clear the floor comes back unresolved rather than bound. The coverage that policy leaves is [published per input shape](https://entitybind.com/accuracy), which is the honest way to answer the question, because a file of truncated names and a file of company numbers are not the same workload.

Recall is also [harder to measure than precision](https://entitybind.com/guides/how-to-measure-entity-resolution-accuracy), because it needs a population in which the true matches are known exhaustively. A fixture that quietly excluded the hard cases reports a better number than one built to include every same-named rival.

### Why a pairwise number is never published alone

A pairwise figure is never published here without a [cluster figure](https://entitybind.com/glossary/cluster-metrics) beside it, and that is a rule rather than a courtesy. Clusters are built from pairwise decisions, so one false edge between two clean groups makes a very large number of record pairs wrong from a single wrong decision, and a resolver can also score well pairwise while fragmenting one company across three clusters.

The same gate reports a [B-cubed](https://entitybind.com/glossary/b-cubed) cluster F1 of 0.7584.

B-cubed cluster F1 on the gate's own 705-record fixture, held as a regression ratchet against a 0.75 floor rather than offered as a headline. It reads far below the live recording's 0.9646 because an unresolved record is a predicted singleton and all 205 name-only arrivals on this fixture abstain by construction, splitting the families they belong to. The two cluster figures come from different instruments over different fixtures and are never averaged.

F1 is not offered as the headline on either instrument. It is the harmonic mean of two quantities whose costs differ by orders of magnitude in this domain: a wrong merge in a payments master can send money to the wrong company, a missed merge costs a duplicate record, and F1 weights the two equally.

Those are two different instruments over two different fixtures and they are reported side by side, never averaged. [Every figure on this site](https://entitybind.com/accuracy) is published with the population it was measured over, the date it was taken and the caveat that makes it true.

### Privacy in matching

Matching here is a question about a company, and the posture follows from that rather than from a promise. Scope is companies and organisations, what the pipeline may keep is an allow-list rather than a filter, and every read is bound to one tenant. Each claim below is rendered with the caveat that travels with it.

- Research findings are checked against an attribute allow-list at the boundary, so anything outside it is dropped rather than persisted, and the research agent's own instructions put private individuals out of scope explicitly. *The limit*: An instruction to the agent is a request. The allow-list is what actually happens to a finding, which is why both exist and why the allow-list is the one worth checking.
- Logs are structured JSON carrying correlation ids and record, entity and decision identifiers, and never names, emails, phone numbers or addresses. Error responses are generic envelopes keyed by stable machine codes.
- Every persistence method takes the tenant as a required keyword argument with no default, so a new call site that forgets whose data it is touching raises rather than quietly reading a shared tenant. *The limit*: There is no default because a default would have meant 'the shared tenant'. The failure mode is a TypeError at the call site rather than a leak in production.
- The cache that recognises a repeat submission is keyed on the content fingerprint and the tenant together, as a composite primary key rather than as a filter, so byte-identical content submitted by two customers resolves to two separate entities. *The limit*: A filter would not have been enough. The insert itself would have collided, and one customer would have been served another's entity as a cache hit.
- Isolation is enforced in the application, not in the database. One database, one schema, one runtime role, no row-level security. *The limit*: A SQL-injection defect or a compromised runtime role would cross every tenant at once. Parameterised queries through the ORM, and a database role that can change rows but not the schema, are defence in depth against that rather than a second enforcement point.
- Four retention windows, each its own maintenance command: decisions, submitted records and the usage ledger default to 2,555 days, and open review items to 365. Every window is a deployment setting a buyer can shorten. *The limit*: Nothing purges itself. All four are scheduled jobs an operator has to run, and an unscheduled deployment retains everything for ever.
- 2,555 days is seven years, chosen to clear the money laundering regulations' five-year duty to retain the evidence behind a customer due-diligence decision. *The limit*: Below roughly 1,825 days a know-your-business customer loses the thing they are legally required to be able to produce. Shortening the window is your choice rather than ours, and it has that consequence.

Entity Bind resolves companies and organisations only. It does not resolve people, vessels, bank-account ownership, sanctions exposure, emissions or geolocation, and its results may not be used for eligibility decisions about individuals, including employment, credit and tenancy.

Retention windows, the erasure transaction and its receipt, the sub-processor list and what assurance does not yet exist are on [the trust page](https://entitybind.com/trust), which is the document a security review should be sent.

## A set you can check by hand, and the one pair that must not match

Every name, domain and registration placeholder below is made up. What the example shows is the method: what the labels say, what comes back, and how precision and recall are computed from the two.

| client_ref | name sent | other fields sent | label: really is | returned |
| --- | --- | --- | --- | --- |
| `acc-101` | Caskmoor Logistics Ltd | `registration_number=CM-0001, domain=caskmoor.example` | Caskmoor Logistics | Caskmoor Logistics |
| `acc-102` | CASKMOOR LOGISTICS LIMITED | `registration_number=CM-0001` | Caskmoor Logistics | Caskmoor Logistics |
| `acc-103` | Caskmoor Logistics | `domain=caskmoor.example` | Caskmoor Logistics | Caskmoor Logistics |
| `acc-104` | Verrenhall Scientific Ltd | `registration_number=VS-0007` | Verrenhall Scientific | Verrenhall Scientific |
| `acc-105` | Verrenhall Scientific (Holdings) Ltd | `registration_number=VS-0912` | Verrenhall Scientific Holdings | Verrenhall Scientific Holdings |
| `acc-106` | Pelmont Foods Ltd | `domain=pelmontfoods.example` | Pelmont Foods | Pelmont Foods |
| `acc-107` | Pelmont Foods | `domain=pelmontfoods.example` | Pelmont Foods | Pelmont Foods |
| `acc-108` | Teal Harbour Consulting | `name only` | Teal Harbour Consulting | unresolved |
| `acc-109` | Caskmoor Logistcs Ltd | `name only` | Caskmoor Logistics | unresolved |

The registration placeholders in that table stand in for a company number or an LEI, and they are written in a shape no register issues, so none of them can be mistaken for a real company's. Every domain is under `.example`, which RFC 2606 reserves for documentation. Both rules are enforced when this site builds, because an invented identifier that turns out to be somebody's real one is how a synthetic example quietly stops being synthetic.

#### What each row was decided by

9 rows · 2 unresolved

- `acc-101`: Binds at the deterministic stage. The registration placeholder agrees, so nothing else runs.
- `acc-102`: Binds to the same company. Case and the legal form are normalised away before anything is compared.
- `acc-103`: Binds on scoring. No registration placeholder, but the name and the agreeing domain carry it.
- `acc-104`: Binds to its own company, alone.
- `acc-105`: Binds to a different company, alone. Its registration placeholder disagrees with acc-104.
- `acc-106`: Binds on scoring, on the agreeing domain.
- `acc-107`: Binds to the same company as acc-106.
- `acc-108`: Unresolved. A name and nothing else, so back comes the shortlist, a typed reason, the column that would settle it and a review id. Zero credits.
- `acc-109`: Unresolved, and this one is a real duplicate. A misspelled name with no second field is the shape the floor refuses.

### The pair that must not match: acc-104 and acc-105 are not the same company

Those two names differ by one token, and every string measure a matcher might reach for scores them as near neighbours. They are two different legal persons, and the registration placeholders say so. A disagreeing strong identifier is answered as a non-match outright and never reaches the merge guard, so the pair is declined rather than scored. Had neither row carried an identifier, the guard would have withheld the merge instead and escalated the record carrying `indistinguishable_candidates`, which is a refusal you can see rather than a merge you cannot.

- **The interesting rows are the ones that did not bind**: The rows that bind on an agreeing identifier are the easy case, and the easy case is what a demonstration usually gets built from. The two unresolved rows are not the same event as each other. acc-108 has no duplicate anywhere in the file, so declining it costs nothing at all. acc-109 is a genuine member of the Caskmoor Logistics cluster, and declining it is the recall this policy spends to keep the wrong merge out.
- **A declined pair is a result, not a gap**: acc-104 with acc-105 never appears as a match, and never appears as a missed match either. They were correctly kept apart, which is the outcome a merge-safety buyer is paying for, and nothing in the arithmetic below rewards it. That is worth knowing before reading any resolver's precision figure, including ours.
- **Everything here is reproducible by hand**: The labels are in the table, the asserted matches are in the table, and the two fractions below are the only arithmetic. Nothing in this example needs a key, a call, or anything from us to check.

### How precision and recall are computed on this set

Five steps, no tooling, and every figure below is counted off the labels in the table rather than asserted. Work them in order and you will get the same two fractions.

1. **Enumerate the pairs, not the rows**. Precision and recall are computed over pairs of records rather than over records, because the question a matcher answers is whether these two rows are the same company. 9 rows make 36 unordered pairs. The labels in the table say which of those are truly the same company, and on this set that is 7: 6 inside the 4-row Caskmoor Logistics cluster (acc-101, acc-102, acc-103, acc-109), and 1 inside the 2-row Pelmont Foods cluster (acc-106, acc-107).
2. **Precision is correct asserted matches over all asserted matches**. The pipeline asserted 4 pairs: 3 inside the 3-row Caskmoor Logistics cluster (acc-101, acc-102, acc-103), and 1 inside the 2-row Pelmont Foods cluster (acc-106, acc-107). All 4 agree with the labels, so precision on this set is 4/4, which is 1.00.
3. **Recall is correct asserted matches over all true matches**. Of the 7 true pairs, 4 were found. The 3 that were missed are every pair involving acc-109, the misspelled name that abstained. Recall is therefore 4/7, which is 0.57, and the gap between the two fractions is the whole shape of the policy: coverage is what pays for the absence of a wrong merge.
4. **Abstentions are not errors, and a refusal is not a find**. An unresolved row is not a wrong answer. It costs recall where the pair was real, as acc-109 does, and costs nothing where the row has no true pair, as acc-108 does. A correctly declined pair, acc-104 with acc-105, enters neither fraction: no match was asserted, so precision cannot see it, and no true match existed, so recall cannot either. Report the coverage beside both numbers, which on this set is 7 of 9 rows decided and 2 declined.
5. **Do not summarise the two with one number**. The harmonic mean of these two is 0.72, and it hides which of them is doing the work. A resolver at 0.72 that over-merges and a resolver at 0.72 that abstains have very different costs in a payments master, so the two are reported separately and the operating point between them is stated rather than optimised away.

- Precision: 1.00 (4/4)
- Recall: 0.57 (4/7)
- Coverage: 7/9 (2 declined)
- Pairs labelled: 7 (of 36 possible)

Those two fractions describe nine invented rows and nothing else. They are arithmetic over a made-up set, chosen to show how the measurement is taken, and they are not a measurement of this engine on anybody's data. The figures that are measurements, with the fixtures, dates, populations and intervals they were taken with, are [on the accuracy page](https://entitybind.com/accuracy).

## Try it on companies you can already check

Start with a handful of companies whose identity you can verify yourself, then bring the awkward ones out of your own data — the same-name pairs, the subsidiaries, the rows carrying a name and not much else. Open the identity and the receipt behind each answer, run the alternatives on the same inputs, and you finish with a result about your data rather than about our fixture.

### Planned free evaluation: 1,000 free credits. The full resolution pipeline.

Give your agent room to test your companies, inspect the citations and compare the results. Deterministic matching, adjudication and registry research, with the same evidence and confidence rules as the paid product.

1,000 free credits. The full resolution pipeline. 1,000 one-time credits, valid for 30 days from your first resolution. No card required. Full resolution depth, with configured sources and their existing licence terms. The full-pipeline evaluation is arranged separately and is not activated by signup. Request it for your workspace; we confirm availability, sources and the start date before activation. Public documentation is open now. Live calls need a key you create in your Entity Bind workspace, which you create online, and the base URL that comes with it. The existing Free plan is a separate tier: shallow decisions only, for wiring up an integration.

[Request API evaluation access →](mailto:hello@entitybind.com?subject=1%2C000-credit%20Entity%20Bind%20evaluation&body=I%20would%20like%20access%20to%20the%20planned%201%2C000-credit%20full-pipeline%20evaluation.%0ACompany%20or%20project%3A%20%0AInput%20fields%20and%20jurisdictions%3A%20%0ARequired%20enrichment%3A%20)

#### The evaluation, step by step

5 steps · what the credits buy

1. Ask the deployment what it can do, before you spend anything. Two reads with your key tell you what is wired up: /v1/reference/sources lists the registers and feeds this deployment can reach, and /v1/reference/enrichment-fields lists the fields that can come back and what you are permitted to resell. Both are free. A field appearing in that list means the deployment can supply it, not that any particular company has a value for it. Over MCP, the same answer comes from get_enrichment_fields.
2. Resolve a company where you already know the answer. Begin with a company number and the register it belongs to. Then take the identifier away and send what your own records actually hold — name plus domain, name plus country, name plus address, name plus a line of context — one input shape at a time, so you can see which of them carry and which do not. Keep your independently verified company number as the expected answer and mark the results against it. Sending it inside a test meant to measure name-plus-domain resolution only proves we can read a company number back.
3. Check the identity, then open the receipt behind it. Compare what came back with the identity you expected: a company number qualified by the register that issued it, or a legal entity identifier. Then fetch the decision itself at /v1/decisions/{decision_id}. match_confidence_basis records which population the confidence was read from, and the cited sources name the pages the evidence came from. Read entity_created carefully — it can mean a company newly adopted from a register, or an identity minted with no register behind it. An HTTP 200 and a fresh entity id tell you a call succeeded, not that the answer is right.
4. Send the ambiguous cases, then add the missing clue. Now try the ones you would not want to get wrong: two live companies sharing a name, a context line pointing at the wrong one, a record with too little evidence to settle. Four outcomes are worth telling apart. unresolved and clarification mean the evidence did not support an answer and nothing was invented to fill the gap — a safe abstention, and a useful result. depth_capped and escalation_unavailable mean the work stopped at a budget cap or could not reach a source, which is a different finding and not a statement about your data. Resubmit with the clue it asked for and check the identity again.
5. Write down what the sample actually showed. Count three things across all the distinct cases you sent: identities accepted and correct, identities accepted and wrong, and abstentions. Split those counts by input shape and by jurisdiction, because a single pooled rate hides the shapes that fail. Keep runs against a cold catalogue separate from runs made after known identities were seeded into it, and treat an exact replay as a test of reliability rather than of new coverage. A small trial can show fit, and evidence you can open; it cannot establish a global accuracy rate.

A decision settled by matching alone costs 1 credit; one that needed a model to adjudicate it or an agent to go and read a register costs 5. The planned 1,000-credit evaluation therefore covers up to 1,000 shallow or 200 deep chargeable decisions, depending on how much work each one needs. An abstention costs 0, and so does a byte-identical replay: 0. Reading a receipt, an entity or a reference collection costs nothing. The planned allowance includes up to 1,000 deep attempts, including those that abstain. Credits and attempts are separate limits. Provider-spend protection may pause work explicitly; unused credits stay visible and the pipeline is never silently downgraded.

#### Which company fields you can get

enrichment, licence and provenance

##### Know which company fields you can get

Once a decision has settled which company a record refers to and admitted that identity, its exact identifiers retrieve the financial and business facts held against it. Find out which fields exist before you build on them, and read every returned fact together with its source, its reporting date and its licence.

A line describing what a company does helps pull back and rank same-name candidates, by weighing activity words alongside the name. On its own it does not establish a match. Country names and codes are normalised on the way in; accepting the name of a country is not a claim that we hold a register for it.

The optional company-data feed supports financial metrics, company descriptions, sector classifications, employees, funding and technology data from an authorised operator-managed export. No commercial supplier, dataset or continuous market-data refresh is bundled. Missing values remain absent. Storage permission does not imply resale permission.

Every returned fact carries where it came from and what you may do with it: provider and provider_record_id name the supplier and its own id for the record, licence_reference names the grant where a supplier gives one, and licence_permits_resale and resellable say whether the fact may leave your systems. Source citations and timestamps sit alongside them. Who supplied a fact and what its licence permits are recorded as two distinct observations.

## Design your filter around these, not around a round number.

A filter at 0.95, or at 0.99, is the instinct when what you are used to reading is a similarity score. Neither is a meaningful bar on this field, and one of them empties the result set instead of tightening it.

### 0.90: The acceptance floor

A match below 0.90 confidence is unrepresentable. The record comes back as an abstention with a ranked shortlist instead.

It is an acceptance contract, not calibration evidence: it says what we refuse to return, not how often an accepted match is right. Read it with the coverage it costs.

### 0.9968: A recorded v8 confidence bound

Confidence bounds depend on the recorded evidence population; there is no universal score for every match. Read match_confidence_basis and the evidence. A 0.99 filter can return some v8 adjudicated matches while excluding valid identifier matches, so validate its coverage cost on your own inputs.

Read it as the cautious end of what one sample supports rather than a headline rate. The nominal 95% Wilson lower bound of 1,190 historical prompt-v8 uncorroborated MATCH verdicts with zero recorded errors. The 2026-08-29 population pools six input shapes with repeated company families and calibration/evaluation overlap. Its independence and transfer to new customer inputs are unproven. Legacy replay cannot certify current calibration. A stricter threshold can exclude valid matches from other evidence populations; check the basis and your own validation sample.

Both numbers are published with the populations they were measured over, including the shapes of file the engine handles worst. [Read the evidence →](https://entitybind.com/accuracy)

## Start with your own company records.

Create your Free workspace, verify your email and create your own API key. Use the same API for CSV matching or an integration, and choose a paid plan in Billing for greater depth. Contact us separately for an assisted Merge Safety Sprint; we write nothing to your CRM, ERP, MDM or warehouse.

[Start free](https://app.entitybind.com/app?signup=1) · [Plans and prices](https://entitybind.com/pricing)

300 credits. No card.

For a guided review of your file, explore [the Merge Safety Sprint](https://entitybind.com/audit-pack).

## Questions engineers ask first

11 answered here, in full. The rest of the bank is on the FAQ page.

### How do I get an API key?

You create it yourself, in the Entity Bind workspace you create online. Create your workspace online and verify your email; it starts on the Free plan with no card. Create your workspace online at https://app.entitybind.com/app?signup=1 and verify your email. Once you are signed in, you create, list and revoke your own keys, each scoped to your tenant with read or resolve permission, and each raw key is shown once. Paid plans are available through Stripe in workspace Billing; registry checks follow your active plan. The evaluation opens at a separately agreed activation date. The planned 1,000-credit evaluation runs at full research depth and is separate from the shallow Free plan. Keys are stored as SHA-256 digests. A read key reads what your tenant already holds and cannot resolve; a resolve key runs the data plane; review and admin credentials carry separate scopes. In your workspace, rotating a key is create, switch, revoke; the admin API's rotate endpoint adds a grace window, and an exposed key can be revoked.

### What does one resolve call return?

A golden record, assembled field by field from whichever source has the best claim to each field — survivorship — with per-field provenance, a calibrated `match_confidence` and the basis behind it, a ranked candidate list, a `decision_id` receipt, the pipeline version, a `usage` block, and a set of flags that say what actually happened: `replayed`, `depth_capped`, `entity_created`, `escalation_unavailable`, `no_identifying_signal`, `more_input_needed`. Every field of the golden record carries its source id, source URL, retrieval time, licence, first-seen and last-checked timestamps, a confirmation count and an age in days, and the entity reports the age of its stalest field rather than its freshest — because one recently confirmed value should not be able to make a stale record look current.

### What is the difference between /v1/resolve and /v1/match?

`/v1/resolve` takes one record and binds it to an entity in your catalogue, or abstains. It persists: it mints a record id, may create or join an entity, and writes a decision receipt you can reopen. `/v1/match` is a stateless pairwise comparison of two records you supply — is this the same company as that one — and it persists nothing at all, so there is no record id, no entity id and no receipt to fetch afterwards. Both charge on the same rule. Use match when you are adjudicating somebody else's proposed pair and do not want a catalogue entry; use resolve when you want a golden record and an audit trail.

### Can I push a whole file through it?

Yes, over `/v1/batch`, which takes up to 500 records per request by default, is `Idempotency-Key` aware, writes one ledger row per outcome rather than one summed row, and is order-independent within the batch. Your Entity Bind workspace also takes a CSV upload, up to 500 company rows by default, and a finished run downloads as CSV or JSON with its decision receipts. For a larger file in a Sprint, the import we run streams it through the same public API — so a successful import is evidence your own integration will work rather than a bespoke pipeline — and carries your key column through as `client_ref`, so the output joins back on your ids rather than ours. It is not a queue: batches go one at a time, and a 300,000-row file is an overnight run. Plan the window rather than the throughput.

### How do I cap what a record costs?

With the `depth` parameter, which is a price and latency ceiling and can be set per record even inside a batch. `research` is the default and runs the whole cascade. `adjudication` stops before agentic research. `probabilistic` is a hard no-egress setting enforced in code at both egress points: no model provider and no registry, so a capped record reaches neither. There is a second, orthogonal control, `enrich=false`, which turns the registries off at any depth for a caller whose contract forbids the lookup. Capping never lowers precision — the alternative to a deep answer is an abstention rather than a cheap guess — so what you trade away is automation rather than correctness.

### Can our plan cap a run below the depth we asked for?

Yes, and it is the trap worth knowing about before you read a resolution rate. The effective depth is the lower of what you requested and what your plan permits, so a `research` request on the Free plan executes at the no-egress stage instead — and because a new Entity Bind workspace starts on that plan, and because the clamp applies even when usage quota enforcement is off, a whole run can execute two stages shallower than the one it was quoted at. What stops that being invisible: `/v1/batch` echoes a `depth_ceiling`, the import we run for a Sprint keeps the shallowest value any batch reported and prints both on its run report, and resuming a run at a different depth is refused rather than blended. Read the ceiling before you read the rate.

### What are the rate limits?

Per endpoint class, and bucketed per tenant rather than per key, so a customer with three keys has bought one allowance. The published defaults are 1,000 reads a minute, 300 resolves, 300 matches, 30 batches and 120 review calls, scaled by a plan multiplier. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` come back on success as well as on a 429, and a `Retry-After` is added when a request is refused. Two caveats worth reading before you size anything: the limits are deployment settings you should read from `GET /v1/reference/limits` rather than hard-code, and without a shared limiter store they are enforced once per replica.

### What does the error model look like?

One envelope on every failure — `{"error": {"code", "message", "correlation_id"}}` — with a stable machine code from a published vocabulary you can fetch at `GET /v1/reference/error-codes`. Internal detail never reaches a caller: any 5xx message is replaced with canned text, so a driver error cannot leak SQL or bound parameters. Two choices are worth knowing. A wrong-tenant id returns 404 rather than 403, because a 403 confirms the id exists and turns every read into an enumeration oracle. And `deadline_exceeded` is a 504 rather than a 503, because nothing upstream is down — our budget for that request ran out, and the fix is a lower depth or a longer allowance.

### Is the API idempotent?

In two different ways. `/v1/batch` takes an `Idempotency-Key` header: identical concurrent callers wait for and replay the sealed winner, the same key with different content is a permanent conflict, and row ids are derived deterministically so a replay mints the same ids. `/v1/resolve` needs no header, because its idempotency is a content fingerprint — the SHA-256 of the submitted record excluding the server-minted id — computed before the cascade runs. A byte-identical resubmission returns the same binding, is flagged `replayed`, and costs nothing. The binding has no expiry. It is per tenant and per exact content: one corrected postcode is a different fingerprint and a fresh resolution.

### Can we correct a merge you got wrong?

Yes, within a deliberately narrow boundary. `POST /v1/entities/{id}/detach` moves exactly one attributed record, and every fact carrying that record's origin id, onto a fresh entity, in one transaction that also updates both of the indexes deciding which candidates get compared, the replay binding, your own crosswalk links and an immutable receipt. It requires a review-scoped credential, so an ingestion key cannot do it, and it fails closed with a 409 before any write if any of six preconditions does not hold. What it is not is a general unmerge: no arbitrary cluster split, no bridge analysis, no rollback to a historical cluster shape, no automatic reattach. Registry and research facts stay on the source rather than being guessed onto one side.

### How does the abstention queue actually get smaller?

By settling items, and settling does two things rather than one. First, a verdict is applied rather than merely recorded: it merges through the same guard the cascade uses, and a non-match excludes the presented candidate and re-runs retrieval, binding an alternative only where that alternative independently clears the floor and the candidate-margin guard. The item reports whether it was applied, which entity it landed on, and a blocked reason where it was not, so "recorded" and "acted on" are distinguishable at a glance. Second, a settled verdict stops the same bytes re-queueing — and only the same bytes. A corrected postcode is a different record and can queue again for a company somebody settled last week. A queue that keeps refilling with the same names is a source system emitting drifting rows, and the fix for that is upstream normalisation rather than more reviewers.

---

Internal fixtures only. The historical 2026-08-29 per-tier recording covers 317 GB companies with identifier-rich arrivals seeding the catalogue first; the context tier includes country and registered-town prose. Its legacy replay has no full-prompt-verified calls and cannot certify current calibration. The September 5 CI reports cover 1,176 labelled pairs and 705 records over 250 companies using offline stubs. These are not independent customer holdouts or live production validation; the 99% minimum-input product target remains unmet. Scope is company and organisation identity only — not people, vessels, bank-account ownership, sanctions or emissions. Results may not be used for eligibility decisions about individuals, including employment, credit and tenancy.

This is the Markdown copy of https://entitybind.com/api, assembled from the same data the page renders and carrying every sentence, table row, figure and question the page's main content carries. The HTML page is canonical; it adds the layout, the navigation and the interactive parts (the ask panel, the forms), which a copy can name but not run. Every page on the site, with a line on each: https://entitybind.com/llms.txt.
