TOVO REST API
Enrich CRM records with FCA Register and Companies House data on UK regulated firms and the individuals.
The API at a glance
| Area | What it does | Cost |
|---|---|---|
| Companies | Fetch and match by domain / name / FRN / CH number. | 1 credit per new record |
| People | Fetch by IRN, match by name + company. | 1 credit per new record |
| Search | Filtered lists of firms and people, people at a firm, disciplinary history. Regulatory data and contact availability, never contact values. | Free |
| Bulk | Sync batches of up to 100 records; async jobs of up to 10,000 with cost estimates before you commit. | 1 credit per new record |
| Credits | Balance, plan, and a per-record ledger you can reconcile against your CRM. | Free |
| Freshness | updated_since polling and 410 Gone tombstones for removed records. | Free |
Authentication#
Every request is authenticated with a bearer token in the Authorization header. Two kinds of token are accepted.
API keys
Keys are issued per organisation in the TOVO dashboard. The prefix tells you which environment a key belongs to.
| Prefix | Environment | Behaviour |
|---|---|---|
tovo_live_ | Production | Real data, real credits. |
tovo_test_ | Sandbox | Same endpoints, deterministic fixture data, simulated credits. Never charged. |
Authorization: Bearer YOUR_API_KEYOAuth 2.0 client credentials
Exchange a client ID and secret for a short-lived access token. Tokens last one hour; request a new one when expires_in runs out — there is no refresh token in this grant.
curl -X POST https://auth.tovodata.co.uk/oauth/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d scope="read enrich jobs credits"{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read enrich jobs credits"
}Scopes
API keys carry a fixed set of scopes chosen when the key is created. OAuth clients request them at token time. A request outside its token's scopes fails with 403 insufficient_scope.
| Scope | Grants |
|---|---|
read | Search endpoints, people-at-a-firm, disciplinary history, and reads of records your organisation holds at their current version. Never spends credits. |
enrich | Fetch and match — any call that can charge a credit. Implies read. |
jobs | Create, poll, and cancel enrichment jobs. |
credits | Read the credit balance and transaction ledger. |
Key handling
- Treat keys and client secrets as passwords. Never embed them in client-side code, mobile apps, or public repositories.
- Send them only over HTTPS. Plain-HTTP requests are rejected, not redirected.
- Rotate a key from the dashboard. The old key keeps working for 24 hours so you can roll deployments.
- Give integrations that only read data a
read-only key. A leaked read-only key cannot spend your credits.
Conventions#
| Topic | Convention |
|---|---|
| Base URL | https://api.tovodata.co.uk/v1. The version is in the path. Breaking changes ship as a new version; /v1 stays stable. |
| Format | JSON in and out, UTF-8. Send Content-Type: application/json on requests with a body. |
| Naming | snake_case for every field, parameter, and enum value, in requests and responses alike. |
| Identifiers | frn, irn, and ch_number are always strings, even when they look numeric. FRNs and CH numbers keep their leading zeros. |
| Dates and times | ISO 8601. Timestamps are UTC with a Z suffix (2026-09-15T09:12:03Z); dates without a time are YYYY-MM-DD. |
| Money | Integers in the currency's minor unit is not used; amounts are whole-currency numbers alongside an explicit currency field ("revenue": 14940000000, "currency": "GBP"). |
| Nulls | A field TOVO knows about but has no value for is null. A field is never omitted from a response because it is empty — unless you asked for a subset with fields. |
| Sparse fields | ?fields=frn,name,contact on any fetch or search returns only those top-level fields plus identifiers. Use it to keep payloads under Salesforce's 6 MB callout limit. |
| Compression | Send Accept-Encoding: gzip. Responses over 1 KB are compressed. |
| Request IDs | Every response carries X-Request-Id. Quote it when contacting support; it is also inside every error body and every credit transaction. |
| Idempotency | Send Idempotency-Key: <uuid> on any POST that can spend credits or create a job. Replaying the same key within 24 hours returns the original response and charges nothing more. A different body under a reused key fails with 409 idempotency_conflict. |
| Deprecation | Anything scheduled for removal answers with Deprecation: true and a Sunset date header, and is listed in the changelog at least 12 months ahead. |
Errors#
Errors are returned with a non-2xx status and a body in the RFC 9457 Problem Details format, with Content-Type: application/problem+json. Branch on code, not on detail — the human-readable text may change, the code will not.
| Field | Type | Description |
|---|---|---|
type | string | URI identifying the error class. Stable; safe to link to. |
title | string | Short human-readable summary. |
status | integer | HTTP status, repeated in the body. |
detail | string | What went wrong in this specific case. |
code | string | Machine-readable code from the error catalogue. |
request_id | string | Matches the X-Request-Id header. |
errors | object[] | On 422 only: one entry per invalid field, with field and message. |
credits | object | On 402 only: required and balance. |
{
"type": "https://api.tovodata.co.uk/errors/insufficient_credits",
"title": "Insufficient credits",
"status": 402,
"detail": "This request needs 1 credit; your balance is 0.",
"code": "insufficient_credits",
"request_id": "req_01J8ZK3M9QW2X7",
"credits": { "required": 1, "balance": 0 }
}{
"type": "https://api.tovodata.co.uk/errors/not_found",
"title": "Not found",
"status": 404,
"detail": "No company with FRN 999999.",
"code": "not_found",
"request_id": "req_01J8ZK4R2TN8VB"
}{
"type": "https://api.tovodata.co.uk/errors/validation_failed",
"title": "Validation failed",
"status": 422,
"detail": "The request body has 2 invalid fields.",
"code": "validation_failed",
"request_id": "req_01J8ZK5T7HP3MC",
"errors": [
{ "field": "company", "message": "Provide at least one of company.frn, company.domain, company.name." },
{ "field": "min_confidence", "message": "Must be between 0 and 1." }
]
}A 402 never charges and never returns contact values. The matched record still comes back with its regulatory data and contact_available, so you can show exactly what a top-up would buy. The full list of codes is in the error catalogue.
Rate limits#
Limits apply per organisation, across all keys, in a rolling one-minute window. Every response reports where you stand.
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the current window. |
RateLimit-Remaining | Requests left in the current window. |
RateLimit-Reset | Seconds until the window resets. |
Retry-After | On 429 only: seconds to wait before retrying. |
| Endpoint group | Standard | Enterprise |
|---|---|---|
| Fetch, match, search | 600 / min | Negotiated |
| Sync batch | 60 / min | Negotiated |
| Enrichment jobs | 10 concurrent | Negotiated |
On 429 rate_limited, wait for Retry-After and retry with exponential backoff. Bulk work belongs in enrichment jobs, which are not subject to the per-minute limit — the queue paces them.
Pagination#
Every list endpoint is cursor-paginated. Cursors are opaque and stable across concurrent inserts, so a long sync never skips or duplicates a record.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 25 | Records per page, 1–100. |
cursor | string | — | The next_cursor from the previous page. Omit for the first page. |
{
"data": [ { "...": "one record per element" } ],
"next_cursor": "eyJvZmZzZXQiOjI1LCJ0IjoiMjAyNi0wOS0xNSJ9",
"has_more": true
}Stop when has_more is false; next_cursor is null on the final page. Cursors expire after 24 hours. Search endpoints do not return a total count — it is expensive to compute and rarely needed for enrichment.
Get a company#
Fetches a single firm by its FCA Firm Reference Number and returns the complete record, contact details included. Charges a credit unless your organisation already holds the firm's current version — in which case it is free, and returns exactly the same payload. Once TOVO changes the record, the copy you hold is superseded and the next fetch charges again: see Keeping data fresh. To read a named firm's regulatory data, use search with filters.frn.
| Parameter | In | Type | Default | Description |
|---|---|---|---|---|
frn | path | string | — | Firm Reference Number, e.g. 122702. |
require | query | string | — | Comma-separated contact fields the firm must have before a credit is spent: phone, linkedin, any_contact. |
fields | query | string | all | Comma-separated top-level fields to return. |
curl "https://api.tovodata.co.uk/v1/companies/122702?require=any_contact" \
-H "Authorization: Bearer YOUR_API_KEY"{
"frn": "122702",
"ch_number": "01026167",
"name": "Barclays Bank PLC",
"trading_names": ["Barclays", "Barclays Business"],
"website": "https://www.barclays.com",
"domain": "barclays.com",
"fca_status": "authorised",
"fca_status_effective_date": "2001-12-01",
"permissions": ["accepting_deposits", "dealing_in_investments_as_principal", "arranging_deals_in_investments"],
"client_money_permission": "hold_and_control",
"principal_firm": null,
"appointed_representatives_count": 2,
"disciplinary_history": { "count": 3, "latest_at": "2024-11-19" },
"ch_status": "active",
"incorporated_at": "1925-08-04",
"sic_codes": [{ "code": "64191", "description": "Banks" }],
"registered_address": { "line_1": "1 Churchill Place", "city": "London", "postcode": "E14 5HP", "country": "GB" },
"officers_count": 120,
"employee_count": 24000,
"employee_band": "10001+",
"counts": { "cf_individuals": 3279, "smf_individuals": 31, "directors": 109 },
"sector_activities": [{ "path": ["Lending & Risk", "Bank (Retail)"] }],
"summary": "Barclays Bank PLC is a UK clearing bank authorised...",
"financials": {
"period_end": "2025-03-31",
"currency": "GBP",
"revenue": 14940000000,
"ebitda": -1994000000,
"net_assets": 61496999936
},
"aum": { "value": 382400000000, "currency": "GBP", "as_of": "2025-03-31" },
"financial_health": { "score": "strong", "based_on_filing_date": "2025-03-31" },
"location": { "city_region": "City of London, London", "postcode": "E14 5HP" },
"contact": {
"phone": [{ "value": "+442071161000", "type": "switchboard" }],
"linkedin": [{ "value": "https://www.linkedin.com/company/barclays-bank", "type": "company" }]
},
"purchased_at": "2026-09-15T09:31:44Z",
"last_updated_at": "2026-09-10T12:04:17Z",
"sources": ["fca_register", "companies_house", "tovo"]
}| Status | When |
|---|---|
200 | Found. Complete record, contact included — charged unless you already owned it, or require was not satisfied. |
402 | The record needs a credit and the balance is zero. The body carries the firm's regulatory data and contact_available, but no contact values. |
404 | No firm with this FRN. |
410 | The firm existed and has been removed from TOVO. See Keeping data fresh. |
Field definitions are in the Company object.
Match a company#
Finds the firm that best matches what you know about it. Send any combination of identifiers; the more you send, the higher the confidence. This is the Account-enrichment call.
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | At least one | Website domain, e.g. barclays.com. Scheme, www., and paths are stripped. The strongest single key. |
name | string | Company or trading name. Fuzzy-matched; punctuation and suffixes (Ltd, PLC, LLP) are normalised. | |
frn | string | Exact FRN. Overrides fuzzy matching when present. | |
ch_number | string | Exact Companies House number. | |
min_confidence | number | No | Default 0.85. Range 0–1. |
require | string[] | No | Contact fields the firm must have before a credit is spent: phone, linkedin, any_contact. All listed fields must be present. Unset, every match charges. |
fields | string[] | No | Top-level company fields to return. |
curl -X POST https://api.tovodata.co.uk/v1/companies/match \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c9a1e-3b7d-4a5e-9c2f-8d4e1b7a2c3d" \
-d '{
"domain": "barclays.com",
"name": "Barclays"
}'{
"match": {
"status": "matched",
"confidence": 0.99,
"matched_on": ["domain", "name"]
},
"company": {
"frn": "122702",
"ch_number": "01026167",
"name": "Barclays Bank PLC",
"domain": "barclays.com",
"fca_status": "authorised",
"contact": {
"phone": [{ "value": "+442071161000", "type": "switchboard" }],
"linkedin": [{ "value": "https://www.linkedin.com/company/barclays-bank", "type": "company" }]
},
"purchased_at": "2026-09-15T09:14:51Z",
"...": "remaining Company object fields"
},
"candidates": [],
"credits": { "charged": 1, "balance": 4998 }
}{
"match": {
"status": "ambiguous",
"confidence": 0.71,
"matched_on": ["name"]
},
"company": null,
"candidates": [
{ "frn": "122702", "name": "Barclays Bank PLC", "domain": "barclays.com", "confidence": 0.71 },
{ "frn": "155595", "name": "Barclays Bank UK PLC", "domain": "barclays.co.uk", "confidence": 0.69 },
{ "frn": "759676", "name": "Barclays Investment Solutions Limited", "domain": "barclays.com", "confidence": 0.44 }
],
"credits": { "charged": 0, "balance": 4998 }
}{
"match": { "status": "not_found", "confidence": 0, "matched_on": [] },
"company": null,
"candidates": [],
"credits": { "charged": 0, "balance": 4998 }
}An ambiguous result is a decision for a human or for stricter input, not an error. Pick a candidate and call GET /v1/companies/{frn} to buy it, or retry the match with the candidate's frn. A name-only match of a group with many regulated entities — banks, insurers — is the common cause; adding the domain almost always resolves it.
Search companies#
Returns firms matching a free-text query and a set of filters, cursor-paginated. Search never charges and never returns contact values. Each result carries the firm's full regulatory data plus contact_available and credits_to_fetch, so you can build a target list, see what each record would cost you, and then buy the ones you want with fetch or match. credits_to_fetch is 1 both for firms you have never bought and for ones TOVO has changed since you did. With filters.frn accepting up to 1,000 exact FRNs, this is also the free way to refresh regulatory data on firms you have not bought.
| Field | Type | Description |
|---|---|---|
query | string | Free text matched against names, trading names, and domains. |
filters | object | Structured filters — see Search filters. All supplied filters must match. |
updated_since | string | ISO 8601 timestamp. Only records changed after it. The refresh key — see Keeping data fresh. |
sort | object | { "field": "…", "order": "asc" | "desc" }. Sortable: name, employee_count, counts.smf_individuals, financials.revenue, financials.net_assets, last_updated_at. Default last_updated_at desc. |
fields | string[] | Top-level fields to return per record. |
limit, cursor | See Pagination. |
curl -X POST https://api.tovodata.co.uk/v1/companies/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"fca_status": ["authorised"],
"location": ["London"],
"employee_band": ["51-200", "201-1000"],
"financial_health": ["strong", "good"],
"permissions": { "include": ["advising_on_investments"], "exclude": ["accepting_deposits"] }
},
"sort": { "field": "counts.smf_individuals", "order": "desc" },
"fields": ["frn", "name", "domain", "fca_status", "counts", "contact_available"],
"limit": 25
}'{
"data": [
{
"frn": "312026",
"name": "Independent Broking Solutions Limited",
"domain": "ibsbroking.com",
"fca_status": "authorised",
"counts": { "cf_individuals": 56, "smf_individuals": 9, "directors": 4 },
"contact_available": { "phone": true, "linkedin": false },
"purchased_at": null,
"credits_to_fetch": 1
},
{ "...": "24 more" }
],
"next_cursor": "eyJzIjpbOSwiMzEyMDI2Il19",
"has_more": true
}People at a company#
Lists the individuals holding FCA functions at a firm. The "find contacts at this Account" call. Free, and like search it returns availability rather than contact values — buy the individuals you want with GET /v1/people/{irn}.
| Parameter | In | Type | Default | Description |
|---|---|---|---|---|
frn | path | string | — | Firm Reference Number. |
current | query | boolean | true | Only people with a current role at the firm. false includes past roles. |
smf_functions | query | string | — | Comma-separated SMF codes, e.g. SMF1,SMF3,SMF16. |
has_contact | query | boolean | — | true returns only people with at least one contact field. Listing is free either way; this narrows the list before you start spending on it. |
fields, limit, cursor | query | See Pagination. |
curl "https://api.tovodata.co.uk/v1/companies/433927/people?smf_functions=SMF1,SMF3&has_contact=true" \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": [
{
"irn": "PCW00020",
"name": "Paul Cheriton Wreford",
"current_roles": [{ "role": "Executive Director", "smf_code": "SMF3", "frn": "433927", "firm_name": "Hoyl Independent Advisers Ltd", "effective_date": "2024-09-13" }],
"contact_available": { "email": true, "phone": false, "linkedin": true },
"purchased_at": null,
"credits_to_fetch": 1
}
],
"next_cursor": null,
"has_more": false
}Get a person#
Fetches a single individual by their FCA Individual Reference Number and returns the complete record, contact details included. Charges a credit unless your organisation already holds the person's current version — in which case it is free, and returns exactly the same payload. Once TOVO changes the record, the copy you hold is superseded and the next fetch charges again: see Keeping data fresh. To read a named individual's regulatory data without buying it, use search with filters.irn.
| Parameter | In | Type | Default | Description |
|---|---|---|---|---|
irn | path | string | — | Individual Reference Number, e.g. PCW00020. |
require | query | string | — | Comma-separated contact fields the person must have before a credit is spent: email, phone, linkedin, any_contact. ?require=email means “free unless there is an email”. |
fields | query | string | all | Comma-separated top-level fields to return. |
curl https://api.tovodata.co.uk/v1/people/PCW00020 \
-H "Authorization: Bearer YOUR_API_KEY"{
"irn": "PCW00020",
"name": "Paul Cheriton Wreford",
"first_name": "Paul",
"last_name": "Wreford",
"avatar_url": null,
"fca_status": "approved",
"smf_functions": [{ "code": "SMF3", "name": "Executive Director" }],
"certification_functions": ["client_dealing"],
"controlled_functions": [],
"disciplinary_history": { "count": 0, "latest_at": null },
"current_roles": [
{ "role": "Executive Director", "smf_code": "SMF3", "frn": "433927", "firm_name": "Hoyl Independent Advisers Ltd", "effective_date": "2024-09-13" }
],
"employment_history": [
{ "frn": "433927", "firm_name": "Hoyl Independent Advisers Ltd", "role": "Executive Director", "from": "2024-09-13", "to": null, "is_current": true },
{ "frn": "186310", "firm_name": "Smith & Williamson Financial Services", "role": "Adviser", "from": "2016-02-01", "to": "2024-08-31", "is_current": false }
],
"location": { "city_region": "Norwich, Midlands" },
"contact": {
"email": [{ "value": "p.wreford@hoyl-advisers.com", "type": "work", "confidence": "high", "verified_at": "2026-08-30" }],
"phone": [],
"linkedin": [{ "value": "https://www.linkedin.com/in/paul-wreford", "type": "profile" }]
},
"purchased_at": "2026-09-15T09:12:03Z",
"last_updated_at": "2026-09-10T15:49:24Z",
"sources": ["fca_register", "tovo"]
}| Status | When |
|---|---|
200 | Found. Complete record, contact included — charged unless you already owned it, or require was not satisfied. |
402 | The record needs a credit and the balance is zero. The body carries the person's regulatory data and contact_available, but no contact values. |
404 | No individual with this IRN. |
410 | The individual existed and has been removed from TOVO. Purge your copy — see Keeping data fresh. |
Field definitions are in the Person object.
Match a person#
Finds the individual that best matches a name and the firm they work at. This is the Contact- and Lead-enrichment call. Email and LinkedIn are outputs of this endpoint, not inputs — TOVO identifies people by who they are and where they are approved, not by their inbox.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | One of | Full name as written. Middle names and initials help; honorifics are ignored. |
first_name, last_name | string | Split name, if that is what your CRM holds. | |
company | object | Yes, unless irn | The firm. Any of frn, domain, name. FRN is exact; domain and name resolve through company matching first. |
irn | string | No | If you already have it, matching is exact and company is optional. Prefer GET /v1/people/{irn} in that case. |
role_hint | string | No | Job title from your CRM. Used as a tie-breaker between people with the same name at the same firm. |
min_confidence | number | No | Default 0.85. |
require | string[] | No | Contact fields the person must have before a credit is spent: email, phone, linkedin, any_contact. All listed fields must be present. Unset, every match charges. |
fields | string[] | No | Top-level person fields to return. |
curl -X POST https://api.tovodata.co.uk/v1/people/match \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2b8e4d10-7f3a-4c96-b1e5-0a9d3c6f8e21" \
-d '{
"first_name": "Paul",
"last_name": "Wreford",
"company": { "domain": "hoyl-advisers.com" },
"role_hint": "Director",
"min_confidence": 0.9
}'{
"match": {
"status": "matched",
"confidence": 0.94,
"matched_on": ["first_name", "last_name", "company.domain", "role_hint"]
},
"person": {
"irn": "PCW00020",
"name": "Paul Cheriton Wreford",
"current_roles": [{ "role": "Executive Director", "smf_code": "SMF3", "frn": "433927", "firm_name": "Hoyl Independent Advisers Ltd", "effective_date": "2024-09-13" }],
"contact": {
"email": [{ "value": "p.wreford@hoyl-advisers.com", "type": "work", "confidence": "high", "verified_at": "2026-08-30" }],
"phone": [],
"linkedin": [{ "value": "https://www.linkedin.com/in/paul-wreford", "type": "profile" }]
},
"purchased_at": "2026-09-15T09:12:03Z",
"...": "remaining Person object fields"
},
"candidates": [],
"credits": { "charged": 1, "balance": 4997 }
}{
"match": {
"status": "ambiguous",
"confidence": 0.62,
"matched_on": ["last_name", "company.name"]
},
"person": null,
"candidates": [
{ "irn": "JSM01822", "name": "James Smith", "current_role": "Compliance Oversight (SMF16)", "frn": "122702", "confidence": 0.62 },
{ "irn": "JSM04410", "name": "Jamie Smith", "current_role": "Executive Director (SMF3)", "frn": "122702", "confidence": 0.58 },
{ "irn": "JSM00917", "name": "J. A. Smith", "current_role": "Money Laundering Reporting (SMF17)", "frn": "122702", "confidence": 0.41 }
],
"credits": { "charged": 0, "balance": 4997 }
}{
"type": "https://api.tovodata.co.uk/errors/insufficient_credits",
"title": "Insufficient credits",
"status": 402,
"detail": "This request needs 1 credit; your balance is 0.",
"code": "insufficient_credits",
"request_id": "req_01J8ZM0V3KQ9RD",
"credits": { "required": 1, "balance": 0 },
"match": { "status": "matched", "confidence": 0.94, "matched_on": ["first_name", "last_name", "company.domain", "role_hint"] },
"person": {
"irn": "PCW00020",
"name": "Paul Cheriton Wreford",
"contact_available": { "email": true, "phone": false, "linkedin": true },
"purchased_at": null,
"...": "remaining Person object fields"
}
}Common names at large firms produce ambiguous results. role_hint and a split first_name/last_name both raise confidence; so does an FRN instead of a company name. The 402 body is unusual in carrying the matched record itself — regulatory data and contact_available, but no contact values — so a top-up flow can show the user exactly what the credit would buy.
Search people#
Returns individuals matching a free-text query and filters, cursor-paginated. Free, and like Search companies it returns contact_available and credits_to_fetch rather than contact values. Same envelope and options; the filters differ. filters.irn takes up to 1,000 exact IRNs, which makes this the free way to refresh regulatory data on people you have not bought.
| Field | Type | Description |
|---|---|---|
query | string | Free text matched against names and current role titles. |
filters | object | See Search filters — people. |
updated_since | string | Only records changed after this timestamp. |
sort | object | Sortable: name, last_updated_at, current_roles.effective_date. Default last_updated_at desc. |
fields, limit, cursor | As elsewhere. |
curl -X POST https://api.tovodata.co.uk/v1/people/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"smf_functions": ["SMF16", "SMF17"],
"firm_fca_status": ["authorised"],
"firm_employee_band": ["201-1000", "1001-10000"],
"has_email": true
},
"fields": ["irn", "name", "current_roles", "contact_available"],
"limit": 50
}'{
"data": [
{
"irn": "JSM01822",
"name": "James Smith",
"current_roles": [{ "role": "Compliance Oversight", "smf_code": "SMF16", "frn": "122702", "firm_name": "Barclays Bank PLC", "effective_date": "2021-03-01" }],
"contact_available": { "email": true, "phone": true, "linkedin": true },
"purchased_at": "2026-06-02T11:20:41Z",
"credits_to_fetch": 1
},
{ "...": "49 more" }
],
"next_cursor": "eyJzIjpbIjIwMjYtMDktMTAiLCJKU00wMTgyMiJdfQ",
"has_more": true
}Sync batch match#
Matches up to 100 records in one request and returns every result inline. Built for real-time use from Salesforce — a batch of 100 completes well inside the 120-second Apex callout limit. For more than 100 records, or when you don't need the answer immediately, use enrichment jobs.
| Field | Type | Required | Description |
|---|---|---|---|
records | object[] | Yes | 1–100 entries. Each is a person match or company match body plus a ref. Over 100 fails with 413 batch_too_large. |
records[].ref | string | Yes | Your identifier for the record — a Salesforce Id, a row number. Echoed unchanged in the matching result and in the credit ledger. Must be unique within the batch. |
min_confidence | number | No | Default 0.85. Applies to every record. |
require | string[] | No | Applies to every record in the batch. Records that fail it come back matched but without a contact block, carrying "charged": 0, "reason": "requirements_not_met". |
fields | string[] | No | Top-level fields to return per record. Strongly recommended for batches — 100 full Person objects is a large response. |
curl -X POST https://api.tovodata.co.uk/v1/people/match/batch \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9c4f2a7e-1d3b-4e8a-a6c5-7b2d9f0e3a18" \
-d '{
"min_confidence": 0.9,
"require": ["email"],
"fields": ["irn", "name", "current_roles", "contact"],
"records": [
{ "ref": "003Ab00000Xk1QzIAB", "name": "Paul Cheriton Wreford", "company": { "domain": "hoyl-advisers.com" } },
{ "ref": "003Ab00000Xk1R0IAB", "name": "James Smith", "company": { "name": "Barclays" } },
{ "ref": "003Ab00000Xk1R1IAB", "name": "Nobody Realname", "company": { "frn": "122702" } }
]
}'{
"results": [
{
"ref": "003Ab00000Xk1QzIAB",
"match": { "status": "matched", "confidence": 0.94, "matched_on": ["name", "company.domain"] },
"person": { "irn": "PCW00020", "name": "Paul Cheriton Wreford", "current_roles": [ "..." ], "contact": { "email": [ "..." ], "phone": [], "linkedin": [ "..." ] } },
"credits": { "charged": 1 }
},
{
"ref": "003Ab00000Xk1R0IAB",
"match": { "status": "ambiguous", "confidence": 0.62, "matched_on": ["name", "company.name"] },
"person": null,
"candidates": [ { "irn": "JSM01822", "name": "James Smith", "current_role": "Compliance Oversight (SMF16)", "frn": "122702", "confidence": 0.62 }, "..." ],
"credits": { "charged": 0 }
},
{
"ref": "003Ab00000Xk1R1IAB",
"match": { "status": "not_found", "confidence": 0, "matched_on": [] },
"person": null,
"credits": { "charged": 0 }
}
],
"summary": { "total": 3, "matched": 1, "ambiguous": 1, "not_found": 1, "already_current": 0, "new": 1, "updated": 0, "requirements_not_met": 0, "purchased": 1, "with_email": 1, "with_phone": 0, "with_linkedin": 1 },
"credits": { "charged": 1, "balance": 4996 }
}summary carries the same breakdown a job reports, so a small selection gets its download summary without the round-trip of a dry run.
A batch is all-or-nothing only for validation: a malformed record fails the whole request with 422 before anything is charged. Once accepted, each record is processed independently, and the response is 200 even if every record is not_found. If your balance runs out mid-batch, the remaining matched records come back without their contact blocks and with "credits": { "charged": 0, "reason": "insufficient_credits" } — the batch still succeeds. Records skipped by require carry "reason": "requirements_not_met" in the same place.
Enrichment jobs#
Jobs process up to 10,000 records asynchronously — or fewer, if your plan's limits.max_records_per_job is lower. You submit, poll for completion, then download results. They are the right tool for backfilling a CRM, and the only tool that tells you what a run will cost before it runs.
Create a job
| Field | Type | Required | Description |
|---|---|---|---|
resource | string | Yes | people or companies. A job handles one resource type. |
records | object[] | Yes | 1–10,000 match bodies, each with a unique ref. Same shape as a sync batch. |
min_confidence | number | No | Default 0.85. |
require | string[] | No | Applies to every record. Set it before a large backfill: it is the difference between paying for 3,104 matches and paying for the 1,402 that carry an email. |
dry_run | boolean | No | Default false. When true, the job matches every record but buys nothing and charges nothing; it exists to produce credits.estimated_max and the counts breakdown. |
fields | string[] | No | Top-level fields in each result. |
curl -X POST https://api.tovodata.co.uk/v1/enrichment_jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"resource": "people",
"dry_run": true,
"min_confidence": 0.9,
"require": ["email"],
"records": [
{ "ref": "003Ab00000Xk1QzIAB", "name": "Paul Cheriton Wreford", "company": { "domain": "hoyl-advisers.com" } },
{ "ref": "003Ab00000Xk1R0IAB", "name": "James Smith", "company": { "frn": "122702" }, "role_hint": "Compliance" }
]
}'{
"id": "job_01J8ZN2C6WQ4HT",
"resource": "people",
"status": "queued",
"dry_run": true,
"min_confidence": 0.9,
"require": ["email"],
"counts": { "total": 4200, "processed": 0, "matched": 0, "ambiguous": 0, "not_found": 0, "failed": 0, "already_current": 0, "new": 0, "updated": 0, "requirements_not_met": 0, "purchased": 0, "with_email": 0, "with_phone": 0, "with_linkedin": 0 },
"credits": { "estimated_max": null, "charged": 0 },
"created_at": "2026-09-15T10:02:11Z",
"started_at": null,
"completed_at": null,
"results_url": null
}The response headers include Location: /v1/enrichment_jobs/job_01J8ZN2C6WQ4HT and Retry-After: 15. When a dry run completes, credits.estimated_max is the number of records that matched at or above min_confidence, satisfied require, and are not already owned — the most a real run of the same records can cost. Submit the real job with the same records, the same require, and dry_run omitted.
Poll a job
Returns the job object. Poll no faster than the Retry-After header suggests — it shrinks as the job nears completion. Jobs progress through queued → running → completed, or end in failed or cancelled.
{
"id": "job_01J8ZN2C6WQ4HT",
"resource": "people",
"status": "completed",
"dry_run": true,
"min_confidence": 0.9,
"require": ["email"],
"counts": { "total": 4200, "processed": 4200, "matched": 3104, "ambiguous": 611, "not_found": 485, "failed": 0, "already_current": 786, "new": 430, "updated": 186, "requirements_not_met": 1702, "purchased": 0, "with_email": 1402, "with_phone": 2190, "with_linkedin": 1876 },
"credits": { "estimated_max": 616, "charged": 0 },
"created_at": "2026-09-15T10:02:11Z",
"started_at": "2026-09-15T10:02:14Z",
"completed_at": "2026-09-15T10:09:47Z",
"results_url": "https://api.tovodata.co.uk/v1/enrichment_jobs/job_01J8ZN2C6WQ4HT/results"
}Read this as a download summary: 3,104 of 4,200 records matched; 786 of those you already hold at their current version, and require: ["email"] rules out a further 1,702 that carry no email. That leaves 616 credits — new 430 records you have never bought, plus updated 186 you own but TOVO has changed since. Those two counts are the web app's “including N new/updated records” line, and their sum is always what gets charged. Drop require and the same run costs 2,318, because every match charges.
Fetch results
Available once status is completed (or cancelled, for the records processed before cancellation). Results are in submission order and each carries its ref. Two formats:
Accept | Returns | Use when |
|---|---|---|
application/json (default) | Cursor-paginated { data, next_cursor, has_more }, up to 100 results per page. | Salesforce and other callers with response-size limits. |
application/x-ndjson | One result per line, the whole job in a single streamed response. | Scripts and data pipelines. |
{
"ref": "003Ab00000Xk1QzIAB",
"match": { "status": "matched", "confidence": 0.94, "matched_on": ["name", "company.domain"] },
"person": { "irn": "PCW00020", "name": "Paul Cheriton Wreford", "contact_available": { "email": true, "phone": false, "linkedin": true } },
"credits": { "charged": 0, "would_charge": 1 }
}Dry-run results carry contact_available and credits.would_charge, never contact values — so you can see per record both what a real run would spend and what it would return. Results are retained for 30 days.
List and cancel
The list is cursor-paginated, newest first, filterable by status. Cancelling a queued or running job stops it at the next record; nothing is charged for records not yet processed, and the records already bought stay bought and readable for free. Cancelling a finished job returns 409.
Credit balance#
Your organisation's current balance, plan, and per-request record caps. Balances are shared across every key and OAuth client in the organisation.
{
"balance": 4996,
"plan": { "name": "Growth", "credits_per_period": 5000, "period": "monthly" },
"period_started_at": "2026-09-01T00:00:00Z",
"renews_at": "2026-10-01T00:00:00Z",
"used_this_period": 4,
"rollover": false,
"limits": {
"max_records_per_batch": 100,
"max_records_per_job": 10000
}
}Credits are consumed in the order they were granted; with rollover: false, unused credits expire at renews_at. Every enrichment response also carries credits.balance, so most integrations only call this endpoint to render a dashboard.
plan.name | Starting credits | Notes |
|---|---|---|
Trial | 100 | Granted on account creation, not renewed. Live keys, real data. |
Growth | Per credits_per_period | Renews on renews_at. |
Enterprise | Per contract | Bounded by the credit balance rather than a download ceiling; limits still applies per request. |
Read limits rather than hard-coding a cap: it is plan-dependent and can change without a version bump. Exceeding either is 413 batch_too_large, with the applicable cap in the problem body — split the work into more requests, or more jobs.
Credit transactions#
The ledger. One entry per credit spent, with enough context to reconcile it to the CRM record that caused it. Cursor-paginated, newest first.
| Parameter | Type | Description |
|---|---|---|
from, to | string | ISO 8601 bounds on occurred_at. |
resource | string | person or company. |
resource_id | string | An IRN or FRN — every charge ever made against one record. |
job_id | string | Charges from one enrichment job. |
ref | string | Charges tagged with one of your own identifiers. |
limit, cursor | See Pagination. |
{
"data": [
{
"id": "txn_01J8ZM1B8XR5TE",
"occurred_at": "2026-09-15T09:12:03Z",
"credits": 1,
"resource": "person",
"resource_id": "PCW00020",
"resource_name": "Paul Cheriton Wreford",
"source": "people.match",
"request_id": "req_01J8ZM0V3KQ9RD",
"job_id": null,
"ref": "003Ab00000Xk1QzIAB",
"key_id": "key_live_4f2a"
}
],
"next_cursor": null,
"has_more": false
}source names the endpoint that spent the credit (people.get, people.match, people.match.batch, enrichment_jobs, and the company equivalents). key_id identifies which key or OAuth client made the call. The ledger is retained indefinitely.
Keeping data fresh#
TOVO does not push changes. Two pull mechanisms keep a CRM in step; a nightly scheduled job using both is the recommended pattern.
A changed record costs a credit to retrieve. Your credit buys the version of the record you were given. When TOVO changes that record — a new email, a new role, a status change — the version you hold is superseded, and retrieving the new one is new information at the usual price. Re-reading a record that has not changed stays free, however often you do it.
Removed records: 410 Gone
When TOVO removes a record — an individual exercising their data-protection rights, a firm merged away, a duplicate collapsed — its fetch endpoint returns 410 permanently, with a tombstone body. This is how a customer holding a copy finds out they must purge it.
{
"type": "https://api.tovodata.co.uk/errors/gone",
"title": "Gone",
"status": 410,
"detail": "This record was removed on 2026-09-01 and will not return.",
"code": "gone",
"request_id": "req_01J8ZP4W7NM2KA",
"removed_at": "2026-09-01T00:00:00Z",
"reason": "data_subject_request",
"superseded_by": null
}reason | Meaning | Action |
|---|---|---|
data_subject_request | The individual asked for their data to be removed. | Delete your copy. Retaining it may breach UK GDPR. |
merged | Duplicate collapsed into another record. | Re-point to superseded_by. What you paid for carries over: the surviving record is charged only if it has changed since you bought the record it absorbed. |
deregistered | Firm dissolved or struck off with no successor. | Mark inactive. |
Tombstones never appear in search results, so a refresh alone will not find them. Once a week, search for your stored identifiers in batches of 1,000 (filters.irn / filters.frn) and treat any identifier missing from the results as removed — then confirm with a fetch, which returns 410. A tombstone never charges, whatever the record's state. The Salesforce package does this on a schedule; a hand-rolled integration should too.
Your account#
Describes the credential making the call. The first request every integration should make: it confirms the key works, which environment it points at, and what it is allowed to do.
{
"organisation": { "id": "org_01H9QK2V8M", "name": "Acme Wealth Ltd" },
"credential": {
"type": "api_key",
"id": "key_live_4f2a",
"environment": "live",
"scopes": ["read", "enrich", "jobs", "credits"],
"created_at": "2026-06-02T14:20:00Z",
"expires_at": null
},
"rate_limit": { "limit": 600, "window_seconds": 60 },
"credits": { "balance": 4996 }
}credential.type is api_key or oauth_client; for a rotated key, expires_at shows when the old value stops working.
Company object#
Returned by every company endpoint. Fields are grouped by source; all are free except the contact block, which is present only on records your organisation owns. Every other field is present in every response unless trimmed with fields; a value TOVO does not hold is null.
Identity
| Field | Type | Description |
|---|---|---|
frn | string | FCA Firm Reference Number. Primary identifier. |
ch_number | string | Companies House number, with leading zeros. |
name | string | Registered name, in its registered casing. |
trading_names | string[] | Trading names on the FCA Register. |
website | string | Full URL with scheme. |
domain | string | Normalised domain, the matching key. |
FCA Register
| Field | Type | Description |
|---|---|---|
fca_status | enum | authorised, registered, eea_authorised, appointed_representative, no_longer_authorised, cancelled. |
fca_status_effective_date | date | When the current status began. |
permissions | string[] | Regulated activities, as snake_case codes. Full list in Search filters. |
client_money_permission | enum | hold_and_control, control_only, none. |
principal_firm | object | For appointed representatives: { frn, name } of the principal. Otherwise null. |
appointed_representatives_count | integer | Current ARs of this firm. |
disciplinary_history | object | { count, latest_at }. |
Companies House
| Field | Type | Description |
|---|---|---|
ch_status | enum | active, dissolved, liquidation, administration, dormant. |
incorporated_at | date | Incorporation date. |
sic_codes | object[] | { code, description }. Code is a string. |
registered_address | object | { line_1, line_2, city, postcode, country }. Country is ISO 3166-1 alpha-2. |
officers_count | integer | Current CH officers. |
Firmographics and financials
| Field | Type | Description |
|---|---|---|
employee_count | integer | Best estimate of headcount. |
employee_band | enum | 1-10, 11-50, 51-200, 201-1000, 1001-10000, 10001+. |
counts | object | { cf_individuals, smf_individuals, directors } — people holding controlled functions, senior management functions, and directorships. |
sector_activities | object[] | Analyst-verified activity taxonomy. Each { path: ["Sector", "Activity"] }, most specific last. |
summary | string | One-paragraph description. |
financials | object | { period_end, currency, revenue, ebitda, net_assets, profit_before_tax } from the latest filed accounts. |
aum | object | { value, currency, as_of }. Assets under management where disclosed. |
financial_health | object | { score, based_on_filing_date }. score: strong, good, fair, weak, distressed, or null when data is insufficient. |
location | object | { city_region, postcode } of the principal place of business. |
Contact and meta
| Field | Type | Description |
|---|---|---|
contact | object | Paid block with phone[] and linkedin[]. Present only on firms you own; contact_available stands in its place elsewhere. See Contact & match blocks. |
purchased_at | timestamp | When your organisation last bought this firm, or null. Later than last_updated_at means the copy you hold is current and free to re-read. |
last_updated_at | timestamp | Last change to any field. Drives updated_since. |
sources | string[] | Which of fca_register, companies_house, tovo contributed. |
Person object#
Returned by every people endpoint and inside People at a company.
Identity and FCA Register
| Field | Type | Description |
|---|---|---|
irn | string | FCA Individual Reference Number. Primary identifier. |
name | string | Full name as registered. |
first_name, last_name | string | Parsed from name. |
avatar_url | string | Profile image where available. |
fca_status | enum | approved, certified, no_longer_approved, prohibited. |
smf_functions | object[] | { code, name } — e.g. { "code": "SMF16", "name": "Compliance Oversight" }. Codes are filterable; names are for display. |
certification_functions | string[] | snake_case codes, e.g. client_dealing, material_risk_taker. |
controlled_functions | string[] | Legacy CF codes for individuals approved before SM&CR. |
disciplinary_history | object | { count, latest_at }. Detail via GET /v1/people/{irn}/disciplinary. |
Employment
| Field | Type | Description |
|---|---|---|
current_roles | object[] | { role, smf_code, frn, firm_name, effective_date }. One entry per current approval; most senior first. |
employment_history | object[] | { frn, firm_name, role, from, to, is_current }. Newest first. to is null while current. |
location | object | { city_region }. |
Contact and meta
| Field | Type | Description |
|---|---|---|
contact | object | Paid block with email[], phone[], linkedin[]. Present only on people you own; contact_available stands in its place elsewhere. |
purchased_at | timestamp | When your organisation last bought this individual, or null. Later than last_updated_at means the copy you hold is current and free to re-read. |
last_updated_at | timestamp | Last change to any field. |
sources | string[] | fca_register, tovo. |
Contact & match blocks#
Contact block
Identical shape on Company (phone, linkedin) and Person (email, phone, linkedin). One shape only — there is no locked variant.
| Field | Type | Present when | Description |
|---|---|---|---|
contact | object | Your organisation owns the record | Absent otherwise: on ambiguous and not_found, on 402, on a match that failed require, and on every search result. |
<field> | object[] | Always, inside contact | Every value held, best first. Empty array when TOVO holds nothing — never null, never absent. |
Availability and ownership, outside the block
Two fields say what a record holds and whether you own it, without exposing values. Both appear wherever contact is withheld, and purchased_at also appears on records you own.
| Field | Type | Where | Description |
|---|---|---|---|
contact_available | object | Search results, 402 bodies, requirements_not_met responses, dry-run job results | { email, phone, linkedin } booleans — whether TOVO holds at least one value for each. Companies omit email. |
purchased_at | timestamp | Everywhere | When your organisation last bought this record; null if never. Compare with last_updated_at: if TOVO has changed the record since, the copy you hold is superseded. |
credits_to_fetch | integer | Search results | 0 or 1 — what retrieving this record right now would cost you. 1 when purchased_at is null (never bought) or older than last_updated_at (bought, then changed). Saves the client comparing timestamps. |
Value objects
| Field | Applies to | Type | Description |
|---|---|---|---|
value | all | string | The email address, E.164 phone number, or full LinkedIn URL. |
type | all | enum | Email: work, personal. Phone: direct, mobile, switchboard. LinkedIn: profile, company. |
confidence | enum | high (verified deliverable), medium (pattern-derived, domain verified), low (pattern-derived only). | |
verified_at | email, phone | date | Last successful verification, or null. |
Match block
Returned by both match endpoints, by batch results, and inside job results.
| Field | Type | Description |
|---|---|---|
status | enum | matched, ambiguous, not_found. |
confidence | number | 0–1. The best candidate's score, whatever the status. |
matched_on | string[] | Input fields that contributed, in order of weight. Company inputs are prefixed company.. |
| Candidate field | Type | Description |
|---|---|---|
irn / frn | string | Identifier — enough to fetch the record directly. |
name | string | Registered name. |
current_role | string | People only. "Role (SMFn)" for display. |
domain | string | Companies only. |
confidence | number | This candidate's score. |
Up to 5 candidates, descending by confidence. Candidates carry neither a contact block nor contact_available — pick one and fetch it.
Job & transaction objects#
Enrichment job
| Field | Type | Description |
|---|---|---|
id | string | job_-prefixed identifier. |
resource | enum | people, companies. |
status | enum | queued, running, completed, failed, cancelled. |
dry_run, min_confidence, require | As submitted. | |
counts | object | { total, processed, matched, ambiguous, not_found, failed, already_current, new, updated, requirements_not_met, purchased, with_email, with_phone, with_linkedin }. failed counts records that errored individually; the job still completes. new (never bought) and updated (bought, then changed by TOVO) are the two chargeable groups; already_current is free. The with_* counts cover the matched set and let you render a download summary from one dry run. |
credits | object | { estimated_max, charged }. estimated_max is null until the job completes; on a dry run it equals new + updated. |
created_at, started_at, completed_at | timestamp | Lifecycle. Later ones null until reached. |
results_url | string | Set on completion. |
error | object | On failed: a problem object explaining why. |
Credit transaction
| Field | Type | Description |
|---|---|---|
id | string | txn_-prefixed. |
occurred_at | timestamp | When the credit was spent. |
credits | integer | Always 1 in v1. Reserved for future pricing. |
resource, resource_id, resource_name | string | What was bought. One entry per record, never per field — so the ledger line count is the credit count. |
source | string | Endpoint that spent it, dotted: people.match, companies.get, enrichment_jobs, … |
request_id | string | The X-Request-Id of the spending call. |
job_id | string | Set when spent by a job. |
ref | string | Your identifier, when the call supplied one. |
key_id | string | Which credential made the call. |
Search filters#
Filters go in the filters object of a search request. Array filters match any listed value. Filters that accept include/exclude take either a bare array (shorthand for include) or { "include": [...], "exclude": [...] }; a value in both is excluded. Range filters take { "min", "max" }, either optional.
Companies
| Filter | Type | Matches |
|---|---|---|
frn | string[] | Exact FRNs, up to 1,000. For refresh syncs. |
fca_status | enum[] | Values as on the Company object. |
permissions | include/exclude | Regulated activity codes: accepting_deposits, advising_on_investments, arranging_deals_in_investments, dealing_in_investments_as_agent, dealing_in_investments_as_principal, managing_investments, insurance_distribution, consumer_credit, mortgage_advice, payment_services, e_money, crypto_asset_registration. |
client_money_permission | enum[] | hold_and_control, control_only, none. |
location | string[] | City/region names, e.g. London, Edinburgh, Midlands. |
employee_band | enum[] | Bands as on the Company object. |
sic_codes | string[] | Exact 5-digit codes. |
sector_activities | include/exclude | Taxonomy paths joined with > , e.g. Lending & Risk > Bank (Retail). A sector alone matches every activity under it. |
financial_health | enum[] | Scores as on the Company object. |
revenue, ebitda, net_assets, aum | range | GBP. |
smf_individuals | range | Count of people holding SMFs. |
authorised_between | range (dates) | On fca_status_effective_date. |
has_disciplinary | boolean | Any disciplinary history. |
has_contact | boolean | At least one contact field available. Search is free; use this to build a list you are willing to pay for, and require to enforce it when you buy. |
fetch_state | enum[] | new (never bought), updated (bought, then changed by TOVO), current (bought and unchanged — free to fetch). ["new","updated"] scopes a list to exactly what a run would charge for. |
is_appointed_representative | boolean | Firm is an AR of a principal. |
People
| Filter | Type | Matches |
|---|---|---|
irn | string[] | Exact IRNs, up to 1,000. |
fca_status | enum[] | Values as on the Person object. |
smf_functions | include/exclude | SMF codes: SMF1 (Chief Executive) … SMF27 (Partner). Codes, not names. |
certification_functions | string[] | Codes as on the Person object. |
location | string[] | City/region names. |
firm_frn | string[] | Currently approved at any of these firms. |
firm_fca_status, firm_employee_band, firm_permissions, firm_sector_activities, firm_net_assets, firm_financial_health | as company | Filter people by attributes of their current firm. Same semantics as the company filter without the prefix. |
role_started_between | range (dates) | On the current role's effective_date. Finds recent movers. |
has_disciplinary | boolean | Any disciplinary history. |
has_contact | boolean | At least one contact field available. |
has_email | boolean | An email is available. The list-building half of require: ["email"]. |
fetch_state | enum[] | new, updated, current — as for companies. Pair with updated_since to find people whose details have moved since you bought them. |
Error codes#
Every code the API can return, with its status. type is always https://api.tovodata.co.uk/errors/<code>.
| Status | code | Meaning | Retry? |
|---|---|---|---|
400 | invalid_request | Malformed JSON, unknown parameter, or wrong content type. | No — fix the request. |
401 | unauthorized | Missing, malformed, expired, or revoked credential. | No — check the key or refresh the token. |
402 | insufficient_credits | The call would charge and the balance is zero. Body includes the matched record without its contact values. | After topping up. |
403 | insufficient_scope | Credential lacks the scope for this endpoint. | No. |
403 | environment_mismatch | A test key was used against a live-only feature, or vice versa. | No. |
404 | not_found | No record, job, or transaction with that identifier. | No. |
409 | idempotency_conflict | An Idempotency-Key was reused with a different body. | No — use a new key. |
409 | job_not_cancellable | Job has already finished. | No. |
410 | gone | Record permanently removed. Body includes reason. | No — purge or re-point. |
413 | batch_too_large | More records than your plan's limits allow — by default 100 in a batch, 10,000 in a job. Body carries limit. | Split it. |
422 | validation_failed | Body parsed but a field is invalid. errors[] lists each one. | No — fix the fields. |
429 | rate_limited | Over the per-minute limit. | After Retry-After. |
500 | internal_error | TOVO fault. Quote request_id to support. | Once, with backoff. |
503 | unavailable | Planned maintenance or overload. | After Retry-After. |