Data API
Developer documentation
Base URL, keys, endpoints, examples and errors for the ContactPit Data API.
Getting access
Access is set up by our team. It usually takes one short conversation.
- Contact us. Tell us what you are building, which countries you need and roughly how many companies you will read.
- Agree the terms. We agree the datasets, the request limit and how the data may be used and stored.
- Get your key. We send a server key with the scopes and registers you need. Keys have an expiry date and can be rotated at any time.
- Build and go live. Start with the capabilities call, then follow the examples below.
Quickstart
All endpoints live under one base URL. Send JSON, receive JSON. Start by asking what your key can use.
- Base URL
https://api.contactpit.com/api/v2/partner- Format
- JSON over HTTPS, UTF-8
- Contract
2.0-draft
curl https://api.contactpit.com/api/v2/partner/capabilities \
-H "Authorization: Bearer $CONTACTPIT_API_KEY"
{
"data": {
"registers": [
{ "country": "EE", "register": "ariregister", "domestic": true },
{ "country": "LV", "register": "lv_enterprise_register", "domestic": false }
],
"features": {
"companies": true, "search": true, "domain_candidates": true,
"changes": true, "signals": false, "contacts": false, "research": false
},
"limits": {
"page_size": 50, "company_batch_size": 1, "requests_per_minute": 120,
"cursor_ttl_seconds": 86400, "journal_retention_seconds": null
},
"signal_types": [],
"data_use_agreement_ref": "your-policy-id"
},
"meta": { "contract_version": "2.0-draft", "generation_id": "…", "published_at": "…", "request_id": "…", "rights_policy_id": "your-policy-id" }
}
Show features in your product only when capabilities says they are on. Limits that are null are not set for your key yet.
Authentication
Send your key in the Authorization header as a bearer token on every request. Keys are 32 to 256 URL-safe characters.
Authorization: Bearer <your-api-key>
- Call the API from your server only. Never put the key in a browser, mobile app or public repository.
- Each key carries scopes (for example companies:read or financials:read) and register grants (EE, LV). A call outside them returns 403.
- Keys expire. Ask us for a new key before the old one runs out; both can work side by side while you switch.
Responses
Every successful answer has a data object and a meta object. meta tells you which data snapshot answered and carries a request ID for support.
{
"data": { … },
"meta": {
"contract_version": "2.0-draft",
"generation_id": "partner-snapshot-demo-7",
"published_at": "2026-10-27T10:00:00Z",
"request_id": "8a6f1c2e-…",
"rights_policy_id": "your-policy-id"
}
}
Responses are sent with Cache-Control: no-store and an X-Request-ID header. Log it: it is the fastest way for us to find a request.
Endpoints
Paths are relative to the base URL. The scope column is what your key needs.
| Method | Path | Purpose | Scope |
|---|---|---|---|
| GET | /capabilities | What your key can use | capabilities:read |
| GET | /companies/{company_id} | One company by ContactPit ID | companies:read |
| POST | /companies/resolve | Match a registry code or a name | companies:read |
| POST | /companies/domain-candidates | Companies that list a website domain | companies:read |
| GET | /companies/filter-capabilities | Filters available for one register | companies:read |
| POST | /companies/search | Filtered company search, paged | companies:read |
| GET | /changes | Change feed after a search | companies:read |
| GET | /companies/{company_id}/financials | Financial history, summary or headlines | financials:read |
| GET | /companies/{company_id}/legal-roles | Registry role records | legal_roles:read |
| GET | /companies/{company_id}/ownership | Shareholders and beneficial owners | ownership:read |
| GET | /companies/{company_id}/holdings | Companies this company holds shares in | ownership:read |
| GET | /companies/{company_id}/officers | Board, council and other officers | officers:read |
| GET | /companies/{company_id}/vat | VAT registration status | vat:read |
| GET | /companies/{company_id}/filings | Annual report years | filings:read |
| GET | /companies/{company_id}/business-profile | Business description from the website | business_profile:read |
| POST | /signals/query | Signals for up to 50 companies | signals:read |
Contacts, research and construction project endpoints exist for pilot partners and are switched on per key. Capabilities shows whether they are on for you. See Company research and Construction projects.
Company record
company_id is ContactPit's permanent company ID (a UUID). It stays the same when a company changes its name or address, so store it as your key.
curl https://api.contactpit.com/api/v2/partner/companies/11111111-1111-4111-8111-111111111111 \
-H "Authorization: Bearer $CONTACTPIT_API_KEY"
{
"data": {
"company_id": "11111111-1111-4111-8111-111111111111",
"country": "EE",
"register": "ariregister",
"registry_code": "00123456",
"revision": "company-r7",
"entity_version": 7,
"updated_at": "2026-10-27T10:00:00Z",
"legal_name": "Example Demonstration OÜ",
"status": "active",
"deletion_date": null,
"legal_form": "OÜ",
"primary_activity": { "classification": "EMTAK", "version": "2025", "code": "62011" },
"employees": { "kind": "period_average", "value": "4.5",
"period": { "start": "2025-01-01", "end": "2025-12-31" } },
"website": { "listed_url": "https://example.com", "available": true },
"linkedin": { "listed_url": "https://www.linkedin.com/company/example", "available": true },
"location": { "city": "Tallinn", "region_code": null, "region_name": "Harju", "region_version": null },
"sources": [
{ "source_id": "demo-register", "dataset": "synthetic_company_facts", "observed_at": "2026-10-27T10:00:00Z" }
],
"availability": {
"legal_name": { "state": "available", "reason": null, "source_refs": ["demo-register"] },
"deletion_date": { "state": "not_applicable", "reason": "Company is active", "source_refs": [] },
"…": "one entry per field"
}
},
"meta": { … }
}
The example is synthetic. Status is one of active, deleted, in_liquidation or unknown. Numbers such as employees are exact decimal strings.
Search and matching
Match by registry code
The code is matched exactly as text, including leading zeros. The answer is matched, unknown or unsupported_country.
POST /companies/resolve
{ "kind": "registry_code", "country": "EE", "register": "ariregister", "registry_code": "00123456" }
→ { "data": { "status": "matched", "items": [ { …company… } ], "next_cursor": null }, "meta": { … } }
Send "kind": "name" to get name candidates instead. Use /companies/domain-candidates with a website domain to get companies that list it. Candidates are suggestions with a reason, not confirmed matches.
Filtered search
Ask /companies/filter-capabilities?country=EE®ister=ariregister which filters a register supports, then search. A filter that is not listed returns 422 unsupported_filter.
POST /companies/search
{
"filters": {
"country": "EE",
"register": "ariregister",
"status": "active",
"activity": { "classification": "EMTAK", "version": "2025", "codes": ["62011", "62021"] },
"employees": { "measurement": "period_average", "minimum": "10", "maximum": "50" }
},
"limit": 50
}
→ {
"data": {
"items": [ { …company… } ],
"next_cursor": "signed-page-cursor",
"sync_cursor": "signed-change-feed-cursor"
},
"meta": { … }
}
Pages hold up to 50 companies. Send next_cursor back with the same filters to read the next page. Cursors are signed, tied to your key and expire.
Keeping a copy fresh
Keep the sync_cursor from the first page of a search. After the last page, read /changes with it and store the new next_cursor after you apply each page.
GET /changes?cursor=signed-change-feed-cursor&limit=50
→ {
"data": {
"items": [
{
"event_id": "22222222-2222-4222-8222-222222222222",
"type": "company_updated",
"company_id": "11111111-1111-4111-8111-111111111111",
"revision": "company-r7",
"entity_version": 7,
"replacement_ids": [],
"occurred_at": "2026-10-27T10:00:00Z",
"revocation": null
}
],
"next_cursor": "signed-change-feed-next",
"has_more": false
},
"meta": { … }
}
- Skip an event you have already applied (same event_id) and any event whose entity_version is not newer than the one you hold.
- Ignore event types you do not know. New types can be added without a new contract version.
- On rights_revoked, remove the ContactPit fields it names. Keep your own notes.
- On 410 cursor_expired, start a new search snapshot and replace the ContactPit fields in your copy.
- The feed is read by polling. Every few minutes is plenty.
Extra datasets
Extra datasets are separate reads by company ID. Each needs its own scope and a dataset agreement on our side. Until both are in place the endpoint returns 501 capability_unavailable or 403 scope_denied.
GET /companies/{company_id}/financials?section=history # or summary, document_headlines
GET /companies/{company_id}/ownership?limit=20¤t_only=true
GET /companies/{company_id}/officers?limit=20
GET /companies/{company_id}/vat
GET /companies/{company_id}/filings
GET /companies/{company_id}/business-profile
POST /signals/query
{ "company_ids": ["11111111-1111-4111-8111-111111111111"], "types": ["liquidation_started", "merger"], "since": "2026-01-01", "limit": 50 }
- Financial amounts are exact decimal strings with their currency, period and scope. We do no currency conversion or growth maths for you.
- Role and ownership records keep the register's own meaning. People are never identified by personal ID code; company holders link to their ContactPit ID.
- Signal types: liquidation_started, bankruptcy_started, company_deleted, name_changed, capital_changed, merger, division, conversion, pledge_registered, pledge_ended, licence_granted, licence_ended, vat_registered, vat_deregistered, tax_debt_appeared, tax_debt_cleared and headcount_changed. A corrected signal comes back as retracted.
Errors and limits
Errors use one shape. retryable tells you whether trying again later can help.
{
"error": {
"code": "rate_limited",
"message": "rate limited",
"request_id": "8a6f1c2e-…",
"retryable": true
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request body or parameters are not valid |
| 401 | unauthorized | Missing, unknown or expired key |
| 403 | scope_denied | Your key lacks the scope, register or dataset |
| 404 | not_found | No such company in your registers |
| 410 | cursor_expired | The cursor is too old; restart the paged read |
| 422 | unsupported_filter | The register does not support this filter |
| 429 | rate_limited | Too many requests; wait for Retry-After seconds |
| 501 | capability_unavailable | The feature is not switched on for your key |
| 503 | temporarily_unavailable | Try again in 5 seconds |
- The default limit is 120 requests a minute per customer. Your limit is in capabilities.
- On 429 wait the number of seconds in the Retry-After header. On 503 wait 5 seconds.
- Request bodies can be at most 16 KB.
Data principles
- Every fact carries its source and the date we observed it. Source dates are not live checks.
- A missing value always comes with a reason. Unknown is never turned into false, zero or an empty list.
- Registry codes are kept as exact text. Matching is by code; names and websites only give candidates.
- Personal ID codes are never returned. Business contacts are only shared under a data-use agreement, and opt-outs reach your copy through the change feed.
Ready to build?
Tell us what you need and we will set up a key for it.