# Account API
Source: https://docs.bouncewatch.com/api-reference/account
Manage your account, credits, and usage
## Overview
The Account API allows you to manage your API account, check credit balance, monitor usage, and configure webhooks.
## Endpoints
| Endpoint | Description |
| ------------------------------ | ------------------------- |
| `GET /api/v1/account/info` | Get account information |
| `GET /api/v1/account/credits` | Check credit balance |
| `GET /api/v1/account/usage` | View usage statistics |
| `GET /api/v1/account/history` | Paginated request history |
| `POST /api/v1/account/webhook` | Set your webhook URL |
All account endpoints are free — they never consume credits.
***
## Get Account Info
Retrieve your account details and plan information.
```
GET /api/v1/account/info
```
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/info" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/info', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const data = await response.json();
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/info',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/info');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"data": {
"account": {
"id": 1042,
"name": "John Doe",
"email": "john@company.com",
"company_name": "Acme Corp",
"plan_type": "professional",
"status": "active",
"created_at": "2026-01-15T10:00:00.000000Z"
},
"limits": {
"credits_per_month": 25000,
"rate_per_minute": 150,
"rate_per_day": 5000
},
"plan_defaults": {
"credits_per_month": 25000,
"rate_per_minute": 150,
"rate_per_day": 5000
},
"rate_limits": {
"per_minute": 150,
"per_day": 5000
}
}
}
```
| Field | Description |
| --------------- | ---------------------------------------------------------------------------------------------------------- |
| `limits` | What is **actually enforced** on your account. Read this one. |
| `plan_defaults` | The catalogue values for your plan. Differs from `limits` only if a limit was raised for you individually. |
| `rate_limits` | The rate half of `limits`, kept for backwards compatibility. |
***
## Get Credit Balance
Check your current credit balance and usage.
```
GET /api/v1/account/credits
```
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/credits" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/credits', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/credits',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/credits');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"data": {
"credits": {
"balance": 21000,
"used": 4000,
"limit": 25000,
"total_purchased": 0,
"percentage_used": 16,
"resets_at": "2026-02-01T09:14:22.000000Z"
}
}
}
```
| Field | Description |
| ----------------- | --------------------------------------------------------------------- |
| `balance` | Credits you can still spend this period. This is the number to watch. |
| `used` | Credits consumed this period. Returns to `0` at each renewal. |
| `limit` | Your plan's allowance for the period. |
| `total_purchased` | Lifetime total ever added by purchase. `0` on a free trial. |
| `percentage_used` | `used / limit`, as a percentage of this period's allowance. |
| `resets_at` | When the allowance was last renewed. `null` if it never has been. |
**Credits do not roll over.** Each period starts at `limit`, whatever was left of the
previous one — 25,000 with 8,000 unspent renews at 25,000, not 33,000.
***
## Get Usage Statistics
View detailed usage statistics for your account.
```
GET /api/v1/account/usage
```
This endpoint always reports the **current calendar month** and takes no parameters.
For a longer or arbitrary window, page through [`/account/history`](#get-request-history).
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/usage" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.bouncewatch.com/api/v1/account/usage',
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/usage',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/usage');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"data": {
"current_limits": {
"requests_today": 45,
"requests_this_minute": 2,
"last_request": "2026-02-10T14:22:05.000000Z"
},
"monthly_usage": {
"requests": 1250,
"credits_consumed": 3200,
"period": {
"start": "2026-02-01T00:00:00.000000Z",
"end": "2026-02-28T23:59:59.999999Z"
}
},
"top_endpoints": [
{
"endpoint": "/api/v1/company/stripe.com",
"requests": 42,
"credits_used": 520,
"avg_credits_per_request": 12.38
}
],
"recent_requests": [
{
"id": 90211,
"endpoint": "/api/v1/company/stripe.com",
"method": "GET",
"credits_consumed": 26,
"response_status": 202,
"response_time": 0.184,
"error_message": null,
"requested_at": "2026-02-10T14:22:05.000000Z"
}
]
}
}
```
| Field | Description |
| ----------------- | ------------------------------------- |
| `current_limits` | Live counters behind your rate limits |
| `monthly_usage` | Totals for the current calendar month |
| `top_endpoints` | Your ten busiest endpoints this month |
| `recent_requests` | Your last 20 requests |
***
## Get Request History
Page through your full request log, newest first.
```
GET /api/v1/account/history
```
Records to return. Capped at 100.
Records to skip.
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/history?limit=50&offset=0" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.bouncewatch.com/api/v1/account/history?limit=50&offset=0',
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/history',
headers={'X-API-Key': 'YOUR_API_KEY'},
params={'limit': 50, 'offset': 0}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/history?limit=50&offset=0');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"data": {
"requests": [
{
"id": 90211,
"endpoint": "/api/v1/company/stripe.com",
"method": "GET",
"credits_consumed": 26,
"response_status": 202,
"response_time": 0.184,
"requested_at": "2026-02-10T14:22:05.000000Z"
}
],
"pagination": {
"total": 1250,
"limit": 50,
"offset": 0,
"has_more": true
}
}
}
```
***
## Set Your Webhook URL
Store a default webhook URL on your account, so you don't have to send
`X-Webhook-URL` on every request.
```
POST /api/v1/account/webhook
```
Your webhook endpoint. Must be `https://` — plain http is rejected.
**There is nothing to subscribe to.** One URL receives every event for your account:
`enrichment.completed`, `enrichment.failed` and `enrichment.no_data_found`. There is no
`events` array and no per-event filtering — see [Webhooks](/webhooks) for the payloads.
Placeholder hosts are rejected with `422 invalid_webhook_url`. That includes
`example.com`, `your-domain.com`, `localhost` and `127.0.0.1` — so don't paste the
literal example from a tutorial. Use a [webhook.site](https://webhook.site) URL to test.
```bash cURL theme={null}
curl -X POST "https://api.bouncewatch.com/api/v1/account/webhook" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://webhook.site/YOUR-ID"}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/webhook', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ webhook_url: 'https://webhook.site/YOUR-ID' })
});
```
```python Python theme={null}
response = requests.post(
'https://api.bouncewatch.com/api/v1/account/webhook',
headers={
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
json={'webhook_url': 'https://webhook.site/YOUR-ID'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/webhook');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'webhook_url' => 'https://webhook.site/YOUR-ID'
]));
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"message": "Webhook URL updated successfully",
"data": {
"webhook_url": "https://webhook.site/YOUR-ID"
}
}
```
**Your signing secret is not returned here.** It is generated once with your account and
shown in the [API Panel → Webhooks](https://bouncewatch.com/api-panel/webhooks) page,
where you can also rotate it.
Learn how to handle and verify webhook events →
# Get account info
Source: https://docs.bouncewatch.com/api-reference/account/get-account-info
/openapi.json get /account/info
Your account, plan and the limits actually enforced on it. Free.
Read `limits` — it is what is enforced. `plan_defaults` is the catalogue value for your plan and differs only if a limit was raised for you individually.
# Get credit balance
Source: https://docs.bouncewatch.com/api-reference/account/get-credit-balance
/openapi.json get /account/credits
What is left this period. Free.
**Credits do not roll over.** Each period restarts at `limit` — 25,000 with 8,000 unspent renews at 25,000, not 33,000. The one exception is a mid-period upgrade, which keeps the remaining balance and adds the new plan's allowance on top.
There is no credit-threshold webhook to subscribe to; poll this endpoint if you want an alert.
# Get request history
Source: https://docs.bouncewatch.com/api-reference/account/get-request-history
/openapi.json get /account/history
Every request this key has made, newest first. Free.
# Get usage statistics
Source: https://docs.bouncewatch.com/api-reference/account/get-usage-statistics
/openapi.json get /account/usage
Aggregate usage for the current period. Free.
This is a summary and takes no parameters — it does not paginate. For the individual calls, use `GET /account/history`.
# Set your webhook URL
Source: https://docs.bouncewatch.com/api-reference/account/set-your-webhook-url
/openapi.json post /account/webhook
Store one webhook URL on the account so you do not have to send `X-Webhook-URL` on every call. Free.
**There is nothing to subscribe to.** That one URL receives every event for the account — `enrichment.completed`, `enrichment.failed`, `enrichment.no_data_found`. No `events` array, no per-event filtering.
Your signing secret is not returned here; it lives in the API Panel, where you can also rotate it.
# Company API
Source: https://docs.bouncewatch.com/api-reference/company-api
Enrich any company domain with comprehensive data
## Overview
The Company API is the primary endpoint for fetching company data. Send a domain and get enriched data — it's that simple.
**How it works:** The API automatically handles everything. If data exists from the last 24 hours, you get it instantly. Otherwise, a fresh enrichment is triggered and results are delivered via webhook.
**Webhook required:** All new enrichment requests require a webhook URL. Configure it in your [API Panel](https://bouncewatch.com/api-panel/webhooks) or send it via the `X-Webhook-URL` header. For quick testing, get a free URL from [webhook.site](https://webhook.site).
## Endpoint
Company domain (e.g., `stripe.com`, `openai.com`)
```
GET /api/v1/company/{domain}
```
## Parameters
Comma-separated list of enrichment modules to include.
Available modules: `business`, `technology`, `funding`, `team`, `signals`, `competitors`
## How It Works
```mermaid theme={null}
flowchart TD
A[GET /company/domain.com] --> B{Enriched\nwithin 24h?}
B -->|Yes| C[200 OK\nCached data, 0 credits]
B -->|No| J{Already\nenriching?}
J -->|Yes| K[409 Conflict\nenrichment_in_progress]
J -->|No| D{https webhook\nconfigured?}
D -->|No| F[400 Bad Request\nwebhook_required]
D -->|Yes| E[202 Accepted\nJobs queued, credits reserved]
E --> G[Background enrichment\n2-10 minutes]
G --> H[Results POSTed to your webhook]
G --> I[Or poll /enrichment/batch_id/status]
```
**409 is normal, not an error to alarm on.** We deduplicate per domain, so a second
request while one is in flight — or within 24 hours of a completed one — returns 409
rather than charging you twice. `recently_enriched` means "ask this endpoint again and
the data comes back free".
### Scenario 1: Recent Data Available (Instant Response)
If the same domain was enriched within the last 24 hours, you get the data instantly at **no additional cost**:
```json theme={null}
{
"success": true,
"data": {
"company": { ... },
"funding": { ... }
},
"credits_used": 0,
"from_recent_enrichment": true,
"credits_remaining": 4500,
"webhook_hint": "To enrich new domains, configure a webhook URL..."
}
```
### Scenario 2: Fresh Enrichment (Async Response)
If no recent data exists, enrichment is triggered automatically and you get `202`.
**Two shapes, depending on whether we already hold the company.** Both always carry
`success`, `batch_id`, `credits_reserved`, `status_endpoint` and `results_endpoint` —
key your integration on those.
```json theme={null}
{
"success": true,
"status": "processing",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"current_data": { "company": { }, "funding": { } },
"enrichment_status": "processing",
"modules_requested": ["business", "funding"],
"modules_count": 2,
"webhook_notification": "enabled",
"status_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/status",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results",
"credits_reserved": 26,
"message": "Enrichment jobs queued successfully. Fresh data will be sent to your webhook when ready.",
"hint": "Results will be delivered to your webhook URL. You can also poll the status_endpoint to check progress."
}
```
`current_data` holds whatever we already had — possibly stale, possibly incomplete.
It is there so you have something to show while the refresh runs.
```json theme={null}
{
"success": true,
"message": "New domain added to enrichment queue",
"domain": "stripe.com",
"enrichment_status": "processing",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"requested_modules": ["business", "funding"],
"credits_reserved": 26,
"credits_remaining": 4974,
"webhook_notification": "enabled",
"status_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/status",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results"
}
```
Differences from the other shape: the module list is called `requested_modules`,
there is no `current_data` (we have nothing yet), and `credits_remaining` is included.
## Request Examples
Get basic company information (10 credits):
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Webhook-URL: https://your-server.com/webhook"
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
{ headers: {
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
}}
);
const data = await response.json();
if (response.status === 200) {
// Instant data (from 24h cache)
console.log(data.data.company);
} else if (response.status === 202) {
// Enrichment queued - wait for webhook
console.log(`Batch: ${data.batch_id}`);
}
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
}
)
data = response.json()
if response.status_code == 200:
# Instant data (from 24h cache)
print(data['data']['company'])
elif response.status_code == 202:
# Enrichment queued
print(f"Batch: {data['batch_id']}")
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/company/stripe.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'X-Webhook-URL: https://your-server.com/webhook'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$data = json_decode($response, true);
if ($httpCode === 200) {
// Instant data (from 24h cache)
print_r($data['data']['company']);
} elseif ($httpCode === 202) {
// Enrichment queued
echo "Batch: " . $data['batch_id'];
}
```
Add funding and team data (26 credits):
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=funding,team" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Webhook-URL: https://your-server.com/webhook"
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=funding,team',
{ headers: {
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
}}
);
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
},
params={'enrich': 'funding,team'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=funding,team');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'X-Webhook-URL: https://your-server.com/webhook'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
Get all available data (60 credits):
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=business,technology,funding,team,signals,competitors" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Webhook-URL: https://your-server.com/webhook"
```
```javascript JavaScript theme={null}
const modules = ['business', 'technology', 'funding', 'team', 'signals', 'competitors'];
const response = await fetch(
`https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=${modules.join(',')}`,
{ headers: {
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
}}
);
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://your-server.com/webhook'
},
params={'enrich': 'business,technology,funding,team,signals,competitors'}
)
```
```php PHP theme={null}
$modules = ['business', 'technology', 'funding', 'team', 'signals', 'competitors'];
$url = 'https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=' . implode(',', $modules);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'X-Webhook-URL: https://your-server.com/webhook'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
## Webhook Setup
**Quick testing tip:** Don't have a webhook endpoint yet? Go to [webhook.site](https://webhook.site) and copy your unique URL. Use it as the `X-Webhook-URL` header value to see enrichment results instantly.
You can set your webhook URL in two ways:
1. **Per-request:** Add `X-Webhook-URL` header to your request
2. **Permanently:** Configure it in your [API Panel → Webhooks](https://bouncewatch.com/api-panel/webhooks)
When enrichment completes, we POST to your webhook with full results:
```json theme={null}
{
"event": "enrichment.completed",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "completed",
"requested_modules": ["business", "funding"],
"requested_at": "2025-11-23T10:00:00Z",
"completed_at": "2025-11-23T10:05:30Z",
"duration_seconds": 330,
"credits": {
"reserved": 26,
"used": 26,
"refunded": 0
},
"data_url": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results",
"_meta": {
"api_version": "2.0",
"webhook_attempt": 1
}
}
```
The webhook payload does not contain enrichment data directly. Use the `data_url` with your API key to fetch the full results.
## 24-Hour Smart Cache
When you query a domain that was enriched within the last 24 hours:
* **Response:** Instant (200 OK, \< 250ms)
* **Cost:** Free (0 credits)
* **Webhook:** Not required
This means your typical workflow is:
1. **First request:** Enrichment triggers (202), webhook delivers results
2. **Subsequent requests within 24h:** Instant data at no cost
This replaces the old "cached mode" — you don't need any special parameters. The API automatically serves cached data when available.
## Credit Costs
| Module | Credits | What's Included? |
| ------------- | ------- | --------------------------------------- |
| Base (always) | 10 | Company info, location, social, legal |
| `business` | +6 | Industry, business model, target market |
| `technology` | +4 | Tech stack, 100+ technologies |
| `funding` | +10 | Funding rounds, investors, valuations |
| `team` | +6 | Team members, hiring status |
| `signals` | +16 | 40+ signal types, highlights |
| `competitors` | +8 | Competitors, similar companies |
## Processing Times
| Modules Requested | Estimated Time |
| ----------------- | -------------- |
| Base only | 1-2 minutes |
| 1-2 modules | 3-5 minutes |
| 3-4 modules | 5-8 minutes |
| All modules | 8-12 minutes |
Processing times depend on data availability. Companies with more online presence are typically faster to enrich.
Monitor batch status and retrieve results →
See all response fields and their types in detail →
# Get company data
Source: https://docs.bouncewatch.com/api-reference/company/get-company-data
/openapi.json get /company/{domain}
Returns company data for a domain, enriching it first when we do not already hold something recent.
**Two outcomes, and both are success:**
- `200` — the domain was enriched within the last 24 hours, so you get the data straight away and spend **0 credits**.
- `202` — nothing recent on file, so enrichment is queued. Credits are reserved now; results arrive at your webhook, or you poll `status_endpoint`.
A `409` is normal too: we deduplicate per domain, so a second request while one is in flight is refused rather than charged twice.
**A webhook URL is required** for anything that triggers enrichment — send `X-Webhook-URL` here, or store one once via `POST /account/webhook`.
# Enrichment Tracking
Source: https://docs.bouncewatch.com/api-reference/enrichment-tracking
Monitor enrichment progress and retrieve results
## Overview
When a domain enrichment is triggered, you receive a `batch_id`. Use these endpoints to track progress and retrieve results.
**Recommended:** Configure a [webhook](/webhooks) to receive results automatically. Use these endpoints as a fallback or for status monitoring.
## Enrichment Lifecycle
```mermaid theme={null}
sequenceDiagram
participant You
participant API
participant Jobs
participant Webhook
You->>API: GET /company/stripe.com
API->>You: 202 Accepted + batch_id
API->>Jobs: Queue enrichment jobs
loop Check Status (optional)
You->>API: GET /enrichment/{batch_id}/status
API->>You: status: processing
end
Jobs->>Jobs: Collect fresh data (2-10 min)
Jobs->>Webhook: POST results to your webhook
You->>API: GET /enrichment/{batch_id}/results
API->>You: Complete enriched data
```
## Check Enrichment Status
Poll this endpoint to track enrichment progress:
```
GET /api/v1/enrichment/{batch_id}/status
```
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/status" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const checkStatus = async (batchId) => {
const response = await fetch(
`https://api.bouncewatch.com/api/v1/enrichment/${batchId}/status`,
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
return response.json();
};
// Poll every 30 seconds
const pollStatus = async (batchId) => {
while (true) {
const result = await checkStatus(batchId);
console.log(`Status: ${result.status}`);
if (['completed', 'failed', 'no_data_found'].includes(result.status)) {
return result;
}
await new Promise(r => setTimeout(r, 30000));
}
};
```
```python Python theme={null}
import requests
import time
def check_status(batch_id):
response = requests.get(
f'https://api.bouncewatch.com/api/v1/enrichment/{batch_id}/status',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
return response.json()
# Poll every 30 seconds
def poll_status(batch_id):
while True:
result = check_status(batch_id)
print(f"Status: {result['status']}")
if result['status'] in ['completed', 'failed', 'no_data_found']:
return result
time.sleep(30)
```
```php PHP theme={null}
function checkStatus($batchId) {
$ch = curl_init("https://api.bouncewatch.com/api/v1/enrichment/{$batchId}/status");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
return json_decode($response, true);
}
```
### Status Response
```json theme={null}
{
"success": true,
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "processing",
"requested_modules": ["business", "technology"],
"message": null,
"credits": {
"reserved": 20,
"used": 0,
"refunded": 0
},
"timing": {
"requested_at": "2026-02-10T10:00:00Z",
"started_at": "2026-02-10T10:00:05Z",
"completed_at": null,
"duration_seconds": null
},
"webhook": {
"url": "https://your-server.com/webhook",
"sent_at": null,
"status": null,
"retries": 0
},
"results_endpoint": null
}
```
`results_endpoint` will be `null` until the enrichment completes. Once status is `completed`, it returns the URL to fetch results.
### Status Values
| Status | Description |
| --------------- | ---------------------------------- |
| `queued` | Request accepted, waiting to start |
| `processing` | Jobs are actively running |
| `completed` | All jobs finished successfully |
| `failed` | One or more jobs failed |
| `no_data_found` | Domain has no available data |
## Get Enrichment Results
Once status is `completed`, retrieve the full results:
```
GET /api/v1/enrichment/{batch_id}/results
```
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const getResults = async (batchId) => {
const response = await fetch(
`https://api.bouncewatch.com/api/v1/enrichment/${batchId}/results`,
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
return response.json();
};
```
```python Python theme={null}
def get_results(batch_id):
response = requests.get(
f'https://api.bouncewatch.com/api/v1/enrichment/{batch_id}/results',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
return response.json()
```
```php PHP theme={null}
function getResults($batchId) {
$ch = curl_init("https://api.bouncewatch.com/api/v1/enrichment/{$batchId}/results");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
return json_decode($response, true);
}
```
### Results Response
```json theme={null}
{
"success": true,
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "completed",
"data": {
"company": {
"name": "Stripe",
"domain": "stripe.com",
"description": "Financial infrastructure for the internet",
"founded_year": 2010,
"employee_count": 8000
},
"funding": {
"total_funding": 8700000000,
"funding_stage": "Series I",
"rounds": [ ... ]
},
"team": {
"total_employees": 8000,
"hiring_status": "actively_hiring",
"leadership": [ ... ]
}
},
"credits_used": 26,
"credits_refunded": 0,
"credits_breakdown": {
"base": 10,
"enrichments": { "funding": 10, "team": 6 }
},
"modules_included": ["base", "funding", "team"],
"completed_at": "2026-02-10T10:05:30Z"
}
```
## Credit Handling
Credits are deducted when the batch is queued, and refunded in full if it produces
nothing:
| Batch outcome | What you pay |
| --------------- | ---------------------------- |
| `completed` | The full reserved amount |
| `no_data_found` | **Nothing** — fully refunded |
| `failed` | **Nothing** — fully refunded |
Refunds are all-or-nothing at the batch level. If the batch completes but an individual
module came back thin, that is still a completed batch and the full amount stands —
there is no partial refund per module.
The refund lands on your balance before the webhook fires, so the `credits` block in the
webhook payload already reflects it.
## Webhook Notification
When enrichment completes, we POST to your configured webhook:
```json theme={null}
{
"event": "enrichment.completed",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "completed",
"requested_modules": ["business", "technology"],
"completed_at": "2026-02-10T10:05:30Z",
"duration_seconds": 330,
"credits": {
"reserved": 20,
"used": 20,
"refunded": 0
},
"data_url": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results"
}
```
Set up your webhook endpoint to receive notifications →
## Estimated Processing Times
| Modules Requested | Estimated Time |
| ----------------- | -------------- |
| Base only | 1-2 minutes |
| 1-2 modules | 3-5 minutes |
| 3-4 modules | 5-8 minutes |
| All modules | 8-12 minutes |
Processing times depend on data availability. Companies with more online presence are typically faster to enrich.
# Check enrichment status
Source: https://docs.bouncewatch.com/api-reference/enrichment/check-enrichment-status
/openapi.json get /enrichment/{batch_id}/status
Poll a batch while it runs. Free — polling never costs credits.
Prefer the webhook: it tells you the moment the batch lands. Polling is the fallback when you cannot receive one.
# Get enrichment results
Source: https://docs.bouncewatch.com/api-reference/enrichment/get-enrichment-results
/openapi.json get /enrichment/{batch_id}/results
Fetch the finished data for a batch. Free — the credits were charged when the batch was queued.
Available once status is `completed`. Asking earlier returns `enrichment_processing` rather than a partial payload.
# API Overview
Source: https://docs.bouncewatch.com/api-reference/overview
Base URL, headers, and error handling
## Base URL
All API requests should be made to:
```
https://api.bouncewatch.com/api/v1
```
## Request Headers
Include these headers with every request:
| Header | Required | Description |
| -------------- | -------- | -------------------------------------- |
| `X-API-Key` | Yes | Your API authentication key |
| `Content-Type` | No | `application/json` (for POST requests) |
| `Accept` | No | `application/json` |
```bash theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Accept: application/json"
```
## Response Format
All responses are returned in JSON format with a consistent structure:
### Successful Response
```json theme={null}
{
"success": true,
"credits_used": 10,
"credits_breakdown": {
"base": 10,
"enrichments": {}
},
"modules_included": ["base"],
"data": {
// Response data here
}
}
```
### Error Response
Every error carries `success: false` and a stable `error` code. **Switch on `error`, not
on `message`** — the code is part of the contract, the sentence is not and may be reworded.
```json theme={null}
{
"success": false,
"error": "webhook_required",
"message": "Webhook URL is required for enrichment requests",
"hint": "Configure a webhook URL in your API panel or provide it via the X-Webhook-URL header."
}
```
## HTTP Status Codes
| Code | Status | Description |
| ----- | --------------------- | ------------------------------------------------------------------ |
| `200` | OK | Request successful (data served from 24h cache) |
| `202` | Accepted | Enrichment queued (results via webhook), or results not ready yet |
| `400` | Bad Request | Invalid parameters, or a missing/non-https webhook URL |
| `401` | Unauthorized | Invalid or missing API key |
| `402` | Payment Required | Insufficient credits, or no active subscription |
| `403` | Forbidden | Key suspended, IP not allowed, or batch belongs to another account |
| `404` | Not Found | Unknown batch id |
| `409` | Conflict | Domain already enriching, or inside a 24-hour cooldown |
| `410` | Gone | Retired `mode=cached` parameter |
| `422` | Unprocessable | Webhook URL rejected as a placeholder |
| `429` | Too Many Requests | Rate limit or concurrent-enrichment limit exceeded |
| `500` | Internal Server Error | Server error — please retry |
**`409` is common and is not a failure.** We deduplicate per domain: if the same domain
is already being enriched, or was enriched in the last 24 hours, you get a 409 rather
than a second charge. Treat it as "ask the main endpoint again" — see
`recently_enriched` below.
## Error Codes
| Error Code | HTTP | What to do |
| ----------------------- | ---- | --------------------------------------------------- |
| `missing_api_key` | 401 | Add the `X-API-Key` header |
| `invalid_api_key` | 401 | Check the key; it may have been regenerated |
| `api_key_disabled` | 403 | Account suspended — contact support |
| `ip_not_allowed` | 403 | Calling IP is outside your allow-list |
| `subscription_required` | 402 | Subscription expired — upgrade in the billing panel |
| Error Code | HTTP | What to do |
| ---------------------- | ---- | ------------------------------------------------------------ |
| `missing_parameter` | 400 | A required parameter was absent |
| `invalid_domain` | 400 | Provide a valid domain, e.g. `stripe.com` |
| `invalid_module` | 400 | Unknown `enrich` module — the response lists `valid_modules` |
| `webhook_required` | 400 | Configure an `https://` webhook, or send `X-Webhook-URL` |
| `invalid_webhook_url` | 422 | Placeholder/example host rejected — use a real URL |
| `deprecated_parameter` | 410 | Remove `mode=cached`; the response carries a migration guide |
| Error Code | HTTP | What to do |
| -------------------------- | ---- | --------------------------------------------------------------------------- |
| `enrichment_in_progress` | 409 | This domain is already being enriched — wait for the webhook |
| `recently_enriched` | 409 | Enriched within 24h. Request the main endpoint again to get it free |
| `no_data_cooldown` | 409 | We found nothing for this domain recently; `Retry-After` says when to retry |
| `concurrent_limit_reached` | 429 | Too many enrichments in flight for your plan |
`company_not_found` is not returned for an unknown domain — the API enriches it
instead and answers `202`. You will only see it if a company was deleted between
queuing a batch and fetching its results.
| Error Code | HTTP | What to do |
| ---------------------- | ---- | ----------------------------------------------------------- |
| `insufficient_credits` | 402 | Response carries `credits_required` and `credits_available` |
| `rate_limit_exceeded` | 429 | Response carries `retry_after` and a `Retry-After` header |
| Error Code | HTTP | What to do |
| ----------------------- | ---- | -------------------------------------------------------- |
| `batch_not_found` | 404 | Unknown `batch_id` |
| `batch_forbidden` | 403 | That batch belongs to another account |
| `enrichment_processing` | 202 | Not finished yet — keep polling the status endpoint |
| `no_data_found` | 200 | No data exists for this domain. **Credits are refunded** |
| `enrichment_failed` | 200 | Enrichment failed. **Credits are refunded** |
| `server_error` | 500 | Transient — retry |
## Error Handling Example
**Note:** When a domain requires enrichment, the API returns `202 Accepted` and starts enrichment automatically. Handle this by checking for `status === 202` and the `batch_id` in the response. Results will be delivered to your webhook.
```javascript JavaScript theme={null}
async function getCompanyData(domain) {
try {
const response = await fetch(
`https://api.bouncewatch.com/api/v1/company/${domain}`,
{ headers: {
'X-API-Key': API_KEY,
'X-Webhook-URL': WEBHOOK_URL
}}
);
const data = await response.json();
// 202 — enrichment queued. Results arrive at your webhook.
if (response.status === 202) {
return { status: 'enriching', batch_id: data.batch_id };
}
if (!data.success) {
switch (data.error) {
case 'insufficient_credits':
console.log(`Need ${data.credits_required}, have ${data.credits_available}`);
break;
case 'rate_limit_exceeded':
console.log(`Rate limited. Retry after ${data.retry_after}s`);
break;
case 'webhook_required':
console.log('Configure an https webhook URL first');
break;
case 'recently_enriched':
case 'enrichment_in_progress':
// Not a failure: the data is on its way, or already free to fetch.
console.log(`Deduplicated: ${data.message}`);
break;
default:
console.log(`${data.error}: ${data.message}`);
}
return null;
}
return data.data;
} catch (error) {
console.error('Network error:', error);
return null;
}
}
```
```python Python theme={null}
import requests
def get_company_data(domain):
try:
response = requests.get(
f'https://api.bouncewatch.com/api/v1/company/{domain}',
headers={'X-API-Key': API_KEY, 'X-Webhook-URL': WEBHOOK_URL}
)
data = response.json()
# 202 — enrichment queued. Results arrive at your webhook.
if response.status_code == 202:
return {'status': 'enriching', 'batch_id': data['batch_id']}
if not data['success']:
error = data.get('error')
if error == 'insufficient_credits':
print(f"Need {data['credits_required']}, have {data['credits_available']}")
elif error == 'rate_limit_exceeded':
print(f"Rate limited. Retry after {data['retry_after']}s")
elif error == 'webhook_required':
print('Configure an https webhook URL first')
elif error in ('recently_enriched', 'enrichment_in_progress'):
# Not a failure: deduplicated, nothing was charged.
print(f"Deduplicated: {data['message']}")
else:
print(f"{error}: {data['message']}")
return None
return data['data']
except requests.RequestException as e:
print(f"Network error: {e}")
return None
```
```php PHP theme={null}
function getCompanyData($domain) {
$ch = curl_init("https://api.bouncewatch.com/api/v1/company/{$domain}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . API_KEY,
'X-Webhook-URL: ' . WEBHOOK_URL,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
echo "Network error\n";
return null;
}
$data = json_decode($response, true);
// 202 — enrichment queued. Results arrive at your webhook.
if ($httpCode === 202) {
return ['status' => 'enriching', 'batch_id' => $data['batch_id']];
}
if (!$data['success']) {
switch ($data['error']) {
case 'insufficient_credits':
echo "Need {$data['credits_required']}, have {$data['credits_available']}\n";
break;
case 'rate_limit_exceeded':
echo "Rate limited. Retry after {$data['retry_after']}s\n";
break;
case 'webhook_required':
echo "Configure an https webhook URL first\n";
break;
case 'recently_enriched':
case 'enrichment_in_progress':
// Not a failure: deduplicated, nothing was charged.
echo "Deduplicated: {$data['message']}\n";
break;
default:
echo "{$data['error']}: {$data['message']}\n";
}
return null;
}
return $data['data'];
}
```
# Response Schema
Source: https://docs.bouncewatch.com/api-reference/response-schema
Complete API response structure and field reference
## Overview
This page documents the complete response structure for all BounceWatch API endpoints. Use this as a reference to understand exactly what data you'll receive.
All responses include a `success` boolean and follow a consistent structure. The API uses a **unified smart enrichment model** — there is no separate cached vs realtime mode.
## API Flow
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=business,funding
```
Include your API key via `X-API-Key` header and a webhook URL via `X-Webhook-URL` header (or configure one in your API panel).
New domain or data older than 24 hours: enrichment is queued. You receive a `batch_id` and tracking endpoints immediately.
When enrichment completes (typically 2–10 min), we POST the results to your webhook URL with a `data_url` to fetch the full data.
If you request the same domain again within 24 hours, cached data is returned **instantly at no additional cost** (0 credits).
***
## Response Structures
The API returns two different response structures depending on whether the domain needs fresh enrichment or has recent data available.
Returned when a new enrichment is triggered. Results will be delivered via webhook.
```json theme={null}
{
"success": true,
"status": "processing",
"batch_id": "batch_abc123xyz",
"current_data": {
"company": { ... }
},
"enrichment_status": "processing",
"modules_requested": ["business", "funding"],
"modules_count": 2,
"webhook_notification": "enabled",
"status_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/status",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/results",
"credits_reserved": 26,
"message": "Enrichment jobs queued successfully. Fresh data will be sent to your webhook when ready.",
"hint": "Results will be delivered to your webhook URL. You can also poll the status_endpoint to check progress."
}
```
| Field | Type | Description |
| ---------------------- | ------- | ------------------------------------------------------- |
| `batch_id` | string | Unique identifier for tracking this enrichment |
| `current_data` | object | Any existing (possibly stale) data for the domain |
| `enrichment_status` | string | Always `processing` for new enrichments |
| `webhook_notification` | string | `enabled` — confirms webhook delivery is configured |
| `status_endpoint` | string | Poll this URL to check enrichment progress |
| `results_endpoint` | string | Fetch completed results from this URL |
| `credits_reserved` | integer | Credits reserved upfront (refunded if enrichment fails) |
A domain **new to our index** returns a slightly different 202: the module list is
named `requested_modules` (not `modules_requested`), there is no `current_data`, and
`credits_remaining` is included. `success`, `batch_id`, `credits_reserved`,
`status_endpoint` and `results_endpoint` are present on both — key on those. See
[Company API](/api-reference/company-api#scenario-2-fresh-enrichment-async-response).
Returned when the same domain was enriched within the last 24 hours. **No credits charged.**
```json theme={null}
{
"success": true,
"data": {
"company": { ... },
"business": { ... },
"funding": { ... }
},
"credits_used": 0,
"credits_breakdown": {
"base": 0,
"enrichments": { "business": 0, "funding": 0 },
"note": "No credits charged — 24-hour deduplication. Normal cost would be 26 credits."
},
"modules_included": ["base", "business", "funding"],
"from_recent_enrichment": true,
"credits_remaining": 4974,
"dedup_note": "This domain was enriched within the last 24 hours. Cached data is returned at no charge."
}
```
| Field | Type | Description |
| ------------------------ | ------- | ---------------------------------------------------- |
| `data` | object | Complete enrichment data, nested by module |
| `credits_used` | integer | Always `0` for deduplicated responses |
| `credits_breakdown` | object | Shows all zeros with a `note` explaining normal cost |
| `modules_included` | array | List of modules in the response |
| `from_recent_enrichment` | boolean | `true` if data comes from a recent enrichment batch |
| `credits_remaining` | integer | Your current credit balance |
| `dedup_note` | string | Explains why no credits were charged |
**Deprecation notice:** If you pass `mode=realtime`, the request will still work but the response will include a `deprecation_notice` field. The `mode=cached` parameter returns **410 Gone**. Remove the `mode` parameter entirely — it is no longer needed.
***
## Webhook Payload
When enrichment completes, this payload is POSTed to your webhook URL:
```json theme={null}
{
"event": "enrichment.completed",
"batch_id": "batch_abc123xyz",
"domain": "stripe.com",
"status": "completed",
"requested_modules": ["business", "funding"],
"requested_at": "2025-01-15T10:25:00Z",
"completed_at": "2025-01-15T10:30:00Z",
"duration_seconds": 300,
"credits": {
"reserved": 26,
"used": 26,
"refunded": 0
},
"data_url": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/results",
"_meta": {
"api_version": "2.0",
"webhook_attempt": 1
}
}
```
| Event | Description |
| -------------------------- | ------------------------------------------------------------- |
| `enrichment.completed` | Data is ready — fetch it from `data_url` |
| `enrichment.failed` | Enrichment failed — credits are refunded |
| `enrichment.no_data_found` | Domain exists but no enrichment data found — credits refunded |
Use `data_url` to fetch the full enrichment results with your API key. The webhook payload itself does not contain the enrichment data — only metadata and the URL to retrieve it.
***
## Base Company Data (Always Included)
Base company information, always included with every request (10 credits).
```json theme={null}
{
"data": {
"company": {
// Identity
"name": "Stripe",
"description": "Financial infrastructure platform for the internet",
"founded_year": 2010,
"company_status": "active",
// Contact
"phone": "+1-888-926-2289",
"email": "support@stripe.com",
"domain": "stripe.com",
"linkedin_url": "https://www.linkedin.com/company/stripe",
"facebook_url": "https://www.facebook.com/stripe",
"twitter_url": "https://twitter.com/stripe",
// Location
"headquarter_country": "United States",
"headquarter_city": "San Francisco",
"registered_address": "354 Oyster Point Blvd, South San Francisco, CA 94080",
"office_locations": [
"London, United Kingdom",
"Dublin, Ireland",
"Singapore"
],
// Team Size
"employee_count": 8000,
"employee_range": "5001-10000",
// Legal Information (UK Companies House data if available)
"legal_name": "Stripe Payments UK Ltd",
"company_number": "07920850",
"company_type": "ltd",
"date_of_creation": "2012-01-23",
"jurisdiction": "england-wales",
"sic_codes": ["62012", "64999"]
}
}
}
```
### Base Company Fields
| Field | Type | Description |
| --------------------- | ------- | -------------------------------------------------- |
| `name` | string | Company display name |
| `description` | string | Company description/about |
| `founded_year` | integer | Year the company was founded |
| `company_status` | string | Operational status: `active`, `acquired`, `closed` |
| `phone` | string | Primary phone number |
| `email` | string | Primary contact email |
| `domain` | string | Company website domain |
| `linkedin_url` | string | LinkedIn company page URL |
| `facebook_url` | string | Facebook page URL |
| `twitter_url` | string | Twitter/X profile URL |
| `headquarter_country` | string | HQ country name |
| `headquarter_city` | string | HQ city name |
| `registered_address` | string | Official registered address |
| `office_locations` | array | List of other office locations |
| `employee_count` | integer | Exact employee count (if available) |
| `employee_range` | string | Employee range bucket |
| `legal_name` | string | Official legal entity name |
| `company_number` | string | Company registration number |
| `company_type` | string | Legal entity type |
| `date_of_creation` | string | Company registration date |
| `jurisdiction` | string | Legal jurisdiction |
| `sic_codes` | array | Industry classification codes |
***
## Business Enrichment (+6 credits)
Add `business` to the enrich parameter: `?enrich=business`
```json theme={null}
{
"data": {
"business": {
// Industry Classification
"industries_count": 3,
"industries": ["Financial Services", "FinTech", "Payments"],
// Business Model
"business_model": ["B2B", "SaaS", "Platform"],
"revenue_model": ["Transaction Fees", "Subscription"],
"product_features": ["Hosted checkout", "Fraud detection", "Reporting API"],
"target_audience": "Developers and businesses of all sizes",
// Taxonomy
"taxonomy_count": 5,
"taxonomy": ["Payment Processing", "API Platform", "Developer Tools", "E-commerce", "SaaS"],
// Services
"services_count": 4,
"services": [
{
"name": "Payment Processing",
"description": "Accept payments online and in-person"
},
{
"name": "Billing",
"description": "Subscription and invoicing platform"
},
{
"name": "Connect",
"description": "Payments for platforms and marketplaces"
},
{
"name": "Atlas",
"description": "Startup incorporation service"
}
],
// Certifications
"certificates_count": 2,
"certificates_and_compliance": [
{
"name": "PCI DSS Level 1",
"description": "Payment Card Industry Data Security Standard"
},
{
"name": "SOC 2 Type II",
"description": "Service Organization Control certification"
}
],
// Market Positioning
"market_problems_count": 2,
"market_problems": [
{
"title": "Complex Payment Integration",
"description": "Businesses struggle with fragmented payment systems"
},
{
"title": "Global Expansion",
"description": "Difficulty accepting payments across borders"
}
],
// Geographic Focus
"target_countries_count": 3,
"target_countries": [
{
"country": "United States",
"country_code": "US",
"ai_reasoning": "Primary market with strong developer ecosystem",
"confidence": "high"
},
{
"country": "United Kingdom",
"country_code": "GB",
"ai_reasoning": "Major European financial hub",
"confidence": "high"
},
{
"country": "Germany",
"country_code": "DE",
"ai_reasoning": "Largest European economy",
"confidence": "medium"
}
],
"potential_target_countries_count": 2,
"potential_target_countries": [
{
"country": "Brazil",
"country_code": "BR",
"ai_reasoning": "Growing e-commerce market",
"confidence": "medium"
},
{
"country": "India",
"country_code": "IN",
"ai_reasoning": "Large developer community and growing digital payments",
"confidence": "medium"
}
]
}
}
}
```
### Business Fields
| Field | Type | Description |
| ---------------------------------- | ------- | --------------------------------------------------------- |
| `industries_count` | integer | Number of industries |
| `industries` | array | Industry classifications |
| `business_model` | array | Business model types: B2B, B2C, SaaS, Platform, etc. |
| `revenue_model` | array | Revenue model types: Subscription, Transaction Fees, etc. |
| `target_audience` | string | Primary customer description |
| `taxonomy_count` | integer | Number of taxonomy tags |
| `taxonomy` | array | Business category tags |
| `services_count` | integer | Number of services |
| `services` | array | Products and services offered |
| `services[].name` | string | Service name |
| `services[].description` | string | Service description |
| `certificates_count` | integer | Number of certifications |
| `certificates_and_compliance` | array | Certifications and compliance |
| `market_problems_count` | integer | Number of market problems |
| `market_problems` | array | Problems the company solves |
| `target_countries_count` | integer | Number of target countries |
| `target_countries` | array | Current focus countries |
| `potential_target_countries_count` | integer | Number of potential countries |
| `potential_target_countries` | array | Future expansion targets |
***
## Technology Enrichment (+4 credits)
Add `technology` to the enrich parameter: `?enrich=technology`
```json theme={null}
{
"data": {
"technology": {
"total_technologies": 47,
"categories_count": 12,
// Flat list of all technology names
"technologies": [
"React", "Node.js", "Ruby", "PostgreSQL", "Redis",
"AWS", "Kubernetes", "Docker", "Terraform", "Datadog"
],
// Grouped by category
"technology_stack": {
"JavaScript Frameworks": [
{
"name": "React",
"description": "A JavaScript library for building user interfaces"
},
{
"name": "Next.js",
"description": "React framework for production"
}
],
"Backend": [
{
"name": "Node.js",
"description": "JavaScript runtime environment"
},
{
"name": "Ruby on Rails",
"description": "Server-side web application framework"
}
],
"Database": [
{
"name": "PostgreSQL",
"description": "Open source relational database"
},
{
"name": "Redis",
"description": "In-memory data structure store"
}
],
"Cloud Infrastructure": [
{
"name": "Amazon Web Services",
"description": "Cloud computing platform"
},
{
"name": "Google Cloud Platform",
"description": "Cloud computing services"
}
],
"DevOps": [
{
"name": "Kubernetes",
"description": "Container orchestration platform"
},
{
"name": "Docker",
"description": "Container platform"
}
],
"Analytics": [
{
"name": "Segment",
"description": "Customer data platform"
},
{
"name": "Amplitude",
"description": "Product analytics platform"
}
]
},
"last_updated": "2025-12-15T10:30:00Z"
}
}
}
```
### Technology Fields
| Field | Type | Description |
| ------------------------------------------ | ------- | ------------------------------------- |
| `total_technologies` | integer | Total number of technologies detected |
| `categories_count` | integer | Number of technology categories |
| `technologies` | array | Flat list of all technology names |
| `technology_stack` | object | Technologies grouped by category |
| `technology_stack[category]` | array | List of technologies in category |
| `technology_stack[category][].name` | string | Technology name |
| `technology_stack[category][].description` | string | Technology description |
| `last_updated` | string | When technology data was last updated |
***
## Funding Enrichment (+10 credits)
Add `funding` to the enrich parameter: `?enrich=funding`
```json theme={null}
{
"data": {
"funding": {
// Summary
"total_funding": 8700000000,
"funding_stage": "Series I",
"last_funding_date": "2023-03-15",
"funding_round_count": 12,
"last_valuation": 50000000000,
// Investment History (newest first)
"investment_history": [
{
"date": "2023-03-15",
"stage": "Series I",
"amount": 6500000000,
"amount_usd": 6500000000,
"currency": "USD",
"valuation": 50000000000,
"valuation_usd": 50000000000,
"investors": ["Andreessen Horowitz", "Sequoia Capital", "GIC"]
},
{
"date": "2021-03-14",
"stage": "Series H",
"amount": 600000000,
"amount_usd": 600000000,
"currency": "USD",
"valuation": 95000000000,
"valuation_usd": 95000000000,
"investors": ["Sequoia Capital", "Allianz X", "Fidelity"]
}
],
// All Investors
"investors_count": 42,
"investors": [
"Sequoia Capital",
"Andreessen Horowitz",
"General Catalyst",
"Founders Fund",
"Tiger Global",
"GIC",
"Fidelity Investments"
],
// Notable Investors (with details)
"notable_investors_count": 5,
"notable_investors": [
{
"name": "Sequoia Capital",
"logo": "https://bouncewatch.com/storage/investors/sequoia.png",
"domain": "sequoiacap.com"
},
{
"name": "Andreessen Horowitz",
"logo": "https://bouncewatch.com/storage/investors/a16z.png",
"domain": "a16z.com"
}
],
"lead_investors": [],
"acquisitions": []
}
}
}
```
### Funding Fields
| Field | Type | Description |
| ------------------------------------ | ------- | ------------------------------------------ |
| `total_funding` | integer | Total funding amount in USD |
| `funding_stage` | string | Current/last funding stage |
| `last_funding_date` | string | Date of most recent funding (Y-m-d format) |
| `funding_round_count` | integer | Total number of funding rounds |
| `last_valuation` | number | Most recent valuation in USD (nullable) |
| `investment_history` | array | All funding rounds (newest first) |
| `investment_history[].date` | string | Round date (Y-m-d format) |
| `investment_history[].stage` | string | Round type (Seed, Series A, etc.) |
| `investment_history[].amount` | number | Amount raised (original currency) |
| `investment_history[].amount_usd` | number | Amount in USD |
| `investment_history[].currency` | string | Original currency code |
| `investment_history[].valuation` | number | Valuation at this round (nullable) |
| `investment_history[].valuation_usd` | number | Valuation in USD (nullable) |
| `investment_history[].investors` | array | Investors in this round |
| `investors_count` | integer | Total unique investors |
| `investors` | array | List of all investor names |
| `notable_investors_count` | integer | Number of notable investors |
| `notable_investors` | array | Notable investors with details |
| `notable_investors[].name` | string | Investor name |
| `notable_investors[].logo` | string | Investor logo URL |
| `notable_investors[].domain` | string | Investor website domain |
***
## Team Enrichment (+6 credits)
Add `team` to the enrich parameter: `?enrich=team`
```json theme={null}
{
"data": {
"team": {
// Team Members
"team_count": 8,
"team_members": [
{
"name": "Patrick Collison",
"position": "CEO & Co-founder",
"linkedin_url": "https://www.linkedin.com/in/patrickcollison",
"country": "United States",
"city": "San Francisco",
"nationality": "Irish",
"biography": "Patrick Collison is the co-founder and CEO of Stripe...",
"experience": [
{
"company": "Stripe",
"position": "CEO & Co-founder",
"description": "Leading the company's vision and strategy",
"start_date": "2010-01-01",
"end_date": null,
"is_current": true,
"location": "United States"
},
{
"company": "Auctomatic",
"position": "Co-founder",
"description": "Online auction management",
"start_date": "2007-01-01",
"end_date": "2008-01-01",
"is_current": false,
"location": "Canada"
}
],
"education": [
{
"school": "MIT",
"degree": "Computer Science (did not complete)",
"description": null,
"start_date": "2009-01-01",
"end_date": "2010-01-01",
"location": "United States"
}
]
}
],
// Employee Growth Metrics
"monthly_growth": {
"change": 120,
"percentage": 1.5,
"from": 7880,
"to": 8000,
"period_start": "November 2025",
"period_end": "December 2025"
},
"three_month_growth": {
"change": 400,
"percentage": 5.3,
"from": 7600,
"to": 8000,
"period_start": "September 2025",
"period_end": "December 2025"
},
"six_month_growth": {
"change": 800,
"percentage": 11.1,
"from": 7200,
"to": 8000,
"period_start": "June 2025",
"period_end": "December 2025"
},
// Historical Data
"historical_employee_data": [
{"month_name": "December 2025", "employee_count": 8000},
{"month_name": "November 2025", "employee_count": 7880},
{"month_name": "October 2025", "employee_count": 7760}
],
// Hiring Activity
"job_listings_count": 145,
"job_titles_count": 89,
"job_titles": [
"Software Engineer",
"Product Manager",
"Data Scientist",
"Solutions Architect"
],
"job_locations_count": 12,
"job_locations": [
"San Francisco",
"New York",
"London",
"Dublin",
"Singapore"
],
"job_countries_count": 8,
"job_countries": [
"United States",
"United Kingdom",
"Ireland",
"Singapore",
"Japan"
],
"hiring_status": "actively_hiring",
// Key Role Openings
"key_role_openings_count": 5,
"key_role_openings": [
{
"title": "Head of Engineering, Payments",
"location": "San Francisco",
"country": "United States",
"posted_date": "2025-12-10"
},
{
"title": "VP of Product",
"location": "Remote",
"country": "United States",
"posted_date": "2025-12-05"
}
]
}
}
}
```
### Team Fields
| Field | Type | Description |
| ----------------------------- | ------- | -------------------------------------- |
| `team_count` | integer | Number of team members in database |
| `team_members` | array | Team member profiles |
| `team_members[].name` | string | Full name |
| `team_members[].position` | string | Current job title |
| `team_members[].linkedin_url` | string | LinkedIn profile URL |
| `team_members[].country` | string | Current country |
| `team_members[].city` | string | Current city |
| `team_members[].nationality` | string | Nationality |
| `team_members[].biography` | string | Bio/about text |
| `team_members[].experience` | array | Work experience history |
| `team_members[].education` | array | Education history |
| `monthly_growth` | object | Month-over-month employee change |
| `three_month_growth` | object | 3-month employee change |
| `six_month_growth` | object | 6-month employee change |
| `historical_employee_data` | array | Monthly employee counts |
| `job_listings_count` | integer | Total open positions |
| `job_titles_count` | integer | Unique job titles |
| `job_titles` | array | List of job titles |
| `job_locations_count` | integer | Unique hiring locations |
| `job_locations` | array | Cities with open positions |
| `job_countries_count` | integer | Countries with open positions |
| `job_countries` | array | Countries hiring in |
| `hiring_status` | string | `actively_hiring`, `hiring`, `passive` |
| `key_role_openings_count` | integer | Leadership positions open |
| `key_role_openings` | array | Leadership/senior positions |
***
## Signals Enrichment (+16 credits)
Add `signals` to the enrich parameter: `?enrich=signals`
**Highlights** are AI-detected signals from LinkedIn posts and news articles. Each highlight includes structured entities (partner names, product names, cities, etc.) extracted from the content.
```json theme={null}
{
"data": {
"signals": {
// Highlights (Recent Activity/News)
"highlights_count": 12,
"highlights": [
{
"type": "funding",
"title": "Raised $6.5B Series I",
"date": "March 15, 2023",
"description": "Secured Series I funding of $6.5B"
},
{
"type": "business",
"title": "Partnership Announced",
"date": "February 2023",
"description": "Stripe partners with Amazon for payment processing",
"entities": {
"partner_name": "Amazon",
"partnership_type": "strategic collaboration"
}
},
{
"type": "product",
"title": "New Product Launched",
"date": "January 2023",
"description": "Stripe launches Stripe Tax for automated tax calculation",
"entities": {
"product_name": "Stripe Tax",
"category": "Tax Automation"
}
},
{
"type": "expansion",
"title": "Expansion Announced",
"date": "January 2023",
"description": "Opening new offices in Toronto and Singapore",
"entities": {
"new_city": "Toronto, Singapore",
"expansion_type": "office opening"
}
},
{
"type": "traction",
"title": "Website traffic increased 15%",
"date": "December 2024",
"description": "Monthly website visitors increased by 15%"
},
{
"type": "hiring",
"title": "Team size increased 8%",
"date": "December 2024",
"description": "Employee count increased by 8%"
}
],
// LinkedIn Metrics
"linkedin_metrics": {
"followers": 1250000,
"change_percentage": 3.2,
"last_post_date": "2025-12-20",
"period_start": "2025-11-01",
"period_end": "2025-12-01"
},
// AI-Powered Insights
"ai_insights": {
"insight": "Stripe continues to demonstrate strong market positioning with consistent product launches and strategic partnerships. The recent Series I funding at $50B valuation, though lower than previous peaks, indicates investor confidence in long-term growth.",
"growth_momentum": "high",
"trajectory": "accelerating",
"key_indicators": [
"New Funding Received",
"Partnership Announced",
"Expansion Announced",
"Hiring For Key Role"
],
"next_likely_move": "Based on recent hiring patterns and expansion announcements, Stripe is likely preparing for international expansion, particularly in emerging markets."
}
}
}
}
```
### Signals Fields
| Field | Type | Description |
| ------------------------------------ | ------- | ------------------------------------------------------------ |
| `highlights_count` | integer | Number of highlights |
| `highlights` | array | Recent company activity/signals |
| `highlights[].type` | string | Signal type (see Signal Types below) |
| `highlights[].title` | string | Signal title |
| `highlights[].description` | string | Detailed description |
| `highlights[].date` | string | When it occurred (e.g., "March 15, 2023" or "December 2024") |
| `highlights[].source` | string | Data source: `linkedin_post` or `news_article` (optional) |
| `highlights[].entities` | object | Extracted entities from the signal (optional) |
| `linkedin_metrics` | object | LinkedIn follower metrics |
| `linkedin_metrics.followers` | integer | Current follower count |
| `linkedin_metrics.change_percentage` | number | Change percentage over period |
| `linkedin_metrics.last_post_date` | string | Date of last LinkedIn post |
| `ai_insights.insight` | string | AI-generated summary |
| `ai_insights.growth_momentum` | string | `high`, `medium`, `low` |
| `ai_insights.trajectory` | string | `accelerating`, `steady`, `declining` |
| `ai_insights.key_indicators` | array | Key signals driving assessment |
| `ai_insights.next_likely_move` | string | AI prediction |
### Signal Types
BounceWatch tracks various signal types across multiple categories. The `type` field in highlights indicates the signal category:
* `funding` - New Funding Received, Grant Received, IPO Announced
* `business` - Partnership Announced, Major Customer Win
* `recognition` - Accepted to Program, Award Received, Certification Achieved
* `product` - New Product Launched, New Feature Launched, Mobile App Launched
* `traction` - Growth Metrics Shared, Revenue Metrics Shared, Website/LinkedIn Traffic Changes
* `expansion` - Expansion Announced, New Office Opened
* `hiring` - Key Hire Announced, Team Size Increased, Open Positions
* `team_change` - Key Exit Announced
* `negative` - Layoffs Announced, Shutdown Announced
* `milestone` - Regulatory Approval
* `strategy` - Pivot Announced
* `acquisition` - Acquisition Announced, Acquisition Made
* `media` - Featured on News
* `rumor` - Acquisition Rumor, IPO Rumor, Partnership Talks, Expansion Plan, Layoff Rumor, Leadership Change Rumor
* `risk` - Shutdown Risk
* `event` - Event Participation
***
## Competitors Enrichment (+8 credits)
Add `competitors` to the enrich parameter: `?enrich=competitors`
```json theme={null}
{
"data": {
"competitors": {
"competitors_count": 12,
"similar_companies": [
{
"name": "Adyen",
"domain": "adyen.com",
"slogan": "The payments platform built for growth",
"industries": ["FinTech", "Payments"],
"country": "Netherlands",
"funding_stage": "Public",
"total_funding": 266000000,
"employee_count": 3500,
"employee_range": "1001-5000"
},
{
"name": "Square",
"domain": "squareup.com",
"slogan": "Start selling today",
"industries": ["FinTech", "Payments", "Point of Sale"],
"country": "United States",
"funding_stage": "Public",
"total_funding": 590000000,
"employee_count": 8000,
"employee_range": "5001-10000"
},
{
"name": "Braintree",
"domain": "braintreepayments.com",
"slogan": "A PayPal service",
"industries": ["FinTech", "Payments"],
"country": "United States",
"funding_stage": "Acquired",
"total_funding": 69000000,
"employee_count": null,
"employee_range": "201-500"
}
]
}
}
}
```
### Competitors Fields
| Field | Type | Description |
| ------------------------------------ | ------- | --------------------------- |
| `competitors_count` | integer | Number of similar companies |
| `similar_companies` | array | List of similar companies |
| `similar_companies[].name` | string | Company name |
| `similar_companies[].domain` | string | Company domain |
| `similar_companies[].slogan` | string | Company tagline |
| `similar_companies[].industries` | array | Industry classifications |
| `similar_companies[].country` | string | HQ country |
| `similar_companies[].funding_stage` | string | Current funding stage |
| `similar_companies[].total_funding` | integer | Total funding in USD |
| `similar_companies[].employee_count` | integer | Exact employee count |
| `similar_companies[].employee_range` | string | Employee range bucket |
***
## Full Response Example
Here's a complete example with all modules enabled:
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=business,technology,funding,team,signals,competitors
```
```json theme={null}
{
"success": true,
"data": {
"company": {
"name": "Stripe",
"description": "Financial infrastructure platform for the internet",
"founded_year": 2010,
"company_status": "active",
"phone": "+1-888-926-2289",
"email": "support@stripe.com",
"domain": "stripe.com",
"linkedin_url": "https://www.linkedin.com/company/stripe",
"facebook_url": "https://www.facebook.com/stripe",
"twitter_url": "https://twitter.com/stripe",
"headquarter_country": "United States",
"headquarter_city": "San Francisco",
"registered_address": null,
"office_locations": ["London", "Dublin", "Singapore", "Tokyo"],
"employee_count": 8000,
"employee_range": "5001-10000",
"legal_name": null,
"company_number": null,
"company_type": null,
"date_of_creation": null,
"jurisdiction": null,
"sic_codes": []
},
"business": {
"industries_count": 3,
"industries": ["Financial Services", "FinTech", "Payments"],
"business_model": ["B2B", "SaaS", "Platform"],
"revenue_model": ["Transaction Fees", "Subscription"],
"target_audience": "Developers and businesses",
"services_count": 4,
"services": [...],
"certificates_count": 2,
"certificates_and_compliance": [...],
"taxonomy_count": 5,
"taxonomy": [...],
"market_problems_count": 2,
"market_problems": [...],
"target_countries_count": 3,
"target_countries": [...],
"potential_target_countries_count": 2,
"potential_target_countries": [...]
},
"technology": {
"total_technologies": 47,
"categories_count": 12,
"technologies": [...],
"technology_stack": {...},
"last_updated": "2025-12-15T10:30:00Z"
},
"funding": {
"total_funding": 8700000000,
"funding_stage": "Series I",
"last_funding_date": "2023-03-15",
"funding_round_count": 12,
"last_valuation": 50000000000,
"investment_history": [...],
"investors_count": 42,
"investors": [...],
"notable_investors_count": 5,
"notable_investors": [...],
"lead_investors": [],
"acquisitions": []
},
"team": {
"team_count": 8,
"team_members": [...],
"monthly_growth": {...},
"three_month_growth": {...},
"six_month_growth": {...},
"historical_employee_data": [...],
"job_listings_count": 145,
"job_titles_count": 89,
"job_titles": [...],
"job_locations_count": 12,
"job_locations": [...],
"job_countries_count": 8,
"job_countries": [...],
"hiring_status": "actively_hiring",
"key_role_openings_count": 5,
"key_role_openings": [...]
},
"signals": {
"highlights_count": 12,
"highlights": [...],
"linkedin_metrics": {...},
"ai_insights": {...}
},
"competitors": {
"competitors_count": 12,
"similar_companies": [...]
}
},
"credits_used": 60,
"credits_breakdown": {
"base": 10,
"enrichments": {
"business": 6,
"technology": 4,
"funding": 10,
"team": 6,
"signals": 16,
"competitors": 8
}
},
"modules_included": ["base", "business", "technology", "funding", "team", "signals", "competitors"],
"from_recent_enrichment": false,
"credits_remaining": 4940
}
```
The example above shows a **first-time enrichment result** fetched from the `results_endpoint` after webhook delivery. For **deduplicated responses** (same domain within 24h), `credits_used` will be `0` and the `credits_breakdown` will show all zeros with a `note` field.
***
## Error Responses
All error responses include `"success": false` and a human-readable `message`. Most also include `hint` or `suggestions` to help you resolve the issue.
```json theme={null}
{
"success": false,
"message": "Webhook URL is required for enrichment requests",
"hint": "Configure a webhook URL in your API panel or provide it via X-Webhook-URL header.",
"quick_test": "For quick testing, get a free webhook URL from https://webhook.site",
"documentation": "https://docs.bouncewatch.com/webhooks"
}
```
```json theme={null}
{
"success": false,
"message": "Insufficient credits",
"credits_needed": 26,
"credits_available": 5
}
```
There are three 409 scenarios:
**1. Domain in 24h cooldown** — domain was recently enriched:
```json theme={null}
{
"success": false,
"message": "This domain was recently enriched. Cached data is available at no charge via the main endpoint.",
"hint": "Request GET /api/v1/company/stripe.com again to receive cached data instantly for free.",
"cooldown_expires_at": "2025-01-16T10:30:00Z",
"hours_remaining": 18.5,
"previous_batch_id": "batch_abc123xyz",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/results"
}
```
**2. Enrichment already in progress** — your batch is running:
```json theme={null}
{
"success": false,
"message": "An enrichment is already in progress for this domain",
"batch_id": "batch_abc123xyz",
"status_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/status",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/results",
"hint": "Please wait for the current enrichment to complete. Results will be delivered via webhook."
}
```
**3. Enrichment already in progress** — another user's batch is running:
```json theme={null}
{
"success": false,
"message": "An enrichment is already in progress for this domain",
"hint": "Please wait for the current enrichment to complete and try again shortly."
}
```
Scenario 1 includes a `Retry-After` header with seconds until the cooldown expires. Scenarios 2-3 indicate a running enrichment — wait for it to finish or poll the status endpoint (if provided).
```json theme={null}
{
"success": false,
"message": "The cached mode has been deprecated and removed.",
"migration_guide": {
"change": "Remove the mode=cached parameter from your requests.",
"how_it_works": "All requests now trigger realtime enrichment automatically. If the same domain was enriched within the last 24 hours, you will receive cached data instantly at no additional cost.",
"webhook_required": "A webhook URL is required for new enrichments.",
"example_request": "GET /api/v1/company/stripe.com?enrich=funding,team"
}
}
```
```json theme={null}
{
"success": false,
"message": "Rate limit exceeded",
"retry_after": 60
}
```
This is for actual rate limiting only — domain cooldowns use 409.
### Error Code Reference
| HTTP Code | When | Description |
| --------- | --------------- | --------------------------------------------------------------------------- |
| **400** | Invalid request | Missing webhook URL, invalid domain, invalid modules |
| **401** | Auth failure | API key is invalid, missing, or disabled |
| **402** | No credits | Insufficient credits for the requested modules |
| **409** | Conflict | Enrichment already in progress, domain in 24h cooldown, or no-data cooldown |
| **410** | Deprecated | `mode=cached` parameter used — removed from API |
| **429** | Rate limited | Too many requests per minute (check your plan limits) |
Learn about authentication, rate limits, and API key management →
# Authentication
Source: https://docs.bouncewatch.com/authentication
Secure your API requests with API keys
## API Key Authentication
All API requests must include your API key in the request header.
```bash theme={null}
X-API-Key: your_api_key_here
```
Keys are prefixed `bw_` followed by 32 random characters. There is no separate
test/live key — every key is live and bills against your credit balance.
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com" \
-H "X-API-Key: bw_9f3c2a71d84e05b6c1f7a92d3e480b5c" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/company/stripe.com', {
headers: {
'X-API-Key': 'bw_9f3c2a71d84e05b6c1f7a92d3e480b5c',
'Content-Type': 'application/json'
}
});
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'bw_9f3c2a71d84e05b6c1f7a92d3e480b5c',
'Content-Type': 'application/json'
}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/company/stripe.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: bw_9f3c2a71d84e05b6c1f7a92d3e480b5c',
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
## Getting Your API Key
Go to [BounceWatch Dashboard](https://bouncewatch.com/api-panel/api-keys)
Your API key is displayed on the dashboard. Copy it to use in your requests.
## Security Best Practices
* Store API keys in environment variables
* Use HTTPS for all requests
* Rotate keys periodically
* Use different keys for different environments
* Monitor usage for anomalies
* Commit API keys to version control
* Share keys publicly or in logs
* Use keys in client-side JavaScript
* Reuse keys across environments
* Share keys via email or chat
## Environment Variables
Store your API key securely using environment variables:
```bash .env theme={null}
BOUNCEWATCH_API_KEY=bw_9f3c2a71d84e05b6c1f7a92d3e480b5c
```
```javascript app.js theme={null}
require('dotenv').config();
const API_KEY = process.env.BOUNCEWATCH_API_KEY;
```
```bash .env theme={null}
BOUNCEWATCH_API_KEY=bw_9f3c2a71d84e05b6c1f7a92d3e480b5c
```
```python app.py theme={null}
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('BOUNCEWATCH_API_KEY')
```
```bash .env theme={null}
BOUNCEWATCH_API_KEY=bw_9f3c2a71d84e05b6c1f7a92d3e480b5c
```
```php config.php theme={null}
load();
$apiKey = $_ENV['BOUNCEWATCH_API_KEY'];
```
```bash theme={null}
export BOUNCEWATCH_API_KEY=bw_9f3c2a71d84e05b6c1f7a92d3e480b5c
```
```bash theme={null}
curl -H "X-API-Key: $BOUNCEWATCH_API_KEY" \
https://api.bouncewatch.com/api/v1/company/stripe.com
```
## Authentication Errors
| HTTP | `error` | Solution |
| ----- | ----------------------- | --------------------------------------------------- |
| `401` | `missing_api_key` | Add the `X-API-Key` header |
| `401` | `invalid_api_key` | Check the key — it may have been regenerated |
| `403` | `api_key_disabled` | Account suspended; contact support |
| `403` | `ip_not_allowed` | Your calling IP is outside the key's allow-list |
| `402` | `subscription_required` | Subscription expired — upgrade in the billing panel |
```json Error Response theme={null}
{
"success": false,
"error": "invalid_api_key",
"message": "The provided API key is not valid"
}
```
Keys do not expire on their own. A key stops working only if it is regenerated, the
account is suspended, or the subscription lapses.
## Key Rotation
For enhanced security, you can regenerate your API key periodically:
Click "Regenerate" in the dashboard to create a new API key. This will immediately invalidate your current key.
Update your environment variables with the new key.
Verify your application works with the new key.
Regenerating your API key will immediately invalidate the old one. Make sure to update all your applications before regenerating.
# Credits & Modules
Source: https://docs.bouncewatch.com/credits-and-modules
Understanding the credit system and enrichment modules
## How Credits Work
BounceWatch uses a credit-based pricing system. Each API request consumes credits based on the data modules you request.
**Base data is always included** and costs 10 credits. Additional enrichment modules add to this base cost.
## Credit Pricing
| Module | Credits | Description |
| --------------- | ------- | ------------------------------------------------------ |
| **Base** | 10 | Always included - company identity, contact, location |
| **Business** | +6 | Industry, business model, services, market positioning |
| **Technology** | +4 | Tech stack, frameworks, tools (100+ technologies) |
| **Funding** | +10 | Investment history, investors, valuation |
| **Team** | +6 | Leadership, employees, hiring activity |
| **Signals** | +16 | 40+ signal types across 8 categories |
| **Competitors** | +8 | Similar companies, competitive landscape |
**Maximum cost per request:** 60 credits (Base + all enrichment modules)
## Enrichment Modules
Always included with every request.
**Data Points:**
* Company name and domain
* Description and tagline
* Founded year
* Employee count
* Headquarters location
* Social media links (LinkedIn, Twitter, etc.)
* Contact information
* Legal entity details
```bash theme={null}
GET /api/v1/company/stripe.com
# Returns base data only - 10 credits
```
Company's business context and market positioning.
**Data Points:**
* Industry classification
* Business model
* Target market
* Services offered
* Value proposition
* Market positioning
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=business
# 16 credits (10 base + 6 business)
```
Complete technology stack analysis.
**Data Points:**
* 100+ technology categories
* Programming languages
* Frameworks and libraries
* Cloud infrastructure
* Analytics tools
* Marketing tools
* Security solutions
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=technology
# 14 credits (10 base + 4 technology)
```
Investment history and financial data.
**Data Points:**
* Funding rounds (Seed, Series A, B, C, etc.)
* Total funding amount
* Latest valuation
* Investors list
* Investment dates
* Lead investors
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=funding
# 20 credits (10 base + 10 funding)
```
Team composition and hiring activity.
**Data Points:**
* Key team members
* Leadership profiles
* Employee growth trends
* Hiring status
* Open positions
* Department breakdown
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=team
# 16 credits (10 base + 6 team)
```
40+ signal types across 8 categories.
| Category | Types | Examples |
| ----------- | ----: | ----------------------------------------------------------------------------------------------------------------------- |
| `business` | 8 | Partnership announced, major customer win, expansion announced, new office, regulatory approval, certification achieved |
| `risk` | 7 | Layoffs, shutdown announced, shutdown risk, senior departure, pivot |
| `milestone` | 5 | IPO announced, acquired, acquired a company, acquisition rumour |
| `product` | 4 | Product launched, feature shipped, mobile app released, homepage repositioned |
| `growth` | 4 | Growth metrics, revenue metrics, web traffic moved, LinkedIn following moved |
| `hiring` | 4 | Senior hire, open roles, open role in a key function, headcount moved |
| `event` | 4 | Event attendance, award received, programme acceptance, news mention |
| `funding` | 3 | Raised a round, signalled intent to raise, grant received |
Some types are **unconfirmed** — rumours or inferences rather than announcements
(`fundraising_intent`, `acquisition_rumor`, `shutdown_risk`, and others). They are
flagged as such in the response; report them with that qualification.
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=signals
# 26 credits (10 base + 16 signals)
```
Competitive landscape analysis.
**Data Points:**
* Similar companies
* Direct competitors
* Market alternatives
* Competitive positioning
```bash theme={null}
GET /api/v1/company/stripe.com?enrich=competitors
# 18 credits (10 base + 8 competitors)
```
## Use Case Combinations
**26 credits**
```bash theme={null}
GET /api/v1/company/startup.com?enrich=funding,team
```
Perfect for:
* Funding history analysis
* Team composition review
* Investment decision support
**36 credits**
```bash theme={null}
GET /api/v1/company/prospect.com?enrich=business,technology,signals
```
Perfect for:
* Lead qualification
* Tech stack identification
* Growth potential assessment
**28 credits**
```bash theme={null}
GET /api/v1/company/competitor.com?enrich=business,technology,competitors
```
Perfect for:
* Market research
* Competitive positioning
* Technology benchmarking
**60 credits**
```bash theme={null}
GET /api/v1/company/target.com?enrich=business,technology,funding,team,signals,competitors
```
Complete company intelligence with all available data.
## Checking Your Credits
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/credits" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/credits', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
const data = await response.json();
console.log(`Credits remaining: ${data.data.credits.balance}`);
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/credits',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
print(f"Credits remaining: {response.json()['data']['credits']['balance']}")
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/credits');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
echo "Credits remaining: " . $data['data']['credits']['balance'];
```
```json Response theme={null}
{
"success": true,
"data": {
"credits": {
"balance": 21000,
"used": 4000,
"limit": 25000,
"total_purchased": 0,
"percentage_used": 16,
"resets_at": "2026-02-01T09:14:22.000000Z"
}
}
}
```
**Credits do not roll over.** Each billing period starts at your plan's `limit`,
whatever was left of the previous one. Plan for the allowance, not for a balance that
accumulates.
## Credit Best Practices
Start with base data and add enrichments as needed. Don't request all modules if you only need funding data.
Company data doesn't change frequently. Cache responses for 24 hours to save credits.
Poll [`GET /account/credits`](/api-reference/account#get-credit-balance) — it returns
`balance`, `used`, `limit`, `percentage_used` and `resets_at`. Webhooks fire on
enrichment events only; there is no credit-threshold event to subscribe to.
Group similar enrichment requests to optimize credit usage.
## Insufficient Credits
If you don't have enough credits for a request, you get `402 Payment Required`:
```json theme={null}
{
"success": false,
"error": "insufficient_credits",
"message": "Insufficient credits for enrichment",
"credits_required": 26,
"credits_available": 15
}
```
Some responses also carry the older `required_credits` / `available_credits` /
`credits_needed` aliases. New integrations should read `credits_required` and
`credits_available`, which are present everywhere.
Upgrade your plan → the new allowance is added on top of what is left of the current one
See exactly what data each module returns →
# Introduction
Source: https://docs.bouncewatch.com/introduction
Access company data instantly with BounceWatch API
## What is BounceWatch API?
BounceWatch API is a comprehensive intelligence platform for **startup and company data**. Get detailed information about companies with a single API call.
Only fetch the data you need — base data plus any of 6 enrichment modules.
Fresh data collected on-demand. Cached instantly for 24 hours at no extra cost.
Pay for what you use. Transparent credit system.
Get notified instantly when enrichment completes. Results sent directly to your endpoint.
## What Data Can You Access?
Always included with every request.
* Company name and domain
* Description and tagline
* Founded year
* Employee count
* Headquarters location
* Social media links (LinkedIn, Twitter, etc.)
* Contact information
* Legal entity details
Company's business context and market positioning.
* Industry classification
* Business model
* Target market
* Services offered
* Value proposition
* Market positioning
Complete technology stack analysis.
* 100+ technology categories
* Programming languages
* Frameworks and libraries
* Cloud infrastructure
* Analytics tools
* Marketing tools
* Security solutions
Investment history and financial data.
* Funding rounds (Seed, Series A, B, C, etc.)
* Total funding amount
* Latest valuation
* Investors list
* Investment dates
* Lead investors
Team composition and hiring activity.
* Key team members
* Leadership profiles
* Employee growth trends
* Hiring status
* Open positions
* Department breakdown
Signals/Highlights New>} icon="signal">
40+ signal types across 8 categories.
**Funding:** Raised a round, signalled intent to raise, grant received
**Hiring:** Senior hire, open roles, open role in a key function, headcount moved
**Business:** Partnership announced, partnership talks, major customer win, expansion announced, expansion plan, new office opened, regulatory approval, certification achieved
**Product:** Product launched, feature shipped, mobile app released, homepage repositioned
**Growth:** Growth metrics published, revenue metrics published, web traffic moved, LinkedIn following moved
**Milestone:** IPO announced, IPO rumour, acquired, acquired a company, acquisition rumour
**Risk:** Shutdown announced, shutdown risk, layoffs announced, layoff rumour, senior departure, leadership change rumour, pivot announced
**Event:** Event attendance, award received, programme acceptance, news mention
Rumours and inferences are flagged `unconfirmed` in the response so you never report
one as an announcement.
Competitive landscape analysis.
* Similar companies
* Direct competitors
* Market alternatives
* Competitive positioning
## Use Cases
**Due Diligence & Deal Flow**
* Get company overview with employee count, founding year, and location
* Analyze complete funding history with rounds, valuations, and investors
* Review leadership team and key personnel
* Track signals and highlights for traction insights
```bash theme={null}
GET /api/v1/company/startup.com?enrich=funding,team,signals
```
**Lead Qualification & Prospecting**
* Identify company size and industry for segmentation
* Discover technology stack for product fit analysis
* Understand business model and target market
* Find key decision makers in leadership
```bash theme={null}
GET /api/v1/company/prospect.com?enrich=business,technology,team
```
**Talent Intelligence**
* Get company overview and culture indicators
* Track current hiring status and velocity
* View open positions by department
* Monitor employee growth trends
```bash theme={null}
GET /api/v1/company/target.com?enrich=team
```
**Competitive Intelligence**
* Analyze company positioning and business model
* Map technology landscape and adoption trends
* Discover direct competitors and alternatives
* Track industry benchmarks
```bash theme={null}
GET /api/v1/company/competitor.com?enrich=business,technology,competitors
```
## Quick Start
Create your API key from the [Dashboard](https://bouncewatch.com/api-panel/api-keys).
```bash theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com" \
-H "X-API-Key: YOUR_API_KEY"
```
Add module parameters based on your needs.
```bash theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=funding,team" \
-H "X-API-Key: YOUR_API_KEY"
```
Make your first API call in 2 minutes with the Quickstart guide →
## Building an AI agent?
The same data is available over the **Model Context Protocol**, so Claude, Cursor, VS Code or
your own agent can query the signal index as a tool — no glue code, same API key, same credits.
Seven tools, five ready-made workflows, and a coverage contract that stops an agent from
reporting our blind spots as market facts →
# MCP Server
Source: https://docs.bouncewatch.com/mcp/overview
Give your AI agent live company signals over the Model Context Protocol
BounceWatch exposes its signal index to AI agents over the **Model Context Protocol**.
One endpoint, ten tools, and your agent can answer the timing question directly:
which companies just raised, who is hiring, what changed and when — and be told when
something happens next, without asking again.
**Not a separate product.** An MCP client is another consumer of the same API key,
the same credit pool and the same rate limits as the [REST API](/api-reference/overview).
No extra subscription, no second key.
Claude Code, Claude Desktop, Cursor, VS Code, or anything that speaks MCP.
What each tool answers, what it costs, and the ready-made workflows.
## Endpoint
```
POST https://api.bouncewatch.com/api/v1/mcp
```
| | |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Transport** | Streamable HTTP — JSON-RPC 2.0, one POST, one JSON response |
| **Protocol revision** | `2025-06-18` |
| **Auth** | OAuth 2.1 (press Connect, approve in the browser), or `Authorization: Bearer YOUR_API_KEY` / `X-API-Key: YOUR_API_KEY` |
| **Not supported** | SSE streams, JSON-RPC batching |
Use `api.bouncewatch.com`, not the `bouncewatch.com` apex. The apex sits behind a
bot challenge that answers a scriptless POST with a 403 interstitial, and an agent
reads that as the server being broken.
## What makes this different from a search API
An agent will happily tell your user that a company is quiet. The dangerous version
of that sentence is the one where the company is not quiet — we simply have not looked
recently. A signal API that returns an empty array for both cases teaches the model to
report our blind spots as market facts.
So every answer from this server carries a `coverage` block:
```json theme={null}
{
"coverage": {
"monitoring": "periodic",
"channels": { "last_signal_at": "2026-07-19" },
"signal_absence_is_meaningful": false,
"interpretation": "We check in on this company rather than following it continuously, so its record is not guaranteed to be complete right now. Do not read an empty or short signal list as a quiet period — refresh_company brings it up to date immediately.",
"refresh_recommended": true
}
}
```
`monitoring` says how closely the company is followed — `continuous`, `active`,
`periodic`, `on_demand`, or `not_indexed` for one we have never seen. It is a band and
never a date, because a date would tell you when we last bothered to look rather than
anything about the company.
When `signal_absence_is_meaningful` is `false`, an empty signal list means **we have not
looked**, not **nothing happened**. `interpretation` is the same fact written for the
model, so a well-behaved client carries the caveat into its answer instead of dropping it.
`monitoring` and `signal_absence_is_meaningful` can disagree, and that is deliberate.
`periodic` reads as a service — we check in on this company — while the boolean is
`false`, telling the model it may not treat an empty list as a quiet period. Reassuring
tone, cautious claim. They answer different questions.
The same contract applies to filters. A funding stage we do not recognise is **rejected
with the list of valid values**, never silently narrowed to a handful of rows — because
seven results look like a real answer and an error does not.
Two independent observation channels feed a company: the profile crawl and the signal
stream. Only the signal date travels in `channels` — a company can have a long-stale
profile and fifty signals last month, and when that happens the block carries a
`profile_note` saying the firmographic fields are older than the events beside them.
Search results carry the same contract at the other scale. Every list states the size of the
universe it searched, so twelve results are read as twelve out of the set we watch rather than
as twelve in the world.
## A signal feed, not a company database
This is worth being blunt about, because it decides which questions get good answers.
We hold over a million companies, and in a given ninety days a little over thirteen thousand of
them produce a signal. That is not a coverage gap — a company that did nothing produces nothing
to observe — but it means "list every company that matches these attributes" is the wrong
question to bring here. "What changed, and when" is the right one.
The tools are shaped accordingly. `search_companies` cannot be pointed at the whole index: it
searches what we observed, and says how much that is. There is no path to a third-party company
directory. And `get_signal_taxonomy` returns a measured `coverage` block naming which signal
categories produce steadily and which are rare.
That last one matters more than it sounds. **Weight and volume run in opposite directions.**
Conference appearances and follower moves are constant and nearly worthless; funding rounds,
acquisitions and shutdowns are worth the most and happen a few hundred times a quarter across
every company we watch. An agent that filters for the valuable ones and finds little will
conclude our coverage is thin, when what it has actually found is how often those events occur.
So: search the dense end, **watch** the rare end. `watch_company` exists for exactly the signals
a window search will keep missing.
## What it will not do
* **It will not invent a company.** Ask for a domain we have never seen and you get an
explicit "not indexed" plus the option to scan it, not a guess.
* **It will not pass rumours off as events.** Rumour-class signals are labelled as such.
* **It will not follow instructions found in signal text.** Summaries are generated from
third-party public posts, so they are sanitised and tagged as untrusted data before the
model ever sees them.
* **It will not bill you for a failed call.** Errors are free; so is polling a running scan.
## Credits and limits
Tools are priced individually and each tool tells the model its own price, so an agent can
pace itself rather than loop blind. Full table on the [tools page](/mcp/tools).
Rate limits are the same ladder as the REST API — see [Rate Limits](/rate-limits). Agents
make many small calls rather than a few large ones, so a single conversation costs 8–13
requests; the ladder is sized for that.
The panel generates a ready-to-paste config for your client, with the key already filled
in, and a **Test connection** button that round-trips the real endpoint.
# Connect your client
Source: https://docs.bouncewatch.com/mcp/setup
Set up the BounceWatch MCP server in Claude Code, Claude Desktop, Cursor, VS Code or any MCP client
You need an API key. Grab it from the [MCP panel](https://bouncewatch.com/api-panel/mcp),
which also generates these configs with your key already filled in.
Two values are the whole setup:
| Server URL | `https://api.bouncewatch.com/api/v1/mcp` |
| ------------------ | ---------------------------------------- |
| **Authentication** | `Authorization: Bearer YOUR_API_KEY` |
## Claude Code
One command. `--scope user` makes it available in every project; drop the flag to add it
to the current project only.
```bash theme={null}
claude mcp add --transport http bouncewatch https://api.bouncewatch.com/api/v1/mcp \
--header "Authorization: Bearer YOUR_API_KEY" \
--scope user
```
Or write it into `.mcp.json` by hand:
```json theme={null}
{
"mcpServers": {
"bouncewatch": {
"type": "http",
"url": "https://api.bouncewatch.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
The `type` field is required. An entry with a `url` and no `type` is read as a local
command and silently skipped.
Verify with `claude mcp list`, then ask Claude Code a question about timing.
## Claude — the Connect button
**No key needed.** Open **Settings → Connectors → Add custom connector**, paste the
server URL and press Connect. Claude discovers the sign-in flow on its own; you approve
once in the browser and it is done.
```
https://api.bouncewatch.com/api/v1/mcp
```
Approved connections appear on your [MCP panel](https://bouncewatch.com/api-panel/mcp),
and you can revoke any of them from there. Custom connectors need a paid Claude plan — on
the free plan, use the config below.
## Claude Desktop with an API key
Claude Desktop can also reach us through `mcp-remote`, a small local bridge that
attaches your key to every call.
Open **Settings → Developer → Edit config**, or edit the file directly:
* macOS — `~/Library/Application Support/Claude/claude_desktop_config.json`
* Windows — `%APPDATA%\Claude\claude_desktop_config.json`
```json theme={null}
{
"mcpServers": {
"bouncewatch": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.bouncewatch.com/api/v1/mcp",
"--header",
"Authorization:${BOUNCEWATCH_AUTH}"
],
"env": {
"BOUNCEWATCH_AUTH": "Bearer YOUR_API_KEY"
}
}
}
}
```
Restart Claude after saving.
The key goes in `env` and the header has no space after the colon on purpose. Claude
Desktop mangles spaces inside `args` when it shells out to `npx`, so
`"Authorization: Bearer …"` written inline arrives broken. Spaces inside an environment
variable survive.
## Cursor
The [MCP panel](https://bouncewatch.com/api-panel/mcp) has a one-click **Add to Cursor**
button. Or add it to `~/.cursor/mcp.json` yourself:
```json theme={null}
{
"mcpServers": {
"bouncewatch": {
"url": "https://api.bouncewatch.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
## VS Code
One-click **Add to VS Code** from the panel, or `.vscode/mcp.json` in your workspace:
```json theme={null}
{
"servers": {
"bouncewatch": {
"type": "http",
"url": "https://api.bouncewatch.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
## Anything else
The server speaks plain JSON-RPC 2.0 over a single POST. Any MCP client library works, and
so does raw HTTP:
```bash cURL theme={null}
curl -X POST https://api.bouncewatch.com/api/v1/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
```javascript JavaScript theme={null}
const res = await fetch('https://api.bouncewatch.com/api/v1/mcp', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
});
const { result } = await res.json();
```
```python Python theme={null}
import requests
res = requests.post(
'https://api.bouncewatch.com/api/v1/mcp',
headers={'Authorization': 'Bearer YOUR_API_KEY'},
json={'jsonrpc': '2.0', 'id': 1, 'method': 'tools/list'},
)
tools = res.json()['result']['tools']
```
Supported methods: `initialize`, `tools/list`, `tools/call`, `prompts/list`, `prompts/get`,
`ping`. Notifications are accepted and answered with `202`.
## Security
Your key is embedded in these config files. Do not commit `mcp.json` or
`claude_desktop_config.json` to a shared repository. If a key is exposed, rotate it on the
[API Keys page](https://bouncewatch.com/api-panel/api-keys) — the old one stops working
immediately.
## Troubleshooting
You are pointing at `bouncewatch.com` instead of `api.bouncewatch.com`. The apex sits
behind a bot challenge that a scriptless POST cannot pass.
The key is missing, mistyped, or was rotated. Check it against the
[MCP panel](https://bouncewatch.com/api-panel/mcp) and use the **Test connection**
button — it round-trips the real endpoint, so it proves the whole path, not just the key.
The balance hit zero. Upgrade the plan on the [billing page](https://bouncewatch.com/api-panel/billing) —
an upgrade mid-period keeps the remaining balance and adds the new allowance on top.
One-time credit packs are not available yet.
No call succeeds until then; the error your agent sees says exactly this.
Agents make many small calls. Wait about a minute and prefer fewer, broader calls —
one `search_signals` over twenty `get_company` calls. Limits by plan are on the
[Rate Limits](/rate-limits) page.
Expected. This server answers over POST only; there is no SSE stream to open. Clients
that probe with GET get a readable JSON-RPC error rather than a bare 405.
In Claude Code and VS Code the `type` field is required — an entry with a `url` and no
`type` is treated as a local command and skipped. Run `claude mcp list` to confirm.
# Tools and prompts
Source: https://docs.bouncewatch.com/mcp/tools
The ten MCP tools, what they cost, and the ready-made workflows
## Tools
Every tool description ends with its own price, generated from config, so the model can see
the meter running and pace itself instead of looping blind.
| Tool | Credits | What it answers |
| --------------------- | -----------: | ------------------------------------------------------------------------------------------------------------------------- |
| `search_signals` | 12 | Which companies did X recently — the timing question. Supports signal stacking for "raised **and** hiring". |
| `search_companies` | 5 | Which of the companies we observe fit these firmographics. |
| `find_company` | 3 | A company name to its domain — the argument every tool below needs. |
| `get_company` | 10 + modules | Firmographic profile of one company. |
| `get_company_signals` | 8 | Dated signal timeline for one company. |
| `refresh_company` | 36 default | Queues a live scan and returns a batch id. Also how a domain we have never seen gets indexed. |
| `get_refresh_status` | **free** | Polls a running scan, and waits for its writes to settle. Free by design — the async model only works if waiting is free. |
| `watch_company` | 5 | Registers a standing interest. The only tool that outlives the session it was called in. |
| `check_watches` | **free** | Collects what has fired since you last asked. |
| `get_signal_taxonomy` | **free** | The signal vocabulary, plus which categories are dense and which are rare. Call it once per session. |
Module prices come from the same table as the REST API, so the same data costs the same
whichever door you come through. See [Credits and Modules](/credits-and-modules).
**Failed calls are never billed.**
### search\_signals
The cross-company question: *who did something worth reacting to, and when.*
| Parameter | Type | Notes |
| --------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `signal_keys` | string\[] | Specific signal types. Unrecognised keys are rejected with the valid list. |
| `categories` | string\[] | Broader groupings instead of individual keys. |
| `require_all_keys` | boolean | Signal stacking — every listed signal must be present, not just one. |
| `days` | integer | Lookback window, up to 365. |
| `country` | string | ISO country code. |
| `min_employees` / `max_employees` | integer | Headcount bounds. |
| `funding_stage` | string | Matched loosely: `series-a`, `Series A` and `SERIES_A` all land on the same stage. An unknown value is rejected with the valid list. The costliest filter here — see the note below. |
| `min_weight` | integer | Floor on how much a signal has to matter, 1-10. |
| `sort` | string | Result ordering. |
| `limit` | integer | Default 25, max 100. |
The response also reports **how many companies produced signals in the window at all**, so a
ranked list of 25 is not mistaken for the whole market.
`require_all_keys` is the one worth knowing about. "Raised a round" is a weak buying signal
on its own; "raised a round **and** is hiring sales roles" is a much smaller, much warmer list.
**`funding_stage` narrows twice.** We hold a stage for about 60% of the companies we observe,
and the rest are excluded rather than guessed at — so the filter cuts by stage *and* by what
we happen to know. A short result is as likely to be a gap in our records as a fact about the
market, and the response says so in `notes`.
### find\_company
Every tool here takes a **domain**, and you will usually have a **name**. Guessing the domain is
the expensive mistake: a wrong guess comes back as "not indexed" for a company we actually hold,
and sends the agent on to spend a scan on a host nobody checked.
`find_company` takes a `name` (a domain works too and is resolved directly), an optional
`country` and `limit`, and returns a handful of candidates with the few fields that tell them
apart — country, founding year, headcount, and when we last saw a signal. It never picks for
you. Once you have chosen, `get_company` returns the full profile.
If search is unavailable it returns an explicit error rather than an empty list, because
"we could not look" and "not in the index" are different answers and only one of them is true.
### search\_companies
Firmographic search over the companies we watch: `country`, `min_employees`, `max_employees`,
`funding_stage`, `observed_within_days`, `founded_after`, `founded_before`, `limit`.
`observed_within_days` is floored at 30 and capped at 365. There is deliberately no way to
search the whole index: this tool covers what we observe, and every answer carries a `coverage`
block saying how many companies that is.
Each result reports `signal_activity.level` — `light`, `moderate`, `high` — rather than a raw
count. Nobody acts differently on six signals versus eight, and a bare number invites a
comparison against vendors who count every conference badge. `latest_signal_date` stays exact;
timing is the thing worth being precise about.
### get\_company / get\_company\_signals
`get_company` takes a `domain` and an optional `include` list of enrichment modules.
`get_company_signals` takes a `domain` plus `days`, `categories`, `signal_keys` and `limit`,
and returns the dated timeline — the raw material for a "why now" argument.
### refresh\_company / get\_refresh\_status
`refresh_company` takes a `domain` and optional `modules`, queues a scan and hands back a
`batch_id`. `get_refresh_status` polls that id for free until the scan lands — the call blocks
and waits for you, so a scan costs two or three patient calls rather than twenty impatient ones.
A scan only refreshes what you ask for, and anything left out keeps the date it already had.
The default is `signals` + `funding`, the two that go out of date fastest; every result names
both what it refreshed and what it did not.
**Read `is_finished`, not `status`.** A batch reaches `completed` when its jobs report back,
but the signal analysis they started keeps writing rows for a few seconds afterwards. Read too
early and you get the pre-scan picture with a completed stamp on it. `status` becomes
`settling` for that window and `is_finished` stays false until the writes stop. When it
finishes, `signals_added` says how many new signals the scan actually produced — zero is a
real answer.
### watch\_company / check\_watches
MCP is request-response: a server can only ever answer, never initiate. So every company-data
MCP server is read-only pull, and an agent finds out something changed only if a human happens
to ask again.
It does not need the protocol. **Registration is an ordinary tool call; the wake-up travels out
of band.** `watch_company` records a standing interest in a company, optionally narrowed by
`signal_keys` or `min_weight`. When a matching signal lands it is delivered two ways:
* **An inbox**, drained by `check_watches` — free, and it returns only what is new since your
last call. This is the path for an agent living inside a chat client, which has no address to
be called back on.
* **A webhook**, if the key has one configured — `signal.matched`, signed exactly like an
enrichment webhook so you do not need a second verifier. This is the path that genuinely wakes
a hosted agent.
The webhook never replaces the inbox row, so a failed delivery is not a lost event.
This is the right instrument for the rare, high-value signals. A funding round is a few
hundred events across the whole index in a quarter — the odds one lands inside the window you
happen to search are poor. You do not search for a funding round; you wait for one.
Guardrails, because an agent loop can dispatch scans far faster than a person would: a daily
cap per key, per-plan concurrency, a 24-hour cooldown on the same domain, and a global
in-flight lock so two keys cannot scan the same domain at once.
## Prompts
The server also exposes MCP **prompts** — ready-made workflows that clients surface as a menu
or slash command. Rendering one is free; the tools it triggers are billed normally.
| Prompt | Arguments | Job |
| --------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pre_round_radar` | `country`, `max_employees` | Companies showing momentum that have **not** raised recently — the ones likely raising soon. Deal sourcing. |
| `why_now` | `domain` *(required)*, `offering` | An outreach angle for one account: what changed, why it matters, what to open with. |
| `funded_and_hiring` | `country`, `days` | Closed a round **and** hiring in the same window — new budget plus a mandate to spend it. |
| `account_watch_brief` | `domains` *(required)*, `days` | What happened across a named list of accounts, ranked by what deserves attention. |
| `risk_scan` | `domains` *(required)* | Layoffs, shutdowns, leadership exits and other distress indicators. Finding nothing is the normal and good outcome — it is a screen, not a search. |
The real value is not the phrasing. Each template has the correct tool sequence and the
freshness caveats baked in — *check coverage before calling a company quiet, do not report a
rumour as an event* — so a first-time user gets expert behaviour without knowing the rules.
## Errors
Two channels, and the difference matters:
* **JSON-RPC errors** mean the *call* was malformed — unknown method, bad envelope. The client
sees them; usually the model does not.
* **Tool errors** mean the call was fine but the *answer* is a failure — out of credits, unknown
domain, quota hit. These come back as results so the model can read them and adapt.
Rejections that happen before the protocol is reached — auth, credits, rate limits — are
rewrapped as JSON-RPC errors with an actionable sentence, rather than being handed to the agent
as an opaque transport failure. A `429` also carries `Retry-After`.
```json theme={null}
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32603,
"message": "Out of credits or no active plan. Upgrade the plan at https://bouncewatch.com/api-panel/billing — no further calls will succeed until then."
}
}
```
# Quickstart
Source: https://docs.bouncewatch.com/quickstart
Make your first API call in 2 minutes
## Prerequisites
You need an API key. If you don't have one, [create it here](https://bouncewatch.com/api-panel/api-keys).
**You also need a webhook URL.** Any request that triggers a fresh enrichment is
refused with `400 webhook_required` unless we have somewhere to deliver the result.
Set one once in your [API Panel](https://bouncewatch.com/api-panel/webhooks), or send
`X-Webhook-URL` per request as the examples below do.
No endpoint yet? Open [webhook.site](https://webhook.site), copy the URL it gives you
and paste it in. It must be `https://`.
**Prefer Postman?** Download our [Postman Collection](https://bouncewatch.com/postman/BounceWatch_Postman_Collection.zip) to get started quickly with pre-configured requests.
## Your First API Call
Use the domain name to fetch **base data** for any company:
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Webhook-URL: https://webhook.site/YOUR-ID"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/company/stripe.com', {
headers: {
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://webhook.site/YOUR-ID'
}
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://webhook.site/YOUR-ID'
}
)
data = response.json()
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/company/stripe.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'X-Webhook-URL: https://webhook.site/YOUR-ID'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
**A domain we have never enriched** — returns `202 Accepted` with a batch ID. Note
the field is `requested_modules` on this path; a domain already in our index returns
the same 202 with `modules_requested` and a `current_data` block alongside it.
```json theme={null}
{
"success": true,
"message": "New domain added to enrichment queue",
"domain": "stripe.com",
"enrichment_status": "processing",
"batch_id": "batch_abc123xyz",
"requested_modules": [],
"credits_reserved": 10,
"credits_remaining": 2490,
"webhook_notification": "enabled",
"status_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/status",
"results_endpoint": "https://api.bouncewatch.com/api/v1/enrichment/batch_abc123xyz/results"
}
```
**No webhook configured** — returns `400`, and nothing is charged:
```json theme={null}
{
"success": false,
"error": "webhook_required",
"message": "Webhook URL is required for enrichment requests",
"hint": "Configure a webhook URL in your API panel at https://bouncewatch.com/api-panel/webhooks or provide it via the X-Webhook-URL header.",
"quick_test": "For quick testing, you can get a free webhook URL from https://webhook.site"
}
```
**Same domain within 24 hours** — returns `200 OK` with instant data (free):
```json theme={null}
{
"success": true,
"data": {
"company": {
"name": "Stripe",
"domain": "stripe.com",
"description": "Financial infrastructure for the internet",
"founded_year": 2010,
"employee_count": 8000,
"headquarter_city": "San Francisco",
"headquarter_country": "United States",
"linkedin_url": "https://linkedin.com/company/stripe",
"twitter_url": "https://twitter.com/stripe"
}
},
"credits_used": 0,
"dedup_note": "This domain was enriched within the last 24 hours. Cached data is returned at no charge."
}
```
Use the `enrich` parameter to get deeper data. Here's a **full enrichment** request with all 6 modules:
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=business,technology,funding,team,signals,competitors" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Webhook-URL: https://webhook.site/YOUR-ID"
```
```javascript JavaScript theme={null}
const response = await fetch(
'https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=business,technology,funding,team,signals,competitors',
{
headers: {
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://webhook.site/YOUR-ID'
}
}
);
const data = await response.json();
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={
'X-API-Key': 'YOUR_API_KEY',
'X-Webhook-URL': 'https://webhook.site/YOUR-ID'
},
params={'enrich': 'business,technology,funding,team,signals,competitors'}
)
data = response.json()
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/company/stripe.com?enrich=business,technology,funding,team,signals,competitors');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'X-Webhook-URL: https://webhook.site/YOUR-ID'
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
**Full enrichment uses 60 credits:** Base (10) + Business (6) + Technology (4) + Funding (10) + Team (6) + Signals (16) + Competitors (8)
You don't have to use all modules — pick only the ones you need. For example `?enrich=funding,team` costs just 26 credits.
## Enrichment Modules
Choose which modules to add:
| Module | Credits | What's Included? |
| ------------- | ------- | --------------------------------------- |
| `business` | +6 | Industry, business model, target market |
| `technology` | +4 | Tech stack, 100+ technologies |
| `funding` | +10 | Funding rounds, investors |
| `team` | +6 | Team members, hiring status |
| `signals` | +16 | 40+ signal types, highlights |
| `competitors` | +8 | Competitors, similar companies |
Use comma-separated values for multiple modules: `?enrich=business,technology,funding`
## Check Your Credit Balance
```bash cURL theme={null}
curl -X GET "https://api.bouncewatch.com/api/v1/account/credits" \
-H "X-API-Key: YOUR_API_KEY"
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/credits', {
headers: { 'X-API-Key': 'YOUR_API_KEY' }
});
```
```python Python theme={null}
response = requests.get(
'https://api.bouncewatch.com/api/v1/account/credits',
headers={'X-API-Key': 'YOUR_API_KEY'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/credits');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: YOUR_API_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
```json Response theme={null}
{
"success": true,
"data": {
"credits": {
"balance": 4580,
"used": 420,
"limit": 5000,
"total_purchased": 0,
"percentage_used": 8.4,
"resets_at": "2026-02-01T09:14:22.000000Z"
}
}
}
```
## Next Steps
API key security and best practices
Credit system and module details
Full endpoint documentation and examples
Set up webhook to receive enrichment results
# Rate Limits
Source: https://docs.bouncewatch.com/rate-limits
Understanding API rate limits and how to handle them
## Rate Limit Overview
Rate limits protect the API from abuse and ensure fair usage for all customers. Limits are applied **per API key**.
## Plan Limits
| Plan | Requests/Minute | Requests/Day | Credits/Month |
| ------------ | --------------- | ------------ | ------------------ |
| Free Trial | 60 | 500 | 2,500 *(one-time)* |
| Starter | 90 | 1,000 | 5,000 |
| Professional | 150 | 5,000 | 25,000 |
| Business | 240 | 10,000 | 125,000 |
| Enterprise | 360 | 15,000 | 600,000 |
Need custom limits? [Contact us](mailto:sedat@bouncewatch.com) for custom enterprise solutions.
## Rate Limit Headers
Every authenticated response includes rate limit information in the headers:
```http theme={null}
X-RateLimit-Limit: 150
X-RateLimit-Remaining: 145
X-RateLimit-Reset: 1786011345
X-RateLimit-Limit-Minute: 150
X-RateLimit-Remaining-Minute: 145
X-RateLimit-Limit-Day: 5000
X-RateLimit-Remaining-Day: 4820
```
| Header | Description |
| ------------------------------------------------ | ------------------------------------------------ |
| `X-RateLimit-Limit` | Maximum requests allowed per minute |
| `X-RateLimit-Remaining` | Requests remaining in the current minute |
| `X-RateLimit-Reset` | Unix timestamp when the per-minute window resets |
| `X-RateLimit-Limit-Minute` / `-Remaining-Minute` | Same as the two above, explicitly scoped |
| `X-RateLimit-Limit-Day` / `-Remaining-Day` | The daily budget and what is left of it |
There are two independent budgets. `X-RateLimit-Remaining` only covers the minute — check
`X-RateLimit-Remaining-Day` too if you run long batches.
## Handling Rate Limits
When you exceed either limit, you'll receive a `429 Too Many Requests` with a
`Retry-After` header:
```json theme={null}
{
"success": false,
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please retry after 45 seconds.",
"retry_after": 45,
"limits": {
"per_minute": 150,
"per_day": 5000,
"current_minute": 150,
"current_day": 3204
},
"reset_time": {
"next_minute": "2026-02-10T14:23:00.000000Z",
"next_day": "2026-02-11T00:00:00.000000Z"
}
}
```
`retry_after` counts seconds to the next window that will actually admit you — the top
of the minute normally, midnight if it was the daily budget you exhausted.
A `429` can also mean `concurrent_limit_reached`: too many enrichments in flight at once
(2 on trial, up to 100 on enterprise). Check the `error` field — waiting a minute will
not help there, you need an in-flight enrichment to finish.
### Implementation Examples
```javascript theme={null}
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i);
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
// Usage
const response = await fetchWithRetry(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
{ headers: { 'X-API-Key': API_KEY } }
);
```
```python theme={null}
import time
import requests
def fetch_with_retry(url, headers, max_retries=3):
for i in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 2 ** i))
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
continue
return response
raise Exception('Max retries exceeded')
# Usage
response = fetch_with_retry(
'https://api.bouncewatch.com/api/v1/company/stripe.com',
headers={'X-API-Key': API_KEY}
)
```
```php theme={null}
function fetchWithRetry($url, $headers, $maxRetries = 3) {
for ($i = 0; $i < $maxRetries; $i++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 429) {
$retryAfter = pow(2, $i);
echo "Rate limited. Retrying in {$retryAfter}s...\n";
sleep($retryAfter);
continue;
}
return $response;
}
throw new Exception('Max retries exceeded');
}
```
## Best Practices
When rate limited, wait progressively longer between retries (1s, 2s, 4s, 8s...)
For bulk operations, implement a request queue to spread requests evenly and stay within limits
Cache company data for 24 hours to reduce the number of API calls needed
Check your usage regularly via the [dashboard](https://bouncewatch.com/api-panel) or `/account/usage` endpoint
For high-volume use cases, consider implementing a simple queue system in your application to automatically space out requests and avoid hitting rate limits.
Set up webhooks to get notified about credit usage →
# Webhooks
Source: https://docs.bouncewatch.com/webhooks
Receive real-time notifications for API events
## Overview
Webhooks allow you to receive real-time HTTP notifications when events occur in your BounceWatch account. Instead of polling the API, webhooks push data to your server automatically.
Webhooks are required for receiving enrichment results. Set the `X-Webhook-URL` header on your API requests or configure a default webhook in the [API Panel](https://bouncewatch.com/api-panel/webhooks).
## Quick Setup
Create an HTTPS endpoint on your server to receive webhook events.
Register your endpoint via the API or dashboard.
Optionally validate incoming webhooks using HMAC-SHA256 signatures for enhanced security.
Return a 2xx status code within 30 seconds.
🧪 **Want to test webhooks quickly?** Use [webhook.site](https://webhook.site) to get a free test URL instantly - no setup required! [See testing guide →](#testing-webhooks)
## Configuring Webhooks
### Via API
```bash theme={null}
POST /api/v1/account/webhook
```
One URL, every event. There is no `events` array to subscribe with — your endpoint
receives all enrichment events for the account.
```bash cURL theme={null}
curl -X POST "https://api.bouncewatch.com/api/v1/account/webhook" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://webhook.site/YOUR-ID"}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://api.bouncewatch.com/api/v1/account/webhook', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ webhook_url: 'https://webhook.site/YOUR-ID' })
});
```
```python Python theme={null}
response = requests.post(
'https://api.bouncewatch.com/api/v1/account/webhook',
headers={'X-API-Key': 'YOUR_API_KEY'},
json={'webhook_url': 'https://webhook.site/YOUR-ID'}
)
```
```php PHP theme={null}
$ch = curl_init('https://api.bouncewatch.com/api/v1/account/webhook');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: YOUR_API_KEY',
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'webhook_url' => 'https://webhook.site/YOUR-ID'
]));
$response = curl_exec($ch);
$data = json_decode($response, true);
```
### Response
```json theme={null}
{
"success": true,
"message": "Webhook URL updated successfully",
"data": {
"webhook_url": "https://webhook.site/YOUR-ID"
}
}
```
The URL must be `https://`. Placeholder hosts — `example.com`, `your-domain.com`,
`localhost`, `127.0.0.1` — are rejected with `422 invalid_webhook_url`.
### Where your signing secret lives
Your signing secret is created with your account and is **not** returned by this endpoint.
Find it — and rotate it — in
[API Panel → Webhooks](https://bouncewatch.com/api-panel/webhooks). It looks like
`whs_` followed by 64 hex characters.
That page also has a **Send test webhook** button. It signs exactly like a real
delivery, so a verifier that accepts the test will accept production traffic. The only
difference is `_meta.is_test: true` in the payload.
## Available Events
Triggered when a realtime enrichment job completes successfully.
```json theme={null}
{
"event": "enrichment.completed",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "completed",
"requested_modules": ["business", "technology"],
"requested_at": "2025-11-23T10:00:00Z",
"completed_at": "2025-11-23T10:05:30Z",
"duration_seconds": 330,
"credits": {
"reserved": 20,
"used": 20,
"refunded": 0
},
"data_url": "https://api.bouncewatch.com/api/v1/enrichment/batch_1gVwXby8PsYHoMQR/results",
"_meta": {
"api_version": "2.0",
"webhook_attempt": 1
}
}
```
The webhook payload does not contain enrichment data directly. Use the `data_url` with your API key to fetch the full results.
Triggered when enrichment fails due to a system error. Credits are automatically refunded.
```json theme={null}
{
"event": "enrichment.failed",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "stripe.com",
"status": "failed",
"error": "Enrichment process timed out. Please try again.",
"credits": {
"reserved": 20,
"used": 0,
"refunded": 20
},
"_meta": {
"api_version": "2.0",
"webhook_attempt": 1
}
}
```
Credits are **reserved** upfront but **automatically refunded** if enrichment fails.
Triggered when the domain exists but no enrichment data could be found. Credits are refunded.
```json theme={null}
{
"event": "enrichment.no_data_found",
"batch_id": "batch_1gVwXby8PsYHoMQR",
"domain": "unknown-startup.xyz",
"status": "no_data_found",
"message": "We could not find enrichment data for this domain. This could mean the company has limited online presence, the domain is new, or it may not be a valid business domain.",
"suggestions": [
"Verify the domain is correct and belongs to an active business",
"Try again later as our data sources update regularly",
"Contact support if you believe this is an error"
],
"credits": {
"reserved": 20,
"used": 0,
"refunded": 20
},
"_meta": {
"api_version": "2.0",
"webhook_attempt": 1
}
}
```
Credits are **automatically refunded** when no data is found for a domain.
Those three are the complete list. There are no credit-balance webhooks — poll
[`GET /account/credits`](/api-reference/account#get-credit-balance) if you want to alert
on a low balance, or watch the `credits_remaining` field returned with cached responses.
## Webhook Security (Optional)
Signature verification is **optional** but strongly recommended for production environments to ensure webhook authenticity.
All webhook requests include security headers for verification:
| Header | Description |
| ------------------------- | ----------------------------------------------------------------------------- |
| `X-BounceWatch-Signature` | HMAC-SHA256 over `"{timestamp}.{raw body}"` — **not** the body alone |
| `X-BounceWatch-Timestamp` | Unix timestamp when the webhook was sent; part of the signed string |
| `X-BounceWatch-Event` | Event type (e.g. `enrichment.completed`) |
| `X-BounceWatch-Batch-ID` | The batch this delivery is for. Stable across retries — use it to deduplicate |
Sign the **raw body exactly as received**. Parsing the JSON and re-serialising it
changes key order and escaping, and the signature will not match.
### Verifying Signatures (Optional)
Always verify webhook signatures to ensure requests are from BounceWatch.
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) {
// Check timestamp (reject if older than 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Webhook timestamp too old');
}
// Calculate expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
throw new Error('Invalid webhook signature');
}
return true;
}
// Express.js handler
app.post('/webhooks/bouncewatch', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-bouncewatch-signature'];
const timestamp = req.headers['x-bouncewatch-timestamp'];
const payload = req.body.toString();
try {
verifyWebhook(payload, signature, timestamp, WEBHOOK_SECRET);
const event = JSON.parse(payload);
// Handle the event
console.log(`Received ${event.event} event`);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook error:', error.message);
res.status(401).send('Invalid signature');
}
});
```
```python theme={null}
import hmac
import hashlib
import time
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = 'whs_4f8a1c6e90b23d75e8104a6fbc2937de'
def verify_webhook(payload, signature, timestamp, secret):
# Check timestamp (reject if older than 5 minutes)
if abs(time.time() - int(timestamp)) > 300:
raise ValueError('Webhook timestamp too old')
# Calculate expected signature
signed_payload = f"{timestamp}.{payload}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures
if not hmac.compare_digest(signature, expected_signature):
raise ValueError('Invalid webhook signature')
return True
@app.route('/webhooks/bouncewatch', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-BounceWatch-Signature')
timestamp = request.headers.get('X-BounceWatch-Timestamp')
payload = request.get_data(as_text=True)
try:
verify_webhook(payload, signature, timestamp, WEBHOOK_SECRET)
event = request.get_json()
# Handle the event
print(f"Received {event['event']} event")
return 'OK', 200
except ValueError as e:
print(f"Webhook error: {e}")
return 'Invalid signature', 401
```
```php theme={null}
300) {
throw new Exception('Webhook timestamp too old');
}
// Calculate expected signature
$signedPayload = "{$timestamp}.{$payload}";
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Compare signatures
if (!hash_equals($signature, $expectedSignature)) {
throw new Exception('Invalid webhook signature');
}
return true;
}
// Get webhook data
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_BOUNCEWATCH_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_BOUNCEWATCH_TIMESTAMP'] ?? '';
try {
verifyWebhook($payload, $signature, $timestamp, $webhookSecret);
$event = json_decode($payload, true);
// Handle the event
error_log("Received {$event['event']} event");
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
error_log("Webhook error: " . $e->getMessage());
http_response_code(401);
echo 'Invalid signature';
}
```
## Handling Events
Example of handling different event types:
```javascript theme={null}
app.post('/webhooks/bouncewatch', (req, res) => {
const event = req.body;
switch (event.event) {
case 'enrichment.completed':
handleEnrichmentComplete(event);
break;
case 'enrichment.failed':
// Credits already refunded. Safe to retry the domain later.
handleEnrichmentFailed(event);
break;
case 'enrichment.no_data_found':
// Credits already refunded. Do not retry for 24 hours — the domain
// is on cooldown and further requests answer 409.
markDomainUnavailable(event.domain);
break;
default:
console.log(`Unknown event: ${event.event}`);
}
res.status(200).send('OK');
});
async function handleEnrichmentComplete(event) {
// Fetch the enriched data
const response = await fetch(event.data_url, {
headers: { 'X-API-Key': API_KEY }
});
const data = await response.json();
// Store in your database
await saveCompanyData(event.domain, data);
console.log(`Enrichment complete for ${event.domain}`);
}
```
## Retry Policy
If your endpoint doesn't respond with a 2xx status code, we'll retry:
| Attempt | Delay |
| ------- | ---------- |
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts the delivery is marked failed and we stop. **Nothing is lost** —
the enrichment itself completed and the data stays available at
`GET /api/v1/enrichment/{batch_id}/results`. Delivery status per batch is visible on
the status endpoint under `webhook`, and in the dashboard.
## Best Practices
Return 200 immediately, then process async. Don't make the webhook wait for your business logic.
Always verify webhook signatures. Never trust incoming data without verification.
A retry re-sends the same `X-BounceWatch-Batch-ID`. Key your processing on it so a
redelivery is a no-op.
Webhook URLs must be `https://`. Plain http is rejected when you configure it, both
via the API panel and the `X-Webhook-URL` header.
## Testing Webhooks
The easiest way to test webhooks is using [webhook.site](https://webhook.site):
Visit [webhook.site](https://webhook.site) - you'll automatically get a unique URL like:
```
https://webhook.site/abc123-def456-ghi789
```
Use this URL when configuring your webhook via the API:
```bash theme={null}
curl -X POST "https://api.bouncewatch.com/api/v1/account/webhook" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://webhook.site/YOUR-UNIQUE-ID"}'
```
Or skip storing it and send `X-Webhook-URL: https://webhook.site/YOUR-UNIQUE-ID`
on individual requests.
All webhook requests will appear in real-time on your webhook.site dashboard. You can inspect headers, payload, and response details.
webhook.site is free and requires no setup - perfect for quick testing. For local development, you can also use [ngrok](https://ngrok.com) to expose your localhost.