From kausate-integration
Integrate the Kausate company-data API (KYB / business registries / shareholder graphs / UBO / documents). Generate a typed client from the live OpenAPI spec, set up async + webhook handling, use customerId and customerReference correctly, fall back to polling only when webhooks aren't possible. Use this skill whenever the user asks to integrate Kausate, fetch company data, do KYB, look up companies in business registries, retrieve shareholder graphs / UBO / company reports / annual accounts, or wire up Kausate webhooks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/kausate-integration:kausate-integrationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Kausate is a real-time KYB / company-data API. It pulls live data from official business registries across 50+ jurisdictions (DE Handelsregister, GB Companies House, FR INPI, NL KVK, etc.). Most data-retrieval endpoints are **async** because government registries are slow and unreliable — your integration must handle this correctly to be production-ready.
Kausate is a real-time KYB / company-data API. It pulls live data from official business registries across 50+ jurisdictions (DE Handelsregister, GB Companies House, FR INPI, NL KVK, etc.). Most data-retrieval endpoints are async because government registries are slow and unreliable — your integration must handle this correctly to be production-ready.
This skill encodes the production-grade patterns. Follow it in order. If anything here disagrees with https://api.kausate.com/openapi.json, the OpenAPI spec wins — it's generated from the live code.
All requests use an API key sent via the X-API-Key header:
curl -H "X-API-Key: $KAUSATE_API_KEY" https://api.kausate.com/v2/...
.env* to .gitignore.X-API-Key only — no OAuth, no JWT for the public API.openapi.jsonKausate publishes its OpenAPI spec at https://api.kausate.com/openapi.json. The endpoint requires the X-API-Key header (it isn't anonymous):
curl -H "X-API-Key: $KAUSATE_API_KEY" \
https://api.kausate.com/openapi.json \
-o openapi.json
Then generate a typed client. Pick the matching ecosystem:
TypeScript — types only:
npx openapi-typescript openapi.json -o src/lib/kausate.d.ts
TypeScript — runtime client (recommended):
npm install openapi-fetch
import createClient from "openapi-fetch";
import type { paths } from "./lib/kausate";
export const kausate = createClient<paths>({
baseUrl: "https://api.kausate.com",
headers: {
"X-API-Key": process.env.KAUSATE_API_KEY!,
"Kausate-Version": "2026-05-01",
},
});
Python:
uvx --from openapi-python-client openapi-python-client generate --path openapi.json
Go:
go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest \
-package kausate -generate types,client openapi.json > kausate.gen.go
Java / Kotlin:
npx @openapitools/openapi-generator-cli generate \
-i openapi.json -g java -o src/main/java/kausate
Kausate-VersionKausate uses date-based API versioning (Cadwyn). Pin a version on every request via the Kausate-Version header so request/response shapes don't drift under you:
Kausate-Version: 2026-05-01
If you don't pin, you get the org's default version (or the latest if no default is set), and breaking changes can land during a major-release window. Always pin in production. Read the info block of openapi.json for the version list.
Important: Different API versions expose different paths. In
2025-04-01the report endpoint wasPOST /v2/companies/{kausateId}/report; in2026-05-01it'sPOST /v2/companies/reportwithkausateIdin the body. Always match your path style to the version you've pinned. Generating the client fromopenapi.jsonwhile sending the matchingKausate-Versionheader keeps these in sync automatically.
2026-05-01)The full source of truth is openapi.json; this is the map an integrator usually needs.
| Endpoint | Purpose | Mode | Latency |
|---|---|---|---|
GET /v2/companies/search/autocomplete | Type-ahead during form input | sync | < 100 ms |
POST /v2/companies/search | Live registry search by name / number / advanced query | async by default, sync available at /search/sync | 2–10 s live |
GET /v2/companies/search/person | Find companies by legal-rep name + DOB | sync | 1–5 s |
POST /v2/companies/prefill | Form-prefill from a known kausateId. Index hit + live fallback. Best onboarding UX. | always sync | < 200 ms typical |
Search request body fields: companyName, companyNumber, advancedQuery (jurisdiction-specific structured query), jurisdictionCode, customerReference. Use advancedQuery when you have native registry numbers — it avoids fuzzy matching.
kausateIdprefill and every data endpoint require a kausateId in the body. They do not accept companyNumber or jurisdictionCode — submitting those fields fails 422 (extra_forbidden). To go from a native ID (KVK number, CRN, SIREN, HRB, etc.) to a kausateId, use search:
# Step 1 — translate native id → kausateId via search
curl -X POST https://api.kausate.com/v2/companies/search/sync \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{"companyNumber":"33255959","jurisdictionCode":"nl"}'
# → result.searchResults[0].kausateId
# Step 2 — use that kausateId everywhere else
curl -X POST https://api.kausate.com/v2/companies/prefill \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{"kausateId":"co_nl_..."}'
Cache the (native_id, kausateId) pair on your side. The mapping is stable, and re-running search on every call wastes credits.
| Endpoint | Purpose |
|---|---|
POST /v2/companies/report | Full company report from the registry (legal name, addresses, identifiers, capital, legal reps, shareholders) |
POST /v2/companies/finance | Financials / annual accounts when available |
POST /v2/companies/ubo | Ultimate Beneficial Owners |
POST /v2/companies/shareholder-graph | Multi-level ownership graph. Always async. Configurable maxDepth (1–7), enriched (governance enrichment), retrievalTimeout (minutes) |
POST /v2/companies/documents/list | List the documents available for a company |
POST /v2/companies/documents | Retrieve a specific document (binary served via pre-signed URL in the result) |
All of the above accept the same standard body fields:
kausateId (required) — Kausate's company identifier (co_{jurisdiction}_{id})customerReference (optional) — your per-request correlation id (see §6)bypassCache (optional, where supported) — skip same-day dedup, force a fresh registry callmaxDepth on shareholder-graph)For each async endpoint there's a matching sync variant at /{family}/sync and a polling endpoint at GET /v2/companies/{family}/{orderId} (see §5).
| Endpoint | Purpose |
|---|---|
POST /v2/webhooks / GET / PUT /{id} / DELETE /{id} | Manage webhook subscriptions |
POST /v2/monitors / GET / GET /{id} / DELETE /{id} | Monitor a company on a cron schedule, fire webhook on detected changes |
GET /v2/analytics/summary | Usage / cost rollup, filterable by date range, workflowType, sku, customerId |
GET /v2/analytics/timeseries | Same metrics over time |
GET /v2/analytics/breakdowns | Pivot by jurisdiction, sku, customerId |
GET /v2/platform/jurisdictions | Capability matrix per jurisdiction (which products are supported, what document types exist) |
Most data endpoints are async because government registries time out, rate-limit, and have weekend/maintenance outages. The async flow:
POSTs to a data endpoint with kausateId in the body. The API returns {orderId, status: "running"} immediately.POSTs the full result to your registered webhook URL. If you don't have a webhook, you poll (§5).Hard rule for production: webhooks first. Polling is a fallback only. Sync mode is for prototypes.
curl -X POST https://api.kausate.com/v2/webhooks \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{
"name": "Order completions",
"url": "https://your-app.example.com/webhooks/kausate",
"customHeaders": {
"Authorization": "Bearer <your-shared-secret>"
}
}'
Notes:
result.type in your handler:result.type | Comes from |
|---|---|
liveSearch | POST /v2/companies/search |
companyReport | POST /v2/companies/report |
financials | POST /v2/companies/finance |
uboReport | POST /v2/companies/ubo |
shareholderGraph | POST /v2/companies/shareholder-graph |
listDocuments | POST /v2/companies/documents/list |
document | POST /v2/companies/documents |
person | GET /v2/companies/search/person (rare in async path) |
A typical receiver pattern:
switch (payload.result?.type) {
case "companyReport": handleReport(payload); break;
case "uboReport": handleUbo(payload); break;
case "shareholderGraph":handleGraph(payload); break;
case "listDocuments": handleDocList(payload); break;
case "document": handleDocument(payload); break;
// ...
default:
// null result = error path; check payload.status / payload.error
}
If payload.result is null, the order didn't reach a successful state — branch on payload.status (see §7) and use payload.error for the user-visible message.
ACTIVE webhooks for the org receive every event. Multi-tenant routing happens in your handler — typically by inspecting customerReference or customerId.apiVersion field on the subscription body is deprecated. Webhook payload shape is determined by the Kausate-Version header that was on the original order request, falling back to your org's default. Pin per-request, not per-subscription.customHeaders are stored encrypted server-side and replayed on every delivery. Six headers are protected and cannot be overridden: content-type, host, content-length, transfer-encoding, connection, kausate-version.The subscription returns an id (UUID), status (active | inactive), and url. Persist the id — you'll need it to update or delete the subscription.
# List
curl -H "X-API-Key: $KAUSATE_API_KEY" -H "Kausate-Version: 2026-05-01" \
https://api.kausate.com/v2/webhooks
# Get one
curl -H "X-API-Key: $KAUSATE_API_KEY" -H "Kausate-Version: 2026-05-01" \
https://api.kausate.com/v2/webhooks/$WEBHOOK_ID
# Pause without deleting (status: inactive — no deliveries until you re-activate)
curl -X PUT -H "X-API-Key: $KAUSATE_API_KEY" -H "Kausate-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{"status":"inactive"}' \
https://api.kausate.com/v2/webhooks/$WEBHOOK_ID
# Delete (returns 204)
curl -X DELETE -H "X-API-Key: $KAUSATE_API_KEY" -H "Kausate-Version: 2026-05-01" \
https://api.kausate.com/v2/webhooks/$WEBHOOK_ID
Pausing is safer than deleting if you're rotating receivers — pause the old one, register the new one, verify it works, then delete the old one. Orders placed during a fully-paused window will not redeliver when you reactivate (no backfill — see §4).
Path style is 2026-05-01: kausateId in the body, not the URL.
curl -X POST https://api.kausate.com/v2/companies/report \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-H "X-Customer-Id: end-customer-789" \
-H "Content-Type: application/json" \
-d '{
"kausateId": "co_de_7KGHtucR88u2omSx3KhaoH",
"customerReference": "kyc-case-12345"
}'
Immediate response:
{
"orderId": "ord_xyz789",
"customerReference": "kyc-case-12345",
"customerId": "end-customer-789",
"status": "running"
}
Set up an HTTPS endpoint on your side that accepts POST requests. It must:
Authorization (or whatever) header you registered.orderId. Kausate retries up to 50 times with exponential backoff (1 s initial, 2× multiplier, 4 h max interval). Same-day dedup can also produce duplicate deliveries.completed. See §7.Kausate sends only two headers beyond your custom ones:
Content-Type: application/jsonKausate-Version: <delivered_version> — the API version this payload was migrated to (matches the order's request version)There is no Kausate-supplied delivery-id, request-id, or signature header. Your auth + idempotency must be handled via your custom headers + the payload's orderId.
ExecutionResponse){
"orderId": "ord_xyz789",
"kausateId": "co_de_7KGHtucR88u2omSx3KhaoH",
"customerReference": "kyc-case-12345",
"customerId": "end-customer-789",
"status": "completed",
"requestTime": "2026-04-29T10:30:00Z",
"responseTime": "2026-04-29T10:30:08Z",
"result": { "type": "companyReport", "...": "product-specific" },
"error": null,
"currentActivity": null
}
Match deliveries back to your originating request via orderId (always present) or customerReference (your value, always echoed back when supplied).
If you place an order, then add a webhook subscription, the result is not delivered retroactively. The notification fires once at workflow completion. Subscribe webhooks during onboarding, not after orders go out. For orders that fired before a subscription existed, poll with GET /v2/companies/{family}/{orderId}.
Use polling only when you can't accept inbound webhooks (no public HTTPS endpoint, restricted networks, dev / test).
curl https://api.kausate.com/v2/companies/report/$ORDER_ID \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01"
Polling endpoints follow GET /v2/companies/{family}/{orderId} — report, documents, documents/list, finance, ubo, shareholder-graph, search. The response shape is the same ExecutionResponse as the webhook payload.
Recommended schedule — exponential backoff with a hard cap:
running, surface that to the user and let them retry asynchronously rather than blocking forever.Belt-and-braces: when possible run BOTH a webhook AND a periodic reconciliation job that polls any orders still running past their expected window. That way an occasional missed webhook delivery doesn't strand orders.
customerReference vs customerId — use both, deliberatelyBoth are optional, both are URL-safe strings ([a-zA-Z0-9_-]), both capped at 150 characters. They have distinct purposes and they're sent at different layers.
| Field | What it is | Typical value | Set per | How to send |
|---|---|---|---|---|
customerReference | Per-request correlation id you generate | KYC case id, order id from your system | request | JSON body field |
customerId | Persistent identifier for your end customer | the user / tenant / account in your product | end customer | X-Customer-Id header |
customerId is a header, not a body field. Sending it in the body fails Pydantic validation because the request models are extra=forbid. The header makes it easy for a thin gateway to inject a per-end-customer id without rewriting bodies.
Both round-trip: they appear in the immediate ProductOrderResponse, in the polling response, and in the webhook payload. Use them like this:
customerReference — set per request. Lets you correlate a webhook delivery back to the originating call. Always set this in production.customerId — set per end customer. Lets you partition usage and cost in /v2/analytics/breakdowns?groupBy=customerId, and is the primary key your handler uses to route results to the right tenant / user.Async runs finish in one of six states. Treat anything other than completed as not-success.
| Status | Meaning | Action |
|---|---|---|
running | Still processing | Wait for webhook or keep polling |
completed | Success — result is populated | Use it |
failed | Permanent failure — error populated | Surface to the user; retry with bypassCache: true if appropriate (registry was down, etc.) |
canceled | Order canceled and acknowledged | Treat as not-found from your side |
terminated | Forcefully terminated (rare) | Same as failed |
timedOut | Reached its time limit (workflow-level 7 days; can hit registry-side limits earlier) | Retry — registries can be slow; the next attempt may succeed |
Don't write code that only checks status === "completed". Make the non-completed branch explicit.
bypassCacheIf you place the same request twice on the same UTC calendar day (same kausateId, same workflow, same customerReference, same customerId), Kausate returns the existing orderId instead of starting a new workflow. This protects against duplicate billing on retries.
If you genuinely need a fresh registry call (e.g. data was missing, registry was down on the first try), set bypassCache: true on the request body. This skips the dedup key, starts a new workflow, and consumes new credits.
bypassCache does not bypass any internal result cache — only the same-day dedup. The result you get back is always live from the registry.
Document retrieval is two async orders, in this order:
List documents available for a company:
curl -X POST https://api.kausate.com/v2/companies/documents/list \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-d '{"kausateId":"co_de_...","customerReference":"docs-list-1"}'
On completion, result has shape (type listDocuments):
{
"type": "listDocuments",
"kausateId": "co_de_...",
"documents": [
{
"kausateDocumentId": "doc_...",
"title": "Aktueller Abdruck",
"documentType": "currentExtract",
"publicationType": null,
"publicationDate": "2024-03-15",
"fileType": "pdf",
"source": "de-handelsregister"
}
],
"totalCount": 1,
"indexedAt": "2024-...",
"expiresAt": "2024-..."
}
documentType values include annualAccounts, currentExtract, articlesOfAssociation, shareholderList, chronologicalExtract, officialFilings, registrationDetails, beneficialOwnersDetails, annualReturn. documentType/publicationDate are sometimes null; filter accordingly.
Retrieve a specific document by kausateDocumentId:
curl -X POST https://api.kausate.com/v2/companies/documents \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-d '{
"kausateId":"co_de_...",
"kausateDocumentId":"<from-list-response>",
"customerReference":"docs-fetch-1"
}'
On completion, result has shape (type document):
{
"type": "document",
"kausateDocumentId": "doc_...",
"documentType": "currentExtract",
"downloadLink": "https://kausate-...s3.../...?X-Amz-Signature=...",
"expiresAt": "2024-...",
"contentType": "application/pdf",
"fileName": "current-extract.pdf"
}
downloadLink is a pre-signed S3-style URL with an expiresAt timestamp. Webhook payloads carry the URL, not the bytes. Fetch the bytes from downloadLink directly (no X-API-Key needed on that fetch — the URL is signed) before expiresAt.
Create a monitor with a cron schedule. Kausate runs the equivalent of a company-report fetch on that schedule, and when it detects a change vs the previous state, it fires a webhook.
curl -X POST https://api.kausate.com/v2/monitors \
-H "X-API-Key: $KAUSATE_API_KEY" \
-H "Kausate-Version: 2026-05-01" \
-d '{
"kausateId":"co_de_...",
"scheduleCron":"0 9 * * 1"
}'
scheduleCron is standard 5-field cron, validated server-side. UTC.monitorId, companyName, isActive, checkCount, lastCheckedAt.changedCategories list (legal_name, addresses, shareholders, etc.).Kausate normalizes companies to kausateId = co_{jurisdiction}_{opaque}. When searching, you can pass native registry identifiers via companyNumber or advancedQuery. The major formats:
| Jurisdiction | Native identifier | Format / example |
|---|---|---|
de | Court-coded register number | R2201_HRB 31248 (Cologne court HRB) — court code + register type + number |
gb | Companies House CRN | 8 chars: 00445790 or MC123456 |
fr | SIREN / SIRET | 9-digit (SIREN) parent, 14-digit (SIRET) establishment |
nl | KVK number | 8-digit |
at | Firmenbuch FNR | digits + lowercase letter (123456a) |
it | REA | regional code + number (MI-1234567); also Codice Fiscale (16) and P.IVA (11) |
ch | UID | CHE + 9 digits |
be | Enterprise number | 10 digits, optional dots: 1234.567.890 |
pl | KRS / NIP / REGON | 10-digit (KRS) / 10-digit (NIP) / 9 or 14-digit (REGON) |
es | NIF / CIF | letter + 7 digits + check digit |
se | Organisationsnummer | 10 or 12 digits |
no | Organisasjonsnummer | 9 digits |
fi | Y-tunnus | 7 digits + - + check digit |
For everything else and for full coverage, call GET /v2/platform/jurisdictions to discover supported jurisdictions and their capabilities at runtime.
Status codes returned by the public API:
| Code | Meaning | What to do |
|---|---|---|
200 | Success — for async endpoints, check status in body | Branch on status |
201 | Created (monitor / webhook) | Persist returned id |
204 | Deleted | Resource is gone |
400 | Invalid input (malformed kausateId, invalid cron, unsupported jurisdiction) | Validate against the OpenAPI spec; check detail |
401 | Missing / invalid X-API-Key | Verify env var; rotate if leaked |
402 | Payment required (credits exhausted) | Top up; check /v2/analytics/summary |
403 | Forbidden (insufficient permissions, or feature not enabled for org) | Contact sales for premium features |
404 | Company / order / monitor / webhook not found | Verify the id; for orders, dedup may have returned an existing one |
408 | Sync request timed out (300 s hard limit) | Switch to async + webhooks |
422 | Pydantic validation failure (type mismatch, missing required field, body field that should be a header — e.g. customerId in body instead of X-Customer-Id) | Check the JSON-schema error in detail |
429 | Rate limited (gateway-level; not surfaced via headers) | Back off; rate limits are per-org |
500/502/503/504 | Server / registry-side error | Retry with exponential backoff; do not retry 4xx |
Error response shape:
{ "detail": "human-readable error or pydantic error list" }
Always retry 5xx with backoff. Never retry 4xx automatically — they're caused by your request and won't change without a fix. Log every orderId you place and every webhook delivery for tracing.
/sync endpoints) in production. Hard 300 s timeout. Government registries are unreliable; you'll see HTTP 408 even when the data would eventually be available. Use sync only for tests and prototypes.completed as the only outcome. Every closed status (failed, canceled, terminated, timedOut) needs handling.KAUSATE_API_BASE_URL so dev / staging / prod can be swapped via env..env.Kausate-Version. You will get bitten when the latest version moves on.orderId.Kausate-Versions. With 2026-05-01 use POST /v2/companies/report (kausateId in body). With 2025-04-01 use POST /v2/companies/{kausateId}/report. Don't mix.customerId in the request body. It's X-Customer-Id header. Body submission returns 422.prefill (or any data endpoint) with companyNumber / jurisdictionCode instead of kausateId. Returns 422. Translate native registry IDs to kausateId via search first, then cache the mapping.customerReference longer than 150 chars or with non URL-safe characters. Will 422.Before you ship:
Kausate-Version pinned to a specific date on every request.orderId.status values handled distinctly.running past their expected window.customerReference set per request, customerId set per end customer (header).orderId on every request and every webhook delivery for tracing./v2/analytics/summary or the dashboard — every call costs credits.https://api.kausate.com/openapi.json.md to any docs URL — e.g. https://docs.kausate.com/api-reference/async-sync.mdIf anything in this skill conflicts with openapi.json, the OpenAPI spec wins.
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub kausate/agents --plugin kausate-integration