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.