Webhooks
Configure webhooks to receive real-time notifications when visitors are identified, when a phone is later unlocked (visitor.phone_revealed), or when billing events occur. Webhooks use HMAC-SHA256 signatures for verification.
Phone enrichment: visitor.identified often has an empty visitor.phone. After a successful phone reveal (manual or auto), Bullseye reuses the same identification webhook URL with event_type: visitor.phone_revealed and visitor.phone populated. You can also poll visitor list/profile or call POST /partner/organizations/\{organizationId\}/visitors/\{visitorId\}/phone-number-search (B2B only; 5 identification credits on found only).
Get Webhook Configuration
Retrieve the current webhook configuration for identification and billing.
Request:
GET /partner/webhooksResponse:
{
"identification": {
"url": "https://example.com/webhooks/identify",
"enabled": true,
"hasSecret": true
},
"billing": {
"url": "https://example.com/webhooks/billing",
"enabled": true,
"hasSecret": true
}
}Example:
curl -X GET "https://api.app.bullseye.so/api/v1/partner/webhooks"
-H "X-Partner-API-Key: your-api-key"Update Identification Webhook
Configure the webhook for visitor identification events.
Request:
PUT /partner/webhooks/identificationBody:
{
"url": "https://example.com/webhooks/identify",
"secret": "whsec_your_webhook_secret",
"enabled": true
}| Field | Type | Required | Description |
|---|---|---|---|
url | string | No | Webhook endpoint URL |
secret | string | No | Secret for HMAC signature verification |
enabled | boolean | No | Enable or disable the webhook |
Response:
{
"identification": {
"url": "https://example.com/webhooks/identify",
"enabled": true,
"hasSecret": true
}
}Update Billing Webhook
Configure the webhook for billing events.
Request:
PUT /partner/webhooks/billingBody:
{
"url": "https://example.com/webhooks/billing",
"secret": "whsec_your_webhook_secret",
"enabled": true
}Response:
{
"billing": {
"url": "https://example.com/webhooks/billing",
"enabled": true,
"hasSecret": true
}
}Send Test Webhook
Send a test webhook to verify your endpoint is configured correctly.
Request:
POST /partner/webhooks/testBody:
{
"type": "identification"
}Valid types: identification, billing
Response:
{
"success": true,
"message": "Test webhook sent successfully"
}Example:
curl -X POST "https://api.app.bullseye.so/api/v1/partner/webhooks/test"
-H "X-Partner-API-Key: your-api-key"
-H "Content-Type: application/json"
-d '{"type": "identification"}'Get Webhook Logs
Retrieve webhook delivery logs for debugging and monitoring.
Request:
GET /partner/webhooks/logsQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number |
pageSize | integer | Items per page |
type | string | Filter by identification or billing |
status | string | Filter by delivery status |
organizationId | string | Filter by organization |
Response:
{
"logs": [
{
"id": "uuid",
"type": "identification",
"organizationId": "uuid",
"status": "delivered",
"statusCode": 200,
"attemptedAt": "2025-02-27T10:30:00Z",
"responseTimeMs": 150
}
],
"total": 100,
"page": 1,
"pageSize": 20
}Identification Webhook Payload
When a visitor is identified, your webhook receives a POST request with an IdentificationWebhookPayload JSON body. Fields may be empty strings or null when unavailable.
IP intelligence (session)
Populated from ipinfo reverse lookup at track time:
| Field | Description |
|---|---|
isp | Network / ISP name |
asn | e.g. AS7018 |
connection_type | isp, hosting, business, education, etc. |
is_hosting | Datacenter / hosting / cloud egress (always sent; false = residential or non-hosting) |
is_anonymous | VPN, proxy, Tor, relay (always sent; false = non-anonymized) |
country, region, city | Geo from reverse-IP when Core tier is enabled |
Use is_hosting and is_anonymous in Partner API segmentation rules (is_hosting, is_anonymous condition types). ip_address is omitted when the sub-org has IP tracking disabled.
See also: API reference - IP intelligence.
Full example payload
{
"event_type": "visitor.identified",
"timestamp": "2025-01-15T10:30:00Z",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"workspace": "Acme Corp",
"visitor": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "jane.smith@acme.com",
"personal_email": "jane.doe@gmail.com",
"first_name": "Jane",
"last_name": "Smith",
"job_title": "VP of Marketing",
"phone": "+14155551234",
"location": "San Francisco, CA",
"photo_url": "https://example.com/photo.jpg",
"linkedin_url": "https://linkedin.com/in/janesmith",
"facebook_url": null,
"twitter_url": null,
"address": "123 Market St",
"address_2": null,
"city": "San Francisco",
"state": "CA",
"postal_code": "94102",
"gender": null,
"age_range": null,
"income_range": null,
"net_worth": null,
"homeowner": null,
"married": null,
"children": null,
"icp": true,
"icp_statuses": { "enterprise-saas": true },
"persona_match": true,
"created_at": "2025-01-10T08:00:00Z",
"last_seen_at": "2025-01-15T10:30:00Z"
},
"company": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "Acme Corp",
"domain": "acme.com",
"industry": "Technology",
"size": "500",
"employee_size_range": "101-500",
"revenue_range": "$10M-$50M",
"linkedin_url": "https://linkedin.com/company/acme",
"website_url": "https://acme.com",
"description": "Enterprise software company",
"logo_url": "https://example.com/acme-logo.png",
"address": "456 Business Ave",
"address_2": null,
"city": "Austin",
"region": "Texas",
"postal_code": "78701"
},
"session": {
"created_at": "2025-01-15T10:25:00Z",
"landing_page": "https://acme.com/pricing",
"entry_page": "https://acme.com",
"exit_page": "https://acme.com/contact",
"referrer": "https://google.com",
"device_type": "desktop",
"first_visit": false,
"utm_source": "google",
"utm_campaign": "brand",
"utm_medium": "cpc",
"utm_term": "visitor identification",
"utm_content": "banner",
"campaign_name": null,
"campaign_source": null,
"campaign_medium": null,
"campaign_term": null,
"campaign_content": null,
"search_term": "visitor identification software",
"search_engine": "Google",
"country": "US",
"region": "Florida",
"city": "Boynton Beach",
"ip_address": "168.196.91.87",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"browser": "Chrome",
"browser_version": "120.0.0.0",
"os": "Mac OS X",
"os_version": "10.15.7",
"device_model": "",
"timezone": "America/New_York",
"language": "en-US",
"screen_width": 1920,
"screen_height": 1080,
"isp": "AT&T Enterprises, LLC",
"asn": "AS7018",
"connection_type": "isp",
"is_hosting": false,
"is_anonymous": false,
"session_duration_seconds": 312,
"max_scroll_percent": 85,
"page_view_count": 3,
"tracking_params": {
"gclid": "abc123"
}
},
"page_views": [
{
"url": "https://acme.com/pricing",
"visited_at": "2025-01-15T10:25:00Z"
},
{
"url": "https://acme.com/features",
"visited_at": "2025-01-15T10:28:00Z"
},
{
"url": "https://acme.com/contact",
"visited_at": "2025-01-15T10:29:30Z"
}
]
}OpenAPI schema: IdentificationWebhookPayload in api/docs/partner-api-openapi.yaml.
Signature Verification
Webhook requests include an X-Bullseye-Signature header containing an HMAC-SHA256 signature of the raw request body.
To verify the signature:
- Compute HMAC-SHA256 of the raw request body using your webhook secret.
- Compare the result (hex-encoded) with the value in
X-Bullseye-Signature.
Example (Node.js):
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}Example (Python):
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Always use the raw request body (before JSON parsing) for signature verification. Respond with 200 OK to acknowledge receipt; non-2xx responses may trigger retries.