Webhooks
Webhooks deliver real-time event notifications when visitors are identified. Configure a webhook endpoint in the Bullseye dashboard, and Bullseye sends an HTTP POST to your URL with visitor, company, and session data. Use webhooks to integrate with internal tools, custom CRMs, or any system that accepts HTTP callbacks.
Plan Requirement
Webhooks are available on Scale plans and above. If you are on Starter, Growth, or Free Trial, upgrade to Scale to enable webhooks.
Configuration
Webhooks are configured per organization in the Bullseye dashboard:
- Navigate to Settings > Integrations > Webhooks
- Click Add Webhook
- Enter your endpoint URL (must be HTTPS)
- Optionally set a secret for HMAC-SHA256 signature verification
- Select the events you want to receive (e.g.,
visitor.identified,visitor.phone_revealed) - Save the webhook
Your endpoint must respond with a 2xx status code within 30 seconds. Non-2xx responses or timeouts trigger retries.
Event Types
| Event | Description |
|---|---|
visitor.identified | Fired when a visitor is identified on your website |
visitor.phone_revealed | Fired when a phone number is unlocked for a visitor (manual or auto enrichment). Same payload shape as visitor.identified with visitor.phone populated. Initial identify events often have an empty phone - listen for this event or poll the visitor profile. |
Payload Structure
Each webhook delivery includes a JSON payload. Fields are included in every delivery but may be empty strings, null, or omitted when data is unavailable.
Top-Level Fields
| Field | Type | Description |
|---|---|---|
event_type | string | Event type (e.g., visitor.identified) |
timestamp | string | ISO 8601 timestamp when the event occurred |
organization_id | string | Your Bullseye organization ID (UUID) |
session_id | string | Unique identifier for the visitor session (UUID) |
workspace | string | Organization name |
visitor | object | Identified visitor profile data |
company | object | Company data associated with the visitor |
session | object | Session metadata (attribution, device, geo, engagement) |
page_views | array | Pages visited during this session (url, visited_at) |
Visitor Object
| Field | Description |
|---|---|
id | Bullseye visitor UUID |
email | Primary email (work email preferred) |
personal_email | Personal email, if different |
first_name, last_name, job_title | Name and role |
phone, location, photo_url | Contact and profile |
linkedin_url, facebook_url, twitter_url | Social profiles |
address, address_2, city, state, postal_code | Mailing address |
gender, age_range, income_range, net_worth | Demographics (when available) |
homeowner, married, children | Demographic booleans (nullable) |
icp | Whether the visitor matches any ICP |
icp_statuses | Map of ICP slug → match status |
persona_match | Whether the visitor matches persona filters |
created_at, last_seen_at | First identified and most recent identification timestamps |
Company Object
| Field | Description |
|---|---|
id | Bullseye company UUID |
name, domain, industry | Company identity |
size | Employee count (string) |
employee_size_range, revenue_range | Firmographic ranges |
linkedin_url, website_url, description, logo_url | Company profile |
address, address_2, city, region, postal_code | Company address |
Session Object
| Field | Description |
|---|---|
created_at | When the session started |
landing_page, entry_page, exit_page | Page navigation |
referrer, device_type, first_visit | Visit context |
utm_source, utm_campaign, utm_medium, utm_term, utm_content | UTM parameters |
campaign_name, campaign_source, campaign_medium, campaign_term, campaign_content | Campaign tracking |
search_term, search_engine | Search attribution |
country, region, city | Geo from reverse-IP lookup (ipinfo) when available; may be empty on older sessions |
ip_address | Visitor IP (omitted when IP tracking is disabled) |
user_agent, browser, browser_version, os, os_version, device_model | Device/browser |
timezone, language, screen_width, screen_height | Client signals (timezone may also come from the browser or ipinfo) |
isp | ISP / network name from reverse-IP (ipinfo as.name) |
asn | Autonomous System Number (e.g. AS7018) |
connection_type | Network type from ipinfo as.type: isp, hosting, business, education, etc. |
is_hosting | true for datacenter / hosting / cloud egress IPs (always present; false is meaningful) |
is_anonymous | true for VPN, proxy, Tor, or relay traffic (always present; false is meaningful) |
session_duration_seconds, max_scroll_percent, page_view_count | Engagement metrics |
tracking_params | Ad platform params (gclid, fbclid, li_fat_id, etc.) |
IP Intelligence
Session IP fields are populated during /track via ipinfo.io reverse lookup. They appear on organization webhooks, Partner identification webhooks, and the Main API visitor profile (GET visitor profile → sessions[]).
| Field | Webhook (session.*) | Main API (SessionProfile) | Segmentation |
|---|---|---|---|
| Country / region / city | country, region, city | country, region, city | - |
| ISP / ASN | isp, asn | isp, asn | - |
| Connection type | connection_type | connectionType | - |
| Datacenter flag | is_hosting | isHosting | is_hosting |
| VPN/proxy flag | is_anonymous | isAnonymous | is_anonymous |
| Visitor IP | ip_address (if IP tracking enabled) | ipAddress | - |
| Postal code, lat/long | Not exposed | Not exposed | Internal cache only |
Plan behavior: With ipinfo Core (or higher), geo, ISP/ASN, connection_type, is_hosting, and is_anonymous are populated. On the free Lite tier, only country, isp, and asn are filled; is_hosting / is_anonymous stay false and city/region may be empty until the plan is upgraded. Fields are best-effort: lookup failures leave strings empty and booleans at false.
Partner API OpenAPI: IdentificationWebhookPayload and segmentation condition enums in api/docs/partner-api-openapi.yaml.
Example Payload (full)
{
"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"
}
]
}Organization webhooks and Partner identification webhooks use the same IdentificationWebhookPayload schema (snake_case). Partner docs: Partner API webhooks. OpenAPI: api/docs/partner-api-openapi.yaml.
Signature Verification
To verify that webhook deliveries come from Bullseye, use the X-Bullseye-Signature header. The header contains an HMAC-SHA256 signature of the raw request body using your webhook secret.
Verification Steps
- Read the raw request body (before any JSON parsing)
- Compute
HMAC-SHA256(raw_body, your_webhook_secret) - Encode the result as hexadecimal
- Compare with the value in
X-Bullseye-Signature
If the values match, the webhook is authentic. Reject requests where the signature does not match.
Example (Pseudocode)
signature = request.headers["X-Bullseye-Signature"]
expected = hmac_sha256_hex(request.raw_body, webhook_secret)
if signature != expected:
return 401 # UnauthorizedRetry Behavior
If your endpoint returns a non-2xx status code or does not respond within 30 seconds, Bullseye retries the delivery:
- Retry schedule - Retries occur at increasing intervals (e.g., 1 minute, 5 minutes, 30 minutes)
- Maximum retries - Up to 3 retries per delivery
- Idempotency - The same payload may be delivered more than once; design your endpoint to handle duplicate events (e.g., using
session_idas a deduplication key)
Best Practices
- Use HTTPS - Webhook endpoints must use HTTPS
- Verify the signature - Always validate
X-Bullseye-Signaturebefore processing - Respond quickly - Return 200 immediately and process the payload asynchronously if needed
- Handle duplicates - Use
session_idor a combination of fields to deduplicate events - Log failures - Monitor for 4xx/5xx responses and fix endpoint issues promptly