This is the full developer documentation for BizVerify Docs # BizVerify Documentation > Verify business entities across 50 US states and international jurisdictions via REST API or AI-native MCP tools. ## Choose your path [Section titled “Choose your path”](#choose-your-path) [REST API ](/quickstart/)Integrate BizVerify into your application with the REST API. [MCP (AI Tools) ](/mcp/setup/)Use BizVerify as an MCP tool in Claude, Cursor, VS Code, and more. [Node.js SDK ](/sdks/node/)Install the npm package and start verifying in minutes. [Python SDK ](/sdks/python/)Async-ready Python client with full type support. [Go SDK ](/sdks/go/)Zero-dependency Go client with context support and iterators. # Authentication > How to authenticate with the BizVerify API using API keys, manage keys, and understand rate limits and credits. ## Getting an API key [Section titled “Getting an API key”](#getting-an-api-key) BizVerify uses a passwordless flow. No password needed — just your email. ### 1. Request access [Section titled “1. Request access”](#1-request-access) ```bash curl -X POST https://api.bizverify.co/v1/auth/request-access \ -H "Content-Type: application/json" \ -d '{"email": "you@company.com", "accept_terms": true}' ``` A 6-digit verification code is sent to your email (valid for 15 minutes). If the email is new, an account is created with 50 free credits. ### 2. Verify and get your key [Section titled “2. Verify and get your key”](#2-verify-and-get-your-key) ```bash curl -X POST https://api.bizverify.co/v1/auth/verify-access \ -H "Content-Type: application/json" \ -d '{"email": "you@company.com", "code": "123456", "label": "production"}' ``` The `label` is optional — if omitted, a label like `key-1712000000` is generated. You can create multiple keys with different labels. Caution The full API key is returned only once. Store it securely. ## Using your API key [Section titled “Using your API key”](#using-your-api-key) Include the key in the `X-API-Key` header on every request: ```bash curl https://api.bizverify.co/v1/account \ -H "X-API-Key: bv_live_abc123..." ``` ## Key management [Section titled “Key management”](#key-management) ### Create additional keys [Section titled “Create additional keys”](#create-additional-keys) ```bash curl -X POST https://api.bizverify.co/v1/account/keys \ -H "Content-Type: application/json" \ -H "X-API-Key: bv_live_abc123..." \ -d '{"label": "CI/CD"}' ``` ### View your keys [Section titled “View your keys”](#view-your-keys) Your API keys are included in the account response: ```bash curl https://api.bizverify.co/v1/account \ -H "X-API-Key: bv_live_abc123..." ``` The `api_keys` array in the response shows each key’s `id`, `label`, `prefix` (first 8 chars), `rate_limit`, `is_active`, and `created_at`. ### Revoke a key [Section titled “Revoke a key”](#revoke-a-key) ```bash curl -X DELETE https://api.bizverify.co/v1/account/keys/key_xyz \ -H "X-API-Key: bv_live_abc123..." ``` Note You cannot revoke the API key you’re currently using to authenticate. Use a different key to revoke it. ## Credits [Section titled “Credits”](#credits) See [Credits & Billing](/guides/billing/) for credit costs, balance tracking, and purchasing. ## Rate limits [Section titled “Rate limits”](#rate-limits) API key requests are rate-limited to **60 requests/minute** by default. See [Rate Limits](/guides/rate-limits/) for headers, error handling, and best practices. # Changelog > A log of API changes, new features, and deprecations. Notable changes to the BizVerify API. For the machine-readable API schema, see the [OpenAPI spec](https://api.bizverify.co/v1/openapi.json). *** ## 2026-08-13 [Section titled “2026-08-13”](#2026-08-13) ### Changed [Section titled “Changed”](#changed) * `POST /v1/checker` (the free, no-signup business checker) now enforces a small shared capacity ceiling and a per-IP daily limit, alongside the existing per-IP per-minute rate limit. When free checking is momentarily at capacity, the endpoint returns HTTP `429` with a new error code — `FREE_TIER_CAPACITY` — and a `Retry-After` header of a few seconds; this is a transient free-tier state, not an API outage and not a caller error. Exceeding the per-minute or per-day IP limit continues to return `429 RATE_LIMIT_EXCEEDED`, which now also carries a `Retry-After` header. Requests made with an API key are unaffected — the ceiling applies to the free checker only. SDKs need no update. ## 2026-07-23 [Section titled “2026-07-23”](#2026-07-23) ### Changed [Section titled “Changed”](#changed-1) * A confirmed no-match on `POST /v1/verify` is now billed as a completed verification instead of being refunded. The lookup runs against the current official record and establishes that the business is not registered in that jurisdiction — that finding is the result you asked for, so it consumes credits like any other verification (quick: 1 credit, deep: 15). The response is unchanged: still `404 ENTITY_NOT_FOUND`. Genuine failures are unaffected and continue to refund automatically — `503 DATA_SOURCE_UNAVAILABLE`, `504 DATA_SOURCE_TIMEOUT`, and asynchronous jobs that fail after all retry attempts. No request or response shape changes; SDKs need no update. ## 2026-06-16 [Section titled “2026-06-16”](#2026-06-16) ### Added [Section titled “Added”](#added) * Agent-discovery metadata so the API is self-describing for AI agents and tooling. New read-only endpoints: an MCP Server Card at `/.well-known/mcp/server-card.json`, an A2A Agent Card at `/.well-known/agent-card.json`, an RFC 9727 API Catalog at `/.well-known/api-catalog`, and an Agent Skills index at `/.well-known/agent-skills/index.json` (with per-skill `SKILL.md` documents). The API root (`GET /`) now advertises these via RFC 8288 `Link` headers and serves a Markdown overview to clients that send `Accept: text/markdown`. These additions are purely discovery metadata — existing endpoints, request/response shapes, and authentication are unchanged, and SDKs need no update. ## 2026-06-15 [Section titled “2026-06-15”](#2026-06-15) ### Fixed [Section titled “Fixed”](#fixed) * `POST /v1/verify` for Kentucky (`us-ky`) at the `deep` level could return the details of an unrelated business; it now consistently returns the correct entity. As part of this fix, the `jurisdiction_id` returned for Kentucky entities is now a stable, unique per-record identifier — so a Kentucky `jurisdiction_id` (and its corresponding cached `entity_id`) may differ from a value stored previously. `quick` verification and `POST /v1/search` were unaffected. No response shape or field changes; SDKs need no update. * `POST /v1/verify` for Florida (`us-fl`) at the `deep` level could fail with `ENTITY_NOT_FOUND` even when the business existed and matched; deep verification now reliably retrieves the full record for a matched Florida entity. No response shape or field changes. * Massachusetts (`us-ma`): when the current official record is temporarily unavailable, `POST /v1/search` and `POST /v1/verify` now return `503 DATA_SOURCE_UNAVAILABLE` and are **not** charged, instead of returning an empty (but billed) result. Genuine no-match searches still return an empty result set as before. ## 2026-06-14 [Section titled “2026-06-14”](#2026-06-14) ### Changed [Section titled “Changed”](#changed-2) * `POST /v1/verify`: when a jurisdiction’s data source can’t complete a synchronous lookup, the API now returns a precise status — `503 DATA_SOURCE_UNAVAILABLE` or `504 DATA_SOURCE_TIMEOUT` — or falls back to an asynchronous verification job, instead of a generic `500`. Asynchronous job results (`check_job_status`) and the `verification.failed` webhook now carry a matching category in their `error` message, so a temporary-outage failure is distinguishable from a timeout. No request shape or field changes; existing success responses are unaffected. ## 2026-06-13 [Section titled “2026-06-13”](#2026-06-13) ### Changed [Section titled “Changed”](#changed-3) * `GET /v1/entity/:id` now returns the full business record directly at the top level — including `registered_agent` (an object with its address), `officers`, `principal_address`, and `filing_history_summary` — instead of a nested wrapper, and adds `last_verified_at` plus a `snapshots` count (retrieve the snapshots themselves via `GET /v1/entity/:id/history`). This aligns the cached-entity response with the verification response shape. **Breaking change** for any client that read the previous nested structure. The official Node, Python, and Go SDKs are updated to match. * Renamed two response timestamp fields for clearer, consistent naming. `GET /v1/entity/:id` now returns `last_verified_at` (when the entity was last refreshed against the official record), and each snapshot from `GET /v1/entity/:id/history` now returns `verified_at`. This is a breaking change for any client that read the previous field names. ## 2026-06-12 [Section titled “2026-06-12”](#2026-06-12) ### Fixed [Section titled “Fixed”](#fixed-1) * `force_refresh` on `POST /v1/verify` is now applied only to `deep` verifications, as documented. On `quick` verifications (including a `deep` request that falls back to `quick` where deep is unavailable) the flag is ignored, so a recent stored result may be returned. Quick verifications remain 1 credit. ## 2026-06-11 [Section titled “2026-06-11”](#2026-06-11) ### Changed [Section titled “Changed”](#changed-4) * `webhook_url` on `POST /v1/verify` must now resolve to a publicly reachable address. URLs pointing to private or reserved networks are rejected with a `400 VALIDATION_ERROR`, and the same check is applied again before each webhook delivery. * `POST /v1/verify` now handles the optional `Idempotency-Key` header safely under concurrency. A retry carrying the same key still replays the original response without charging again, but a second request sent **while the first is still being processed** now returns `409 IDEMPOTENCY_CONFLICT` instead of being processed (and charged) twice — retry once the first request completes. The header is documented in the OpenAPI schema and limited to 128 characters; longer values are rejected with `400 VALIDATION_ERROR`. ## 2026-06-03 [Section titled “2026-06-03”](#2026-06-03) ### Changed [Section titled “Changed”](#changed-5) * MCP tool descriptions now state each tool’s credit cost, authentication requirements, and usage guidance — improves tool selection for AI agents. ## 2026-06-01 [Section titled “2026-06-01”](#2026-06-01) ### Added [Section titled “Added”](#added-1) * Three new verified jurisdictions: Massachusetts (`us-ma`), Wisconsin (`us-wi`), and Alabama (`us-al`) ### Removed [Section titled “Removed”](#removed) * Colorado (`us-co`) and Wyoming (`us-wy`) are temporarily unavailable while we work to restore coverage. **19 active jurisdictions total.** ## 2026-05-24 [Section titled “2026-05-24”](#2026-05-24) ### Added [Section titled “Added”](#added-2) * Root discovery endpoint — `GET /` returns a JSON entry point linking to the docs, API reference, OpenAPI spec, MCP endpoint, and health check * `GET /.well-known/security.txt` (RFC 9116) for security researchers ## 2026-05-05 [Section titled “2026-05-05”](#2026-05-05) ### Changed [Section titled “Changed”](#changed-6) * `POST /v1/search` now charges credits only for jurisdictions that completed successfully — failed jurisdictions are refunded and listed in the new `jurisdictions_failed` response field. ### Fixed [Section titled “Fixed”](#fixed-2) * Async verification jobs that fail now correctly deliver the `verification.failed` webhook (previously sent `verification.completed` with empty data) and auto-refund credits. * `GET /v1/verify/status/:jobId` now returns the same response shape as the sync `POST /v1/verify` endpoint and includes `entityId` on completed jobs. * `POST /v1/verify` and `POST /v1/search` reject invalid jurisdictions before charging credits — users with low balances see the validation error rather than `INSUFFICIENT_CREDITS`. ### Security [Section titled “Security”](#security) * OAuth tokens now require the appropriate scope on REST account and billing routes (previously enforced only for MCP): * `account:read` — `GET /v1/account`, `GET /v1/account/usage`, `GET /v1/account/data-export` * `account:write` — `PATCH /v1/account`, `PUT /v1/account/password`, `DELETE /v1/account`, `POST /v1/account/keys`, `DELETE /v1/account/keys/:keyId` * `billing` — `GET /v1/billing`, `POST /v1/billing/purchase` * API key auth continues to grant all scopes. ## 2026-04-18 [Section titled “2026-04-18”](#2026-04-18) ### Added [Section titled “Added”](#added-3) * California (`us-ca`), Nevada (`us-nv`), New York (`us-ny`), and Wyoming (`us-wy`) now verified — 18 active jurisdictions total ## 2026-04-06 [Section titled “2026-04-06”](#2026-04-06) ### Added [Section titled “Added”](#added-4) * LLM tool definitions served at `GET /tools/openai.json` and `GET /tools/anthropic.json` — auto-generated from MCP schemas * [`bizverify-agents`](https://pypi.org/project/bizverify-agents/) Python package — LangChain and CrewAI tool wrappers with built-in execution * [`n8n-nodes-bizverify`](https://www.npmjs.com/package/n8n-nodes-bizverify) — n8n community node with 9 operations * [AI Agents guide](/guides/ai-agents) — integration docs for all supported platforms ## 2026-04-02 [Section titled “2026-04-02”](#2026-04-02) ### Added [Section titled “Added”](#added-5) * Documentation site launched at [docs.bizverify.co](https://docs.bizverify.co) * Node.js SDK (`@bizverify/sdk`) and Python SDK (`bizverify`) published ## 2026-03-28 [Section titled “2026-03-28”](#2026-03-28) ### Added [Section titled “Added”](#added-6) * MCP server at `POST /mcp` — connect BizVerify to Claude, Cursor, VS Code, and other AI tools * OAuth 2.1 + PKCE authentication for MCP clients * Dynamic Client Registration (RFC 7591) ## 2026-03-20 [Section titled “2026-03-20”](#2026-03-20) ### Added [Section titled “Added”](#added-7) * Agent-first API refactor — unified passwordless auth flow (`/v1/auth/request-access`, `/v1/auth/verify-access`) * Standardized error responses with `code`, `message`, `details`, `suggestion` * Credit headers (`X-Credits-Remaining`, `X-Credits-Charged`) on all authenticated responses * GDPR data export endpoint (`GET /v1/account/data-export`) ### Changed [Section titled “Changed”](#changed-7) * API docs moved from `/docs` to `/reference` ## 2026-03-10 [Section titled “2026-03-10”](#2026-03-10) ### Added [Section titled “Added”](#added-8) * Prometheus metrics endpoint for monitoring * Audit log for admin actions * Automated alerting when a jurisdiction’s availability changes * Better Stack log shipping ## 2026-03-01 [Section titled “2026-03-01”](#2026-03-01) ### Added [Section titled “Added”](#added-9) * Admin dashboard at `/internal/dashboard/` * User management (search, credit adjustment, disable/enable) * Job management (list, retry, refund) ## 2026-02-28 [Section titled “2026-02-28”](#2026-02-28) ### Added [Section titled “Added”](#added-10) * Webhook callbacks — receive notifications when jobs complete, fail, or are refunded * Search pagination (`limit`/`offset` on `POST /v1/search`) * Account management — email update, password change, account deletion (GDPR) * Jurisdiction gating — non-active jurisdictions rejected before credit deduction ## 2026-02-15 [Section titled “2026-02-15”](#2026-02-15) ### Added [Section titled “Added”](#added-11) * Email verification and password reset flows * OpenAPI 3.1 spec generation with Scalar interactive docs * CORS configuration * Auto-refund on job failure * CI/CD pipeline ## 2026-02-01 [Section titled “2026-02-01”](#2026-02-01) ### Added [Section titled “Added”](#added-12) * Initial API launch with 14 verified jurisdictions * Credit-based billing with Stripe checkout * Verification coverage across 50 US states and 11 international jurisdictions * Sentry error monitoring # Verification tiers > When to use Quick check vs Deep verification — capability, cost, and availability. BizVerify offers two verification tiers. They are parallel products, not different quality levels — pick the one that matches the question you’re asking. ## Quick check [Section titled “Quick check”](#quick-check) **1 credit. Available in every active jurisdiction.** Answers the core question: **does this business exist, and is it currently active?** Returns: * `exists` * `status` (active, inactive, dissolved, suspended, revoked, etc.) * `good_standing` * `entity_name` (canonical / registered form) * `jurisdiction` Use quick check when you need to confirm a counterparty is real and operational — onboarding forms, invoice validation, lead qualification, real-time flows where sub-second latency matters. ## Deep verification [Section titled “Deep verification”](#deep-verification) **15 credits. Available in a subset of jurisdictions.** Returns everything from quick check, **plus**: * `entity_type` (LLC, corporation, LP, etc.) * `formation_date` * `jurisdiction_id` (the registry identifier) * `registered_agent` * `officers` * `principal_address` * `filing_history_summary` Use deep verification when you need the full entity record — KYB/compliance workflows, contract counterparty review, customer due diligence, anything that needs the registered agent or the officer list. Add `force_refresh: true` (25 credits) to get the most current result instead of a recent stored one. `force_refresh` applies to deep verification only — it is ignored on quick verifications. ## What happens if I ask for deep in a quick-only jurisdiction? [Section titled “What happens if I ask for deep in a quick-only jurisdiction?”](#what-happens-if-i-ask-for-deep-in-a-quick-only-jurisdiction) You get a quick result, charged at the quick rate (1 credit). The response includes: ```json { "verification_level": "quick", "full_verification_available": false, "reason": { "code": "jurisdiction_quick_only", "message": "This jurisdiction currently supports quick verification only. Deep verification is not available for this jurisdiction at this time." } } ``` This way you never pay for data you can’t get, and a single integration works across every supported jurisdiction without branching logic. When deep is available, responses include `"full_verification_available": true` and no `reason` field. ## Choosing a tier [Section titled “Choosing a tier”](#choosing-a-tier) | You need to know… | Tier | | ------------------------------------------------------- | ----- | | Is this business real and active right now? | Quick | | Is this the right counterparty to sign a contract with? | Deep | | Is the invoice from a legitimate company? | Quick | | Who are the officers and registered agent? | Deep | | Does this company match my CRM record? | Quick | | Full KYB file for compliance review | Deep | If in doubt, start with quick. You can always promote to deep on the entities that matter. ## Per-jurisdiction availability [Section titled “Per-jurisdiction availability”](#per-jurisdiction-availability) See the [Coverage](/coverage/) page for a live filterable table showing which tier is available in each jurisdiction. Coverage is sourced from `/v1/config` and updates as new jurisdictions ship. # Coverage > Every supported jurisdiction and which verification tiers are available. Searchable and filterable. Sourced from /v1/config. This is the live list of every active BizVerify jurisdiction and the tier(s) available for each. Data is fetched directly from [`/v1/config`](https://api.bizverify.co/v1/config) at page load — it reflects the current state of the platform. * **Quick check** is available in every active jurisdiction. * **Deep verification** availability varies. Where deep isn’t listed, [requesting deep returns a quick result with a reason](/concepts/tiers/#what-happens-if-i-ask-for-deep-in-a-quick-only-jurisdiction). Search jurisdictions… All tiers (all) Loading… | Jurisdiction | Code | Quick check | Deep verification | | ------------ | ---- | ----------- | ----------------- | Looking for a jurisdiction that isn’t listed? Email — we ship new jurisdictions regularly. # Error Codes > Complete reference of BizVerify API error codes, HTTP statuses, and example responses. ## Error response format [Section titled “Error response format”](#error-response-format) All API errors return a consistent JSON structure: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description", "details": {}, "suggestion": "Actionable next step" } } ``` | Field | Type | Description | | ------------ | ------- | ---------------------------------------------------------------- | | `code` | string | Machine-readable error code (use this for programmatic handling) | | `message` | string | Human-readable description | | `details` | object? | Additional context (varies by error) | | `suggestion` | string? | Actionable hint for resolving the error | ## Error codes [Section titled “Error codes”](#error-codes) ### Authentication errors [Section titled “Authentication errors”](#authentication-errors) | Code | HTTP | Description | | --------------------- | ---- | -------------------------------------------------- | | `UNAUTHORIZED` | 401 | Missing or invalid authentication | | `INVALID_API_KEY` | 401 | API key not found or malformed | | `API_KEY_REVOKED` | 401 | API key has been revoked | | `INVALID_CREDENTIALS` | 401 | Wrong password (legacy auth) | | `EMAIL_NOT_VERIFIED` | 403 | Email must be verified before using paid endpoints | | `INSUFFICIENT_SCOPE` | 403 | OAuth token lacks required scope | | `FORBIDDEN` | 403 | Action not permitted | | `ACCOUNT_DELETED` | 403 | Account has been deleted | | `ACCOUNT_DISABLED` | 403 | Account has been disabled by admin | ### Billing errors [Section titled “Billing errors”](#billing-errors) | Code | HTTP | Description | | ---------------------- | ---- | ------------------------------------- | | `INSUFFICIENT_CREDITS` | 402 | Not enough credits for this operation | | `PAYMENT_FAILED` | 402 | Payment processing failed | ### Validation errors [Section titled “Validation errors”](#validation-errors) | Code | HTTP | Description | | ---------------------------- | ---- | ----------------------------------------------------- | | `VALIDATION_ERROR` | 400 | Request body, query, or path params failed validation | | `JURISDICTION_NOT_SUPPORTED` | 400 | Jurisdiction code is not recognized or not active | | `TOO_MANY_JURISDICTIONS` | 400 | Search requested too many jurisdictions | | `INVALID_VERIFICATION_CODE` | 400 | OTP code is incorrect | | `VERIFICATION_CODE_EXPIRED` | 400 | OTP code has expired (15-minute window) | | `TERMS_NOT_ACCEPTED` | 422 | Must accept terms of service | ### Not found errors [Section titled “Not found errors”](#not-found-errors) | Code | HTTP | Description | | ------------------ | ---- | -------------------------- | | `NOT_FOUND` | 404 | Generic resource not found | | `ENTITY_NOT_FOUND` | 404 | Entity ID does not exist | | `JOB_NOT_FOUND` | 404 | Job ID does not exist | ### Rate limiting [Section titled “Rate limiting”](#rate-limiting) | Code | HTTP | Description | | --------------------- | ---- | ------------------------------------------------------- | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests — check `details.retry_after_seconds` | ### Conflict errors [Section titled “Conflict errors”](#conflict-errors) | Code | HTTP | Description | | ---------------------- | ---- | ----------------------------- | | `EMAIL_ALREADY_EXISTS` | 409 | Email is already registered | | `ALREADY_REFUNDED` | 409 | Job has already been refunded | ### Server errors [Section titled “Server errors”](#server-errors) | Code | HTTP | Description | | ------------------------- | ---- | --------------------------------------------------- | | `DATA_SOURCE_UNAVAILABLE` | 503 | The official data source is temporarily unavailable | | `DATA_SOURCE_TIMEOUT` | 504 | The official data source did not respond in time | | `INTERNAL_ERROR` | 500 | Unexpected server error | ## Example error responses [Section titled “Example error responses”](#example-error-responses) ### Insufficient credits [Section titled “Insufficient credits”](#insufficient-credits) ```json { "error": { "code": "INSUFFICIENT_CREDITS", "message": "Insufficient credits. Required: 15, available: 3.", "details": { "required": 15, "available": 3 }, "suggestion": "Purchase more credits via POST /v1/billing/purchase. Check available packages at GET /v1/billing." } } ``` ### Invalid jurisdiction [Section titled “Invalid jurisdiction”](#invalid-jurisdiction) ```json { "error": { "code": "JURISDICTION_NOT_SUPPORTED", "message": "Jurisdiction 'us-ca' is not supported.", "details": { "provided": "us-ca", "suggestions": ["us-co", "us-ct"] }, "suggestion": "Use GET /v1/jurisdictions for valid codes or GET /v1/config for the full list." } } ``` ### Rate limit exceeded [Section titled “Rate limit exceeded”](#rate-limit-exceeded) ```json { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded", "details": { "retry_after_seconds": 45 }, "suggestion": "Retry after 45 seconds. Check X-RateLimit-Reset header for exact reset time." } } ``` ### Validation error [Section titled “Validation error”](#validation-error) ```json { "error": { "code": "VALIDATION_ERROR", "message": "body/entity_name: Required", "suggestion": "Check the request body, query parameters, or path parameters against the API schema at GET /reference." } } ``` # AI Agents > Integrate BizVerify with LLM agents — OpenAI function calling, Anthropic tool use, LangChain, CrewAI, n8n, and MCP. BizVerify provides pre-built tool definitions for major LLM platforms, plus native MCP support. Choose the integration that matches your stack. ## MCP (recommended) [Section titled “MCP (recommended)”](#mcp-recommended) If your client supports [Model Context Protocol](https://modelcontextprotocol.io/), use BizVerify’s built-in MCP server. This is the recommended approach for Claude Desktop, Cursor, Windsurf, and other MCP-compatible tools. See the [MCP Setup guide](/mcp/setup) for configuration instructions. ## OpenAI Function Calling [Section titled “OpenAI Function Calling”](#openai-function-calling) BizVerify serves a ready-to-use `tools` array at a public endpoint. Fetch it and pass it directly to the OpenAI Chat Completions API. * Node.js ```javascript import OpenAI from 'openai'; import BizVerify from '@bizverify/sdk'; const openai = new OpenAI(); const bv = new BizVerify({ apiKey: 'bv_live_...' }); // Fetch tool definitions const tools = await fetch('https://api.bizverify.co/tools/openai.json').then(r => r.json()); const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Is Acme Inc registered in Florida?' }], tools, }); // Handle tool calls for (const call of response.choices[0].message.tool_calls ?? []) { const args = JSON.parse(call.function.arguments); if (call.function.name === 'verify_business') { const result = await bv.verification.verify(args.entity_name, args.jurisdiction); // Send result back to the model... } } ``` * Python ```python import httpx from openai import OpenAI from bizverify import BizVerify client = OpenAI() bv = BizVerify(api_key="bv_live_...") # Fetch tool definitions tools = httpx.get("https://api.bizverify.co/tools/openai.json").json() response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Is Acme Inc registered in Florida?"}], tools=tools, ) # Handle tool calls in response.choices[0].message.tool_calls ``` **Endpoint:** `GET https://api.bizverify.co/tools/openai.json` The tool definitions tell the model what’s available. Your code handles execution using the [Node.js](/sdks/node), [Python](/sdks/python), or [Go](/sdks/go) SDK. ## Anthropic Tool Use [Section titled “Anthropic Tool Use”](#anthropic-tool-use) Same pattern, different format: * Node.js ```javascript import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic(); const tools = await fetch('https://api.bizverify.co/tools/anthropic.json').then(r => r.json()); const response = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, messages: [{ role: 'user', content: 'Is Acme Inc registered in Florida?' }], tools, }); ``` * Python ```python import httpx import anthropic client = anthropic.Anthropic() tools = httpx.get("https://api.bizverify.co/tools/anthropic.json").json() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Is Acme Inc registered in Florida?"}], tools=tools, ) ``` **Endpoint:** `GET https://api.bizverify.co/tools/anthropic.json` ## LangChain [Section titled “LangChain”](#langchain) The `bizverify-agents` package provides LangChain tools with built-in execution — no manual tool call routing needed. ```bash pip install bizverify-agents[langchain] ``` ```python from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent from bizverify_agents.langchain import BizVerifyTools tools = BizVerifyTools(api_key="bv_live_...").get_tools() agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools) result = agent.invoke({ "messages": [{"role": "user", "content": "Is Acme Inc registered in Florida?"}] }) ``` Or use the `BIZVERIFY_API_KEY` environment variable: ```bash export BIZVERIFY_API_KEY=bv_live_... ``` ```python tools = BizVerifyTools().get_tools() # reads from env ``` ## CrewAI [Section titled “CrewAI”](#crewai) ```bash pip install bizverify-agents[crewai] ``` ```python from crewai import Agent, Task, Crew from bizverify_agents.crewai import BizVerifyTools tools = BizVerifyTools(api_key="bv_live_...").get_tools() analyst = Agent( role="Business Analyst", goal="Verify business registration status", tools=tools, ) task = Task( description="Check if Acme Inc is registered in Florida", agent=analyst, expected_output="Registration status and details", ) crew = Crew(agents=[analyst], tasks=[task]) result = crew.kickoff() ``` ## n8n [Section titled “n8n”](#n8n) Install the BizVerify node via n8n’s Community Nodes: 1. Go to **Settings > Community Nodes** 2. Enter `n8n-nodes-bizverify` and click **Install** 3. Add your API key in **Credentials > BizVerify API** The node provides a single **Operation** dropdown with all BizVerify operations (Verify Business, Search Entities, Get Entity, etc.). Fields appear dynamically based on the selected operation. ## Available Tools [Section titled “Available Tools”](#available-tools) All integrations expose the same 9 tools: | Tool | Auth Required | Credits | Description | | -------------------- | :-----------: | -------------- | ------------------------------------------ | | `get_config` | No | Free | Service configuration and jurisdictions | | `list_jurisdictions` | No | Free | Supported jurisdictions with active status | | `verify_business` | Yes | 1-25 | Verify a business entity | | `search_entities` | Yes | 2/jurisdiction | Search entities by name | | `check_job_status` | Yes | Free | Check async job status | | `get_entity` | Yes | Free | Get cached entity details | | `get_entity_history` | Yes | 5 | Get verification history | | `get_account` | Yes | Free | View account info and credits | | `purchase_credits` | Yes | N/A | Purchase credits via Stripe | ## Authentication [Section titled “Authentication”](#authentication) All tool integrations use **API key** authentication. Get a key at [bizverify.co](https://bizverify.co) — 50 free credits on signup. * **OpenAI / Anthropic:** You handle auth in your tool execution code using a BizVerify SDK * **LangChain / CrewAI:** Pass `api_key=` or set `BIZVERIFY_API_KEY` env var * **n8n:** Configure in the BizVerify API credential * **MCP:** Uses OAuth — see [MCP Setup](/mcp/setup) # AML & Beneficial Ownership > Use BizVerify as the registry-sourced input to an AML / KYC workflow — confirm the entity, then pull the registered agent and officers from the official record. Anti-money-laundering and customer due-diligence workflows start from the same foundation: **is this a real, active legal entity, and who is on its official record?** BizVerify supplies that foundation straight from the current registry record — the entity’s status and, where a jurisdiction publishes them, its officers and registered agent. ## What BizVerify provides — and what it doesn’t [Section titled “What BizVerify provides — and what it doesn’t”](#what-bizverify-provides--and-what-it-doesnt) BizVerify returns the **official registry record**: existence, status, good standing, and the people and agent the registry lists. That is the authoritative first layer of a business AML file. It is **not** a sanctions or PEP screen, and the officer list is the registry’s record of officers and directors — not a beneficial-ownership (UBO) determination. Use BizVerify for the entity and registry-listed people, then run those names through your sanctions/PEP and UBO tooling. Keeping the layers separate keeps each one auditable. ## Step 1 — Confirm the entity is real and active [Section titled “Step 1 — Confirm the entity is real and active”](#step-1--confirm-the-entity-is-real-and-active) * Node.js ```ts import BizVerify from '@bizverify/sdk'; const biz = new BizVerify({ apiKey: 'bv_live_...' }); const result = await biz.verification.verify({ entity_name: 'Acme Trading Ltd', jurisdiction: 'gb', }); const eligible = result.exists && result.status === 'active' && result.good_standing === true; ``` * Python ```python from bizverify import BizVerify biz = BizVerify(api_key="bv_live_...") result = biz.verification.verify("Acme Trading Ltd", "gb") eligible = result.exists and result.status == "active" and result.good_standing is True ``` A dissolved, revoked, or withdrawn entity should never enter onboarding as a live counterparty. Gating on `status` and `good_standing` first means your AML review only spends effort on entities that are actually operating. ## Step 2 — Pull the officers and registered agent [Section titled “Step 2 — Pull the officers and registered agent”](#step-2--pull-the-officers-and-registered-agent) Deep verification returns the registry-listed people and the agent for service of process — the names your AML process screens. * Node.js ```ts const job = await biz.verification.verifyAndWait( { entity_name: 'Acme Trading Ltd', jurisdiction: 'gb', verification_level: 'deep' }, { timeoutMs: 120_000 }, ); const record = job.data; for (const officer of record.officers) { // Feed officer.name into your sanctions / PEP / UBO screening. } record.registered_agent?.name; record.principal_address; ``` * Python ```python job = biz.verification.verify_and_wait( "Acme Trading Ltd", "gb", verification_level="deep", timeout=120.0, ) record = job.data for officer in record.officers: ... # Feed officer.name into your sanctions / PEP / UBO screening. record.registered_agent # name + address record.principal_address ``` ## Coverage varies by registry [Section titled “Coverage varies by registry”](#coverage-varies-by-registry) What the officer and agent fields contain depends on what each jurisdiction’s registry publishes. Some registries expose a full officer and director list; others publish only the registered agent and principal office. Check the [coverage page](/coverage) for what a given jurisdiction returns, and treat an empty `officers` list as “the registry doesn’t publish officers here,” not as “no officers exist.” ## Keep an auditable record [Section titled “Keep an auditable record”](#keep-an-auditable-record) Every deep result carries a `jurisdiction_id` and, when you fetch it later, a `last_verified_at`. Persist both so your AML file records exactly which official entity you screened and when. Because the record reflects the current registry state on each request — no stale snapshot — a periodic re-check surfaces a counterparty that has since been dissolved or lost good standing. ## Related [Section titled “Related”](#related) * [KYB Pipeline](/guides/kyb-pipeline) — the two-stage gate-then-enrich flow * [Verification tiers](/concepts/tiers) — what quick vs deep returns * [Coverage](/coverage) — which jurisdictions publish officers and agents # Async Jobs > How async verification and search jobs work — polling, webhooks, and the job lifecycle. ## When jobs are async [Section titled “When jobs are async”](#when-jobs-are-async) Both `POST /v1/verify` and `POST /v1/search` can return results in two ways: * **Immediate** (`200`) — the entity is already cached from a previous verification. You get the full result in the response. * **Async** (`202 Accepted`) — a background job is created to retrieve the record from the official source. You get a `job_id` to track progress. Most first-time verifications are async. Subsequent lookups of the same entity are usually immediate. ## Job lifecycle [Section titled “Job lifecycle”](#job-lifecycle) ```plaintext pending → processing → completed → failed (credits auto-refunded) ``` 1. **Pending** — job is queued, waiting to be processed 2. **Processing** — the latest record is being retrieved from the official source 3. **Completed** — data retrieved successfully, entity cached 4. **Failed** — all retry attempts exhausted (3 attempts total). Credits are automatically refunded. See [Credits & Billing](/guides/billing/) for refund details. ## Consuming results [Section titled “Consuming results”](#consuming-results) ### Option 1: Polling [Section titled “Option 1: Polling”](#option-1-polling) Poll the job status endpoint until the job completes: ```bash curl https://api.bizverify.co/v1/verify/status/{job_id} \ -H "X-API-Key: bv_live_..." ``` Response while pending/processing: ```json { "status": "processing", "job_id": "job_abc123" } ``` Response when complete: ```json { "status": "completed", "job_id": "job_abc123", "entity_id": "ent_xyz", "data": { "entity_name": "ACME CORPORATION", "entity_type": "corporation", "status": "active", "jurisdiction": "us-fl", "formation_date": "2015-03-12", "confidence": 95 } } ``` Tip Poll every 2-3 seconds. Most jobs complete within 10-30 seconds. ### Option 2: Webhooks [Section titled “Option 2: Webhooks”](#option-2-webhooks) Instead of polling, provide a `webhook_url` when creating the request. BizVerify will POST to your URL when the job completes or fails. See the [Webhooks guide](/webhooks/) for payload format and best practices. ### Option 3: SDK helpers [Section titled “Option 3: SDK helpers”](#option-3-sdk-helpers) The SDKs provide convenience methods that handle polling automatically: * Node.js ```ts // Polls automatically until complete or timeout const result = await biz.verification.verifyAndWait( { entity_name: 'Acme Inc', jurisdiction: 'us-fl' }, { timeoutMs: 120_000 }, ); console.log(result.data); ``` * Python ```python # Polls automatically until complete or timeout result = client.verification.verify_and_wait( "Acme Inc", "us-fl", timeout=60.0, ) print(result.data) ``` * Go ```go // Polls automatically until complete or timeout job, err := client.Verification.VerifyAndWait(ctx, bv.VerifyParams{ EntityName: "Acme Inc", Jurisdiction: "us-fl", }, &bv.PollOptions{Timeout: 120 * time.Second}) ``` ## Search jobs [Section titled “Search jobs”](#search-jobs) `POST /v1/search` works the same way. When searching across multiple jurisdictions, each jurisdiction creates a separate job. The response aggregates results as they complete. If some jurisdictions fail while others succeed, you are only charged for the successful ones. The response includes a `jurisdictions_failed` array so you know which ones had issues. # Credits & Billing > How credits work, what operations cost, and how to purchase more. ## How credits work [Section titled “How credits work”](#how-credits-work) BizVerify uses a prepaid credit system. Credits are deducted when you make verification or search requests. If a job fails after all retry attempts, credits are automatically refunded. New accounts start with **50 free credits**. ## Operation costs [Section titled “Operation costs”](#operation-costs) | Operation | Credits | Notes | | --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Quick check | 1 | Existence, status, and good-standing. Fast, always-available answer. | | Deep verification | 15 | Adds entity type, formation date, registered agent, officers, principal address, and filing history. Availability varies by jurisdiction. | | Deep verification (force refresh) | 25 | Same as deep, but bypasses cache to return the most current result. | | Search (per jurisdiction) | 2 | Charged per jurisdiction searched | | Entity history | 5 | Historical verification snapshots | | Entity lookup | Free | Retrieve cached entity by ID | | Job status check | Free | Poll async job status | See the [Verification tiers](/concepts/tiers/) page for when to use quick vs deep, and the [Coverage](/coverage/) page for per-jurisdiction tier availability. ## Credit headers [Section titled “Credit headers”](#credit-headers) Every authenticated API response includes credit information in headers: | Header | Description | | --------------------- | ------------------------------------------------------- | | `X-Credits-Remaining` | Your credit balance after this request | | `X-Credits-Charged` | Credits consumed by this request (0 for free endpoints) | These headers are included on both success and error responses, so you can always track your balance. ## Checking your balance [Section titled “Checking your balance”](#checking-your-balance) ### Via API [Section titled “Via API”](#via-api) ```bash curl https://api.bizverify.co/v1/account \ -H "X-API-Key: bv_live_..." ``` The `credit_balance` field in the response shows your current balance. ### Via billing endpoint [Section titled “Via billing endpoint”](#via-billing-endpoint) For more detail including transaction history: ```bash curl https://api.bizverify.co/v1/billing \ -H "X-API-Key: bv_live_..." ``` Returns your balance, available packages, and paginated transaction history. Use `?limit=10&offset=0` query parameters to paginate transactions. ### Via SDK [Section titled “Via SDK”](#via-sdk) * Node.js ```ts const meta = biz.lastResponseMeta; console.log(meta?.creditsRemaining); // balance after last request ``` * Python ```python # Credits are tracked automatically after each request print(client.last_response_meta.credits_remaining) ``` ## Purchasing credits [Section titled “Purchasing credits”](#purchasing-credits) Note Your email must be verified before you can purchase credits. The verification happens automatically during the passwordless signup flow. ```bash curl -X POST https://api.bizverify.co/v1/billing/purchase \ -H "Content-Type: application/json" \ -H "X-API-Key: bv_live_..." \ -d '{"package_id": "credits_100"}' ``` This returns a Stripe checkout URL. Complete the payment there and credits are added to your account immediately. ### Available packages [Section titled “Available packages”](#available-packages) | Package | Credits | Price | Per credit | | ---------- | ------- | ----- | ---------- | | Starter | 100 | $8 | $0.08 | | Pro | 500 | $35 | $0.07 | | Business | 2,000 | $120 | $0.06 | | Enterprise | 10,000 | $500 | $0.05 | ## Refunds [Section titled “Refunds”](#refunds) Credits are automatically refunded when a verification or search job fails after all retry attempts. You don’t need to take any action — the refund happens immediately and your balance is updated. The `X-Credits-Remaining` header on subsequent requests will reflect the refunded amount. ### A confirmed no-match is billed [Section titled “A confirmed no-match is billed”](#a-confirmed-no-match-is-billed) Not finding the business is a result, not a failure. When a verification completes and establishes that the name isn’t registered in that jurisdiction, the call returns `404 ENTITY_NOT_FOUND` and **is charged** at the normal rate — the lookup ran against the current official record and answered your question. Refunds are for calls that couldn’t produce an answer at all: | Outcome | Status | Charged? | | ---------------------------------- | ----------------------------- | ------------- | | Business found | `200` | Yes | | Confirmed no-match | `404 ENTITY_NOT_FOUND` | Yes | | Source temporarily unavailable | `503 DATA_SOURCE_UNAVAILABLE` | No — refunded | | Source timed out | `504 DATA_SOURCE_TIMEOUT` | No — refunded | | Async job failed after all retries | webhook `verification.failed` | No — refunded | In rare cases, refunds can also be issued manually by BizVerify support. Contact if you believe a charge was incorrect. For more on how async jobs work and when failures occur, see [Async Jobs](/guides/async-jobs/). # KYB Pipeline > Build a Know-Your-Business pipeline — gate on a quick existence and status check, enrich with deep verification, and interpret status and good standing. A KYB (Know-Your-Business) pipeline answers two questions about a counterparty: **is it a real, active business?** and **what is the entity record behind it?** BizVerify maps those to two tiers — a [quick check](/concepts/tiers) that gates cheaply on every request, and [deep verification](/concepts/tiers) that returns the structured record when you need it. This guide wires them into one flow. ## The two-stage pattern [Section titled “The two-stage pattern”](#the-two-stage-pattern) 1. **Gate** with a quick check (1 credit, available in every active jurisdiction). Confirm the entity exists, is active, and is in good standing before you spend anything else. 2. **Enrich** with deep verification (15 credits, a subset of jurisdictions) only for entities that pass the gate and need the full record — registered agent, officers, principal address. Running the cheap check first keeps cost proportional to how far each counterparty gets in your funnel. ## Stage 1 — Gate on a quick check [Section titled “Stage 1 — Gate on a quick check”](#stage-1--gate-on-a-quick-check) * Node.js ```ts import BizVerify from '@bizverify/sdk'; const biz = new BizVerify({ apiKey: 'bv_live_...' }); const result = await biz.verification.verify({ entity_name: 'Acme Holdings, Inc.', jurisdiction: 'us-de', }); if (!result.exists) { // No matching entity on the register — reject or route to manual review. } const live = result.status === 'active' && result.good_standing === true; ``` * Python ```python from bizverify import BizVerify biz = BizVerify(api_key="bv_live_...") result = biz.verification.verify("Acme Holdings, Inc.", "us-de") if not result.exists: ... # No matching entity on the register — reject or route to manual review. live = result.status == "active" and result.good_standing is True ``` A quick check returns `exists`, `status`, `good_standing`, the canonical `entity_name`, and `jurisdiction`. That is enough to auto-approve a clean counterparty or drop a questionable one into review — the hot-path decision most onboarding flows actually need. ## Stage 2 — Enrich with deep verification [Section titled “Stage 2 — Enrich with deep verification”](#stage-2--enrich-with-deep-verification) For entities that pass the gate and need the full record, request deep verification. `verifyAndWait` handles the async job and returns the completed record. * Node.js ```ts const job = await biz.verification.verifyAndWait( { entity_name: 'Acme Holdings, Inc.', jurisdiction: 'us-de', verification_level: 'deep' }, { timeoutMs: 120_000 }, ); const record = job.data; record.registered_agent; // { name, address } record.officers; // [{ name, title, address }] record.principal_address; record.formation_date; record.jurisdiction_id; // registry identifier — store this ``` * Python ```python job = biz.verification.verify_and_wait( "Acme Holdings, Inc.", "us-de", verification_level="deep", timeout=120.0, ) record = job.data record.registered_agent # { name, address } record.officers # [{ name, title, address }] record.principal_address record.formation_date record.jurisdiction_id # registry identifier — store this ``` Deep verification returns everything from the quick check plus `entity_type`, `formation_date`, `jurisdiction_id`, `registered_agent`, `officers`, `principal_address`, and `filing_history_summary`. Which fields carry data depends on what the jurisdiction’s registry publishes — some registries expose officers, others only the registered agent. Add `force_refresh: true` (25 credits) when you need the most current result instead of a recent stored one. ## Interpreting status and good standing [Section titled “Interpreting status and good standing”](#interpreting-status-and-good-standing) `status` is the entity’s lifecycle state; `good_standing` is a separate compliance signal. Read both — in some jurisdictions an entity is `active` but not in good standing (for example, delinquent on an annual report), and that combination is exactly what a KYB gate should catch. | `status` | `good_standing` | KYB decision | | ------------------------------------- | --------------- | ----------------------------------------------------------------- | | `active` | `true` | Pass — real and current | | `active` | `false` | Review — exists but has a compliance gap (e.g. delinquent filing) | | `dissolved` / `revoked` / `withdrawn` | `false` | Fail — no longer a live counterparty | | `inactive` | `null` | Not a live entity (e.g. a name-only record) — treat as no match | The exact status vocabulary varies by jurisdiction; the normalized `status` and `good_standing` fields are consistent across all of them, so one branch of logic works everywhere. ## Run it in batch [Section titled “Run it in batch”](#run-it-in-batch) For portfolio screening or re-verification, drive the same two stages over a list. Deep verifications are asynchronous — use [webhooks](/webhooks) to receive results instead of blocking, or [async jobs](/guides/async-jobs) to submit and poll. Failed jobs refund their credits automatically, so a transient registry issue never costs you. ## Store the `jurisdiction_id` [Section titled “Store the jurisdiction\_id”](#store-the-jurisdiction_id) Persist the `jurisdiction_id` from each deep result. It is the stable registry key, so you can re-check the same entity later without a fuzzy name match, and [fetch the cached record](/sdks/node) for free between verifications: ```ts const entity = await biz.entities.get(storedEntityId); console.log(entity.entity_name, entity.status, entity.last_verified_at); ``` ## Related [Section titled “Related”](#related) * [Verification tiers](/concepts/tiers) — quick vs deep, cost, and availability * [AI Agents](/guides/ai-agents) — run the gate from an LLM agent or MCP client * [Webhooks](/webhooks) and [Async Jobs](/guides/async-jobs) — batch and non-blocking flows * [Error Codes](/errors) — handling registry timeouts and no-match results # Lead Enrichment > Enrich CRM leads and signups with the official business record — confirm the company is real, canonicalize its name, and attach entity type, formation date, and address. A raw lead is usually just a typed company name. Lead enrichment turns that string into a verified business: confirmation the company is a real, active entity, its canonical registered name, and the structured record behind it. BizVerify does this from the current registry, so an enriched lead reflects the company’s real status — not a data broker’s stale copy. ## Two enrichment paths [Section titled “Two enrichment paths”](#two-enrichment-paths) * **Exact name on signup** → a [quick check](/concepts/tiers) confirms the entity and returns the canonical name in one call, fast enough to run inline. * **Fuzzy or partial name** → [search](/sdks/node) first to resolve candidates, then verify the chosen match. ## Real-time enrichment on signup [Section titled “Real-time enrichment on signup”](#real-time-enrichment-on-signup) When a user types their company name into a signup form, verify it inline and store the canonical result. * Node.js ```ts import BizVerify from '@bizverify/sdk'; const biz = new BizVerify({ apiKey: 'bv_live_...' }); const result = await biz.verification.verify({ entity_name: 'acme holdings', jurisdiction: 'us-de', }); if (result.exists) { lead.companyName = result.entity_name; // canonical registered form lead.status = result.status; // active / dissolved / ... lead.verified = result.good_standing === true; } ``` * Python ```python from bizverify import BizVerify biz = BizVerify(api_key="bv_live_...") result = biz.verification.verify("acme holdings", "us-de") if result.exists: lead.company_name = result.entity_name # canonical registered form lead.status = result.status # active / dissolved / ... lead.verified = result.good_standing is True ``` Even the quick check upgrades a lead: a messy input becomes the canonical registered name, plus a real/active signal you can score on. ## Resolve fuzzy names with search [Section titled “Resolve fuzzy names with search”](#resolve-fuzzy-names-with-search) When the input is partial or ambiguous, search returns ranked candidates with a `confidence` score. Pick the best match, then verify it. * Node.js ```ts const page = await biz.search.find({ entity_name: 'acme', jurisdiction: 'us-de', limit: 5 }); const best = page.results[0]; // highest confidence if (best && best.confidence > 0.9) { const enriched = await biz.entities.get(best.entity_id); } ``` * Python ```python response = biz.search.find("acme", jurisdiction="us-de", limit=5) best = response.results[0] # highest confidence if best and best.confidence > 0.9: enriched = biz.entities.get(best.entity_id) ``` ## Enrich the full record in batch [Section titled “Enrich the full record in batch”](#enrich-the-full-record-in-batch) For an existing CRM, run [deep verification](/concepts/tiers) over your accounts to attach the structured record — `entity_type`, `formation_date`, `registered_agent`, `principal_address`, and the `jurisdiction_id`. Deep verifications are asynchronous; use [webhooks](/webhooks) to collect results without blocking your batch. ```ts const job = await biz.verification.verifyAndWait( { entity_name: account.name, jurisdiction: account.jurisdiction, verification_level: 'deep' }, { timeoutMs: 120_000 }, ); account.entityType = job.data.entity_type; account.foundedOn = job.data.formation_date; account.registryId = job.data.jurisdiction_id; ``` ## Dedupe on the `jurisdiction_id` [Section titled “Dedupe on the jurisdiction\_id”](#dedupe-on-the-jurisdiction_id) Two leads with slightly different spellings of the same company resolve to the same `jurisdiction_id`. Use it as the dedupe key so “Acme Holdings, Inc.” and “acme holdings inc” collapse into one account. ## Related [Section titled “Related”](#related) * [Verification tiers](/concepts/tiers) — quick for real-time, deep for the full record * [KYB Pipeline](/guides/kyb-pipeline) — when enrichment doubles as a compliance gate * [AI Agents](/guides/ai-agents) — enrich leads from an agent workflow # Rate Limits > Default rate limits, response headers, and how to handle 429 errors. ## Default limits [Section titled “Default limits”](#default-limits) | Context | Limit | Scope | | ------------------------ | ------------------ | -------------- | | REST API (authenticated) | 60 requests/minute | Per API key | | MCP (unauthenticated) | 20 requests/minute | Per IP address | Your per-key rate limit is visible in the `GET /v1/account` response under `api_keys[].rate_limit`. ## Rate limit headers [Section titled “Rate limit headers”](#rate-limit-headers) Every API key-authenticated response includes rate limit headers: | Header | Description | | ----------------------- | ----------------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the current window | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets | ## When you exceed the limit [Section titled “When you exceed the limit”](#when-you-exceed-the-limit) You’ll receive a `429 Too Many Requests` response: ```json { "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded", "details": { "retry_after_seconds": 45 }, "suggestion": "Retry after 45 seconds. Check X-RateLimit-Reset header for exact reset time." } } ``` See the full error format in the [Error Codes](/errors/) reference. ## Best practices [Section titled “Best practices”](#best-practices) * **Check headers proactively.** Monitor `X-RateLimit-Remaining` and slow down before hitting the limit rather than reacting to 429 errors. * **Use exponential backoff.** When you receive a 429, wait for `retry_after_seconds` before retrying. If you get repeated 429s, increase the wait time exponentially. * **Batch where possible.** A single search across multiple jurisdictions is one request, not one per jurisdiction. * **Cache results.** Entity lookups (`GET /v1/entity/:id`) are free and don’t count against your rate limit. Use them to retrieve previously verified entities instead of re-verifying. ## Need higher limits? [Section titled “Need higher limits?”](#need-higher-limits) If your use case requires more than 60 requests/minute, contact us at to discuss custom rate limits. # API Versioning > BizVerify's API versioning policy and backward compatibility commitment. ## Current version [Section titled “Current version”](#current-version) The BizVerify API is currently on **v1**. All endpoints are prefixed with `/v1/`. ## Backward compatibility [Section titled “Backward compatibility”](#backward-compatibility) Within a major version, we commit to **no breaking changes**. This means: * Existing fields in responses will not be removed or change type * Existing endpoints will not be removed * Required request parameters will not be added to existing endpoints * Error codes will not change meaning We may add new fields to responses, new optional parameters, new endpoints, and new error codes. These are backward-compatible changes — your existing integrations will continue to work. ## How we communicate changes [Section titled “How we communicate changes”](#how-we-communicate-changes) * **New features and additions** are announced in the [Changelog](/changelog/) * **Deprecations** are announced at least 90 days before removal, marked in the changelog and API responses * **Breaking changes** only happen in a new major version (e.g., v2), with a migration guide and overlap period where both versions are available ## OpenAPI spec [Section titled “OpenAPI spec”](#openapi-spec) The machine-readable API specification is always available at: ```plaintext https://api.bizverify.co/v1/openapi.json ``` Use this to auto-generate clients, validate requests, or stay up to date with the latest schema. The interactive API reference is at [api.bizverify.co/reference](https://api.bizverify.co/reference). # Jurisdiction Coverage > Moved — see the live Coverage page. This page has moved. The current list of supported jurisdictions and their verification tiers is now on the [Coverage](/coverage/) page, which is powered by live data from [`/v1/config`](https://api.bizverify.co/v1/config). # MCP Setup > Connect BizVerify to Claude, Cursor, VS Code, and other AI tools via the Model Context Protocol. The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets AI tools call BizVerify directly. No SDK or code needed — paste a config snippet and start verifying businesses in natural language. ## Connect your AI tool [Section titled “Connect your AI tool”](#connect-your-ai-tool) * Claude Desktop Add to your Claude Desktop config (`claude_desktop_config.json`): ```json { "mcpServers": { "bizverify": { "type": "streamable-http", "url": "https://api.bizverify.co/mcp" } } } ``` * Claude Code Run in your terminal: ```bash claude mcp add bizverify --transport streamable-http https://api.bizverify.co/mcp ``` * Cursor Add to your Cursor MCP config: ```json { "mcpServers": { "bizverify": { "type": "streamable-http", "url": "https://api.bizverify.co/mcp" } } } ``` * VS Code / Copilot Add to `.vscode/mcp.json` in your workspace: ```json { "servers": { "bizverify": { "type": "streamable-http", "url": "https://api.bizverify.co/mcp" } } } ``` * ChatGPT ```plaintext Settings → Connected Tools → Add MCP Server URL: https://api.bizverify.co/mcp Auth: OAuth (automatic) ``` * Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "bizverify": { "serverUrl": "https://api.bizverify.co/mcp" } } } ``` Tip **Other tools** (Cline, Continue.dev, JetBrains IDEs) also support Streamable HTTP. Use `https://api.bizverify.co/mcp` as the server URL. For **stdio-only clients**, use the `mcp-remote` bridge: ```bash npx mcp-remote https://api.bizverify.co/mcp ``` ## Authentication [Section titled “Authentication”](#authentication) On first use, your AI tool will prompt you to authenticate via OAuth. This is automatic — follow the browser flow to log in and authorize. Alternatively, pass your API key directly via the `X-API-Key` header if your tool supports custom headers. ## Available tools [Section titled “Available tools”](#available-tools) | Tool | Auth | Cost | Description | | -------------------- | ---- | ---------------------- | ------------------------------------------- | | `get_config` | No | Free | Get service config (jurisdictions, pricing) | | `list_jurisdictions` | No | Free | List all supported jurisdictions | | `verify_business` | Yes | 1-25 credits | Verify a business entity | | `search_entities` | Yes | 2 credits/jurisdiction | Search for entities by name | | `check_job_status` | Yes | Free | Check async verification job status | | `get_entity` | Yes | Free | Get cached entity details | | `get_entity_history` | Yes | 5 credits | Get historical verification snapshots | | `get_account` | Yes | Free | View account details and credit balance | | `purchase_credits` | Yes | Free | Get a Stripe checkout link to buy credits | ## Example prompts [Section titled “Example prompts”](#example-prompts) Try these with any connected AI tool: * *“Verify that Acme Corporation is an active business in Florida”* * *“Search for Tesla in Delaware and Texas”* * *“Check if my verification job job\_abc123 has completed”* * *“How many credits do I have left?”* * *“What jurisdictions does BizVerify support?”* ## Learn more [Section titled “Learn more”](#learn-more) * [BizVerify MCP landing page](https://bizverify.co/mcp) — visual overview and demo * [Model Context Protocol docs](https://modelcontextprotocol.io) — MCP specification # Quickstart > Get your API key and make your first business verification request in under 5 minutes. 1. **Get an API key** Send your email to get a one-time verification code: ```bash curl -X POST https://api.bizverify.co/v1/auth/request-access \ -H "Content-Type: application/json" \ -d '{"email": "you@company.com", "accept_terms": true}' ``` Check your inbox for the 6-digit code, then exchange it for an API key: ```bash curl -X POST https://api.bizverify.co/v1/auth/verify-access \ -H "Content-Type: application/json" \ -d '{"email": "you@company.com", "code": "123456", "label": "my-app"}' ``` Response: ```json { "api_key": "bv_live_abc123...", "key_id": "key_xyz", "label": "my-app" } ``` Caution Store your API key securely. It is shown only once. 2. **Verify a business** * cURL ```bash curl -X POST https://api.bizverify.co/v1/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: bv_live_abc123..." \ -d '{"entity_name": "Acme Corporation", "jurisdiction": "us-fl"}' ``` * Node.js ```ts import { BizVerify } from '@bizverify/sdk'; const biz = new BizVerify({ apiKey: 'bv_live_abc123...' }); const result = await biz.verification.verify({ entity_name: 'Acme Corporation', jurisdiction: 'us-fl', }); ``` * Python ```python from bizverify import BizVerify client = BizVerify(api_key="bv_live_abc123...") result = client.verification.verify("Acme Corporation", "us-fl") ``` If the result is cached, you get an immediate response with `status: "completed"`. Otherwise, you get `status: "pending"` with a `job_id`. 3. **Poll for results (if async)** If you received a `job_id`, poll until the job completes: ```bash curl https://api.bizverify.co/v1/verify/status/job_abc123 \ -H "X-API-Key: bv_live_abc123..." ``` Response when complete: ```json { "status": "completed", "job_id": "job_abc123", "entity_id": "ent_xyz", "data": { "entity_name": "ACME CORPORATION", "entity_type": "corporation", "status": "active", "jurisdiction": "us-fl", "formation_date": "2015-03-12", "confidence": 95 } } ``` Tip Use [webhooks](/webhooks/) instead of polling to get notified when a job completes. ## Credit costs [Section titled “Credit costs”](#credit-costs) Operations cost between 1-25 credits. Entity lookups and job status checks are free. See [Credits & Billing](/guides/billing/) for the full pricing table. ## Next steps [Section titled “Next steps”](#next-steps) * [Authentication](/authentication/) — API keys, key management * [Credits & Billing](/guides/billing/) — Credit costs and purchasing * [Async Jobs](/guides/async-jobs/) — Polling, webhooks, and the job lifecycle * [Error Codes](/errors/) — Handle errors gracefully * [Jurisdiction Coverage](/jurisdictions/) — All supported jurisdictions * [MCP Setup](/mcp/setup/) — Use BizVerify from AI tools * [OpenAPI Spec](https://api.bizverify.co/v1/openapi.json) — Machine-readable API schema * [API Reference](https://api.bizverify.co/reference) — Full endpoint documentation # Go SDK > Install and use the BizVerify Go SDK with zero external dependencies. ## Installation [Section titled “Installation”](#installation) ```bash go get github.com/bizverify/bizverify-go ``` Requires Go 1.21+. Zero external dependencies (stdlib only). ## Quick start [Section titled “Quick start”](#quick-start) ```go package main import ( "context" "fmt" "log" bv "github.com/bizverify/bizverify-go" ) func main() { client := bv.New(bv.WithAPIKey("bv_live_...")) resp, err := client.Verification.Verify(context.Background(), bv.VerifyParams{ EntityName: "Acme Corporation", Jurisdiction: "us-fl", }) if err != nil { log.Fatal(err) } fmt.Println(resp.Status) } ``` ## Passwordless auth [Section titled “Passwordless auth”](#passwordless-auth) ```go ctx := context.Background() client := bv.New() // Step 1: Request a code _, err := client.Auth.RequestAccess(ctx, "you@example.com", true) // Step 2: Verify code — client auto-configures resp, err := client.Auth.VerifyAccess(ctx, "you@example.com", "123456", nil) fmt.Println("API Key:", resp.APIKey) ``` ## Verify and wait [Section titled “Verify and wait”](#verify-and-wait) ```go job, err := client.Verification.VerifyAndWait(ctx, bv.VerifyParams{ EntityName: "Acme Inc", Jurisdiction: "us-fl", }, &bv.PollOptions{ PollInterval: 2 * time.Second, Timeout: 120 * time.Second, OnStatusChange: func(status *bv.JobStatusResponse) { log.Printf("Status: %s\n", status.Status) }, }) ``` ## Search [Section titled “Search”](#search) ```go // Single page resp, err := client.Search.Find(ctx, bv.SearchParams{ EntityName: "Acme", Jurisdiction: "us-fl", Limit: 10, }) for _, r := range resp.Results { fmt.Printf("%s (confidence: %d%%)\n", r.EntityName, r.Confidence) } // Auto-paginate iter := client.Search.FindAll(ctx, bv.SearchParams{EntityName: "Acme"}) for iter.Next() { r := iter.Value() fmt.Println(r.EntityName) } if err := iter.Err(); err != nil { log.Fatal(err) } ``` ## Fetch a cached entity [Section titled “Fetch a cached entity”](#fetch-a-cached-entity) Retrieve a previously verified business by its entity ID (returned from a `Verify` or `Search` call) — free, no credits charged. ```go entity, err := client.Entities.Get(ctx, "550e8400-e29b-41d4-a716-446655440000") if err != nil { log.Fatal(err) } fmt.Println(entity.EntityName, entity.Status) if entity.RegisteredAgent != nil { fmt.Println(entity.RegisteredAgent.Name) } fmt.Println(entity.Officers) if entity.LastVerifiedAt != nil { fmt.Println(*entity.LastVerifiedAt) } ``` ## Configuration [Section titled “Configuration”](#configuration) ```go client := bv.New( bv.WithAPIKey("bv_live_..."), bv.WithBaseURL("https://api.bizverify.co"), bv.WithMaxRetries(2), bv.WithTimeout(30 * time.Second), ) ``` ## Error handling [Section titled “Error handling”](#error-handling) ```go import "errors" entity, err := client.Entities.Get(ctx, "ent_nonexistent") if err != nil { var notFound *bv.NotFoundError var noCredits *bv.InsufficientCreditsError var rateLimit *bv.RateLimitError switch { case errors.As(err, ¬Found): fmt.Printf("Not found: %s\n", notFound.Message) case errors.As(err, &noCredits): fmt.Println("Need more credits") case errors.As(err, &rateLimit): fmt.Printf("Retry after %ds\n", rateLimit.RetryAfter) default: log.Fatal(err) } } ``` ## Full reference [Section titled “Full reference”](#full-reference) See the [SDK README on GitHub](https://github.com/bizverify/bizverify-go) for the complete API. # Node.js SDK > Install and use the BizVerify Node.js SDK to verify businesses, search entities, and manage your account. ## Installation [Section titled “Installation”](#installation) ```bash npm install @bizverify/sdk ``` Requires Node.js 18+. Zero runtime dependencies. ## Quick start [Section titled “Quick start”](#quick-start) ```ts import { BizVerify } from '@bizverify/sdk'; const biz = new BizVerify({ apiKey: 'bv_live_...' }); // Verify a business const result = await biz.verification.verify({ entity_name: 'Acme Corporation', jurisdiction: 'us-fl', }); console.log(result.status, result.data); ``` ## Passwordless auth [Section titled “Passwordless auth”](#passwordless-auth) If you don’t have an API key yet: ```ts const biz = new BizVerify(); // Step 1: Request a code await biz.auth.requestAccess({ email: 'you@company.com', accept_terms: true, }); // Step 2: Verify code — client auto-configures const { api_key } = await biz.auth.verifyAccess({ email: 'you@company.com', code: '123456', label: 'my-app', }); ``` ## Verify and wait [Section titled “Verify and wait”](#verify-and-wait) For async verifications, `verifyAndWait` polls until complete: ```ts const job = await biz.verification.verifyAndWait( { entity_name: 'Acme Inc', jurisdiction: 'us-fl' }, { timeoutMs: 120_000, onStatusChange: (status) => console.log(status.status), }, ); console.log(job.data); ``` ## Search [Section titled “Search”](#search) ```ts // Single page const page = await biz.search.find({ entity_name: 'Acme', jurisdiction: 'us-fl', limit: 10, }); // Auto-paginate for await (const entity of biz.search.findAll({ entity_name: 'Acme' })) { console.log(entity.entity_name, entity.confidence); } ``` ## Fetch a cached entity [Section titled “Fetch a cached entity”](#fetch-a-cached-entity) Retrieve a previously verified business by its `entity_id` (returned from a `verify` or `search` call) — free, no credits charged. ```ts const entity = await biz.entities.get('550e8400-e29b-41d4-a716-446655440000'); console.log(entity.entity_name, entity.status); console.log(entity.registered_agent?.name); console.log(entity.officers); console.log(entity.last_verified_at); ``` ## Configuration [Section titled “Configuration”](#configuration) ```ts const biz = new BizVerify({ apiKey: 'bv_live_...', baseUrl: 'https://api.bizverify.co', // default maxRetries: 2, // retries on 5xx timeout: 30_000, // milliseconds }); ``` ## Credit tracking [Section titled “Credit tracking”](#credit-tracking) ```ts const meta = biz.lastResponseMeta; console.log(meta?.creditsRemaining, meta?.creditsCharged); ``` ## Error handling [Section titled “Error handling”](#error-handling) ```ts import { BizVerify, AuthenticationError, InsufficientCreditsError, RateLimitError, NotFoundError, ValidationError, } from '@bizverify/sdk'; try { await biz.verification.verify({ entity_name: 'Test', jurisdiction: 'us-fl' }); } catch (err) { if (err instanceof InsufficientCreditsError) { console.log('Need more credits'); } else if (err instanceof RateLimitError) { console.log(`Retry after ${err.retryAfter}s`); } else if (err instanceof AuthenticationError) { console.log('Invalid API key'); } else if (err instanceof NotFoundError) { console.log('Not found'); } else if (err instanceof ValidationError) { console.log('Invalid request:', err.details); } } ``` ## Full reference [Section titled “Full reference”](#full-reference) See the [SDK README on GitHub](https://github.com/bizverify/bizverify-node) for the complete API. # Python SDK > Install and use the BizVerify Python SDK with sync and async support. ## Installation [Section titled “Installation”](#installation) ```bash pip install bizverify ``` Requires Python 3.9+. ## Quick start [Section titled “Quick start”](#quick-start) ```python from bizverify import BizVerify client = BizVerify(api_key="bv_live_...") result = client.verification.verify("Acme Corporation", "us-fl") print(result.status, result.data) ``` ## Async support [Section titled “Async support”](#async-support) ```python from bizverify import AsyncBizVerify import asyncio async def main(): async with AsyncBizVerify(api_key="bv_live_...") as client: result = await client.verification.verify("Acme Corporation", "us-fl") print(result.data) asyncio.run(main()) ``` ## Passwordless auth [Section titled “Passwordless auth”](#passwordless-auth) ```python client = BizVerify() # Step 1: Request a code client.auth.request_access("you@company.com", accept_terms=True) # Step 2: Verify code — client auto-configures resp = client.auth.verify_access("you@company.com", "123456", label="my-app") print(resp.api_key) ``` ## Verify and wait [Section titled “Verify and wait”](#verify-and-wait) ```python job = client.verification.verify_and_wait( "Acme Inc", "us-fl", timeout=60.0, on_status_change=lambda s: print(s.status), ) print(job.data) ``` ## Search [Section titled “Search”](#search) ```python # Single page response = client.search.find("Acme", jurisdiction="us-fl", limit=10) for result in response.results: print(result.entity_name, result.confidence) # Auto-paginate for result in client.search.find_all("Acme"): print(result.entity_name) # Async auto-paginate async for result in client.search.find_all("Acme"): print(result.entity_name) ``` ## Fetch a cached entity [Section titled “Fetch a cached entity”](#fetch-a-cached-entity) Retrieve a previously verified business by its `entity_id` (returned from a `verify` or `search` call) — free, no credits charged. ```python entity = client.entities.get("550e8400-e29b-41d4-a716-446655440000") print(entity.entity_name, entity.status) print(entity.registered_agent.name if entity.registered_agent else None) print(entity.officers) print(entity.last_verified_at) ``` ## Configuration [Section titled “Configuration”](#configuration) ```python client = BizVerify( api_key="bv_live_...", base_url="https://api.bizverify.co", # default max_retries=2, # retries on 5xx timeout=30.0, # seconds ) # Context manager for automatic cleanup with BizVerify(api_key="bv_live_...") as client: result = client.verification.verify("Acme Inc", "us-fl") ``` ## Error handling [Section titled “Error handling”](#error-handling) ```python from bizverify import ( BizVerify, AuthenticationError, InsufficientCreditsError, RateLimitError, NotFoundError, ValidationError, BizVerifyError, ) try: result = client.verification.verify("Acme", "us-fl") except InsufficientCreditsError: print("Need more credits") except RateLimitError as e: print(f"Retry after {e.retry_after}s") except AuthenticationError: print("Invalid API key") except NotFoundError: print("Not found") except ValidationError as e: print(f"Invalid request: {e.message}") except BizVerifyError as e: print(f"Error {e.code}: {e.message}") ``` ## Full reference [Section titled “Full reference”](#full-reference) See the [SDK README on GitHub](https://github.com/bizverify/bizverify-python) for the complete API. # Webhooks > Receive real-time notifications when verification jobs complete, fail, or are refunded. ## Overview [Section titled “Overview”](#overview) Instead of polling `GET /v1/verify/status/:jobId`, you can provide a `webhook_url` when creating a verification request. BizVerify will `POST` to that URL when the job completes, fails, or is refunded. ## Setting up webhooks [Section titled “Setting up webhooks”](#setting-up-webhooks) Include `webhook_url` in your verification request: ```bash curl -X POST https://api.bizverify.co/v1/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: bv_live_abc123..." \ -d '{ "entity_name": "Acme Corporation", "jurisdiction": "us-fl", "webhook_url": "https://your-app.com/webhooks/bizverify" }' ``` ## Events [Section titled “Events”](#events) ### `verification.completed` [Section titled “verification.completed”](#verificationcompleted) Sent when a verification job finishes successfully. ```json { "event": "verification.completed", "job_id": "job_abc123", "jurisdiction": "us-fl", "entity_name": "Acme Corporation", "status": "completed", "data": { "entity_name": "ACME CORPORATION", "entity_type": "corporation", "status": "active", "jurisdiction": "us-fl", "formation_date": "2015-03-12", "confidence": 95 }, "credits_charged": 1, "credits_refunded": false, "completed_at": "2026-04-02T12:00:00.000Z" } ``` ### `verification.failed` [Section titled “verification.failed”](#verificationfailed) Sent when a verification job fails after all retry attempts. ```json { "event": "verification.failed", "job_id": "job_abc123", "jurisdiction": "us-fl", "entity_name": "Acme Corporation", "status": "failed", "error": "Data source timeout: the official source did not respond in time", "credits_charged": 1, "credits_refunded": true, "completed_at": "2026-04-02T12:05:00.000Z" } ``` Note Credits are automatically refunded when a job fails. ### `verification.refunded` [Section titled “verification.refunded”](#verificationrefunded) Sent when an admin manually refunds credits for a job. ```json { "event": "verification.refunded", "job_id": "job_abc123", "jurisdiction": "us-fl", "entity_name": "Acme Corporation", "status": "refunded", "credits_charged": 1, "credits_refunded": true, "completed_at": "2026-04-02T12:10:00.000Z" } ``` ## Delivery details [Section titled “Delivery details”](#delivery-details) | Property | Value | | ------------ | -------------------------------------------- | | Method | `POST` | | Content-Type | `application/json` | | User-Agent | `BizVerify-Webhook/1.0` | | Signature | `X-BizVerify-Signature` header (HMAC-SHA256) | | Timeout | 10 seconds per attempt | | Retries | 3 retries (4 total attempts) | | Backoff | Exponential (1s, 2s, 4s) | | Success | Any `2xx` HTTP response | ## Verifying signatures [Section titled “Verifying signatures”](#verifying-signatures) Every webhook delivery includes an `X-BizVerify-Signature` header containing an HMAC-SHA256 signature of the request body. Use this to verify that the request came from BizVerify and hasn’t been tampered with. The header format is `sha256=`. ```javascript import { createHmac, timingSafeEqual } from 'node:crypto'; function verifyWebhookSignature(body, signature, secret) { const expected = 'sha256=' + createHmac('sha256', secret) .update(body) .digest('hex'); return timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } // In your webhook handler: app.post('/webhooks/bizverify', (req, res) => { const signature = req.headers['x-bizverify-signature']; if (!signature || !verifyWebhookSignature(req.rawBody, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process the webhook payload const event = req.body; console.log(`Received ${event.event} for job ${event.job_id}`); res.status(200).send('OK'); }); ``` Tip Your webhook secret is available in your BizVerify dashboard. Use `timingSafeEqual` (not `===`) to prevent timing attacks. ## Best practices [Section titled “Best practices”](#best-practices) * **Respond quickly.** Return a `200` status immediately and process the payload asynchronously. If your endpoint takes longer than 10 seconds, the delivery will be marked as failed and retried. * **Handle duplicates.** In rare cases (e.g., network issues), you may receive the same event more than once. Use `job_id` as an idempotency key. * **Verify the signature.** Always verify the `X-BizVerify-Signature` header to ensure the request came from BizVerify. See [Verifying signatures](#verifying-signatures) above. * **Use HTTPS.** Always use an `https://` URL for your webhook endpoint.