Partner ApiWebhooks

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/webhooks

Response:

{
  "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/identification

Body:

{
  "url": "https://example.com/webhooks/identify",
  "secret": "whsec_your_webhook_secret",
  "enabled": true
}
FieldTypeRequiredDescription
urlstringNoWebhook endpoint URL
secretstringNoSecret for HMAC signature verification
enabledbooleanNoEnable 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/billing

Body:

{
  "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/test

Body:

{
  "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/logs

Query Parameters:

ParameterTypeDescription
pageintegerPage number
pageSizeintegerItems per page
typestringFilter by identification or billing
statusstringFilter by delivery status
organizationIdstringFilter 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:

FieldDescription
ispNetwork / ISP name
asne.g. AS7018
connection_typeisp, hosting, business, education, etc.
is_hostingDatacenter / hosting / cloud egress (always sent; false = residential or non-hosting)
is_anonymousVPN, proxy, Tor, relay (always sent; false = non-anonymized)
country, region, cityGeo 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:

  1. Compute HMAC-SHA256 of the raw request body using your webhook secret.
  2. 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.