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:

  1. Navigate to Settings > Integrations > Webhooks
  2. Click Add Webhook
  3. Enter your endpoint URL (must be HTTPS)
  4. Optionally set a secret for HMAC-SHA256 signature verification
  5. Select the events you want to receive (e.g., visitor.identified, visitor.phone_revealed)
  6. Save the webhook

Your endpoint must respond with a 2xx status code within 30 seconds. Non-2xx responses or timeouts trigger retries.

Event Types

EventDescription
visitor.identifiedFired when a visitor is identified on your website
visitor.phone_revealedFired 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

FieldTypeDescription
event_typestringEvent type (e.g., visitor.identified)
timestampstringISO 8601 timestamp when the event occurred
organization_idstringYour Bullseye organization ID (UUID)
session_idstringUnique identifier for the visitor session (UUID)
workspacestringOrganization name
visitorobjectIdentified visitor profile data
companyobjectCompany data associated with the visitor
sessionobjectSession metadata (attribution, device, geo, engagement)
page_viewsarrayPages visited during this session (url, visited_at)

Visitor Object

FieldDescription
idBullseye visitor UUID
emailPrimary email (work email preferred)
personal_emailPersonal email, if different
first_name, last_name, job_titleName and role
phone, location, photo_urlContact and profile
linkedin_url, facebook_url, twitter_urlSocial profiles
address, address_2, city, state, postal_codeMailing address
gender, age_range, income_range, net_worthDemographics (when available)
homeowner, married, childrenDemographic booleans (nullable)
icpWhether the visitor matches any ICP
icp_statusesMap of ICP slug → match status
persona_matchWhether the visitor matches persona filters
created_at, last_seen_atFirst identified and most recent identification timestamps

Company Object

FieldDescription
idBullseye company UUID
name, domain, industryCompany identity
sizeEmployee count (string)
employee_size_range, revenue_rangeFirmographic ranges
linkedin_url, website_url, description, logo_urlCompany profile
address, address_2, city, region, postal_codeCompany address

Session Object

FieldDescription
created_atWhen the session started
landing_page, entry_page, exit_pagePage navigation
referrer, device_type, first_visitVisit context
utm_source, utm_campaign, utm_medium, utm_term, utm_contentUTM parameters
campaign_name, campaign_source, campaign_medium, campaign_term, campaign_contentCampaign tracking
search_term, search_engineSearch attribution
country, region, cityGeo from reverse-IP lookup (ipinfo) when available; may be empty on older sessions
ip_addressVisitor IP (omitted when IP tracking is disabled)
user_agent, browser, browser_version, os, os_version, device_modelDevice/browser
timezone, language, screen_width, screen_heightClient signals (timezone may also come from the browser or ipinfo)
ispISP / network name from reverse-IP (ipinfo as.name)
asnAutonomous System Number (e.g. AS7018)
connection_typeNetwork type from ipinfo as.type: isp, hosting, business, education, etc.
is_hostingtrue for datacenter / hosting / cloud egress IPs (always present; false is meaningful)
is_anonymoustrue for VPN, proxy, Tor, or relay traffic (always present; false is meaningful)
session_duration_seconds, max_scroll_percent, page_view_countEngagement metrics
tracking_paramsAd 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[]).

FieldWebhook (session.*)Main API (SessionProfile)Segmentation
Country / region / citycountry, region, citycountry, region, city-
ISP / ASNisp, asnisp, asn-
Connection typeconnection_typeconnectionType-
Datacenter flagis_hostingisHostingis_hosting
VPN/proxy flagis_anonymousisAnonymousis_anonymous
Visitor IPip_address (if IP tracking enabled)ipAddress-
Postal code, lat/longNot exposedNot exposedInternal 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

  1. Read the raw request body (before any JSON parsing)
  2. Compute HMAC-SHA256(raw_body, your_webhook_secret)
  3. Encode the result as hexadecimal
  4. 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  # Unauthorized

Retry 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_id as a deduplication key)

Best Practices

  • Use HTTPS - Webhook endpoints must use HTTPS
  • Verify the signature - Always validate X-Bullseye-Signature before processing
  • Respond quickly - Return 200 immediately and process the payload asynchronously if needed
  • Handle duplicates - Use session_id or a combination of fields to deduplicate events
  • Log failures - Monitor for 4xx/5xx responses and fix endpoint issues promptly