Skip to content
Pathbound DOCS

Streaming

Streaming is Pathbound’s third way to get your data, alongside the REST API and the MCP server:

  • REST API — pull a snapshot when you ask for it.
  • MCP — let an AI agent read your data as tools.
  • Streaming — Pathbound pushes changes to your endpoint as they happen.

Use it to keep an external system in sync: notify your service when a high-intent visitor identifies, push form submissions to your warehouse, or mirror contacts and companies into your own database as integrations enrich them.

Each subscription targets one data type:

Data typeChange typesFires when
eventa tracked event name, or *the tracker or an integration records an event
contactcreated, updated, deleted, or *a unified contact is created, changed, or removed
companycreated, updated, deleted, or *a unified company is created, changed, or removed

Contact and company changes capture every source — manual edits, the REST API, form submissions, and (the common case) integration syncs that create or enrich records pulled from HubSpot, Apollo, and the rest.

In the dashboard, open Events → Streaming → New stream, then:

  1. Pick the data type (events, contacts, or companies).
  2. Pick the change type — for events, the event name or *; for contacts/companies, created / updated / deleted / *.
  3. Set the destination URL. Must be https:// and publicly reachable — internal IP ranges are rejected.
  4. Optionally add filters (see below).
  5. Optionally add custom headers (encrypted at rest) and a signing secret.
  6. Save.

Pathbound sends a POST with a JSON body and these headers:

Content-Type: application/json
User-Agent: Pathbound-Webhooks/1.0
X-Pathbound-Signature: sha256=<hex> (only if a signing secret is set)

Plus any custom headers you configured.

{
"id": "evt_xyz",
"event": "page_view",
"timestamp": "2026-06-08T12:00:00.000Z",
"data": { "title": "Pricing", "path": "/pricing" },
"url": "https://example.com/pricing",
"domain": "example.com",
"visitor_id": "vis_123",
"contact_id": "ct_abc"
}
{
"id": "ec_abc123",
"type": "contact.updated",
"resource": "contact",
"action": "updated",
"timestamp": "2026-06-08T12:00:00.000Z",
"source": "hubspot",
"object": {
"contact_id": "ct_abc",
"properties": { "email": "[email protected]", "jobtitle": "VP Engineering", "lifecyclestage": "mql" }
},
"changed_fields": ["jobtitle", "lifecyclestage"]
}
  • object is the full record, identical in shape to GET /v1/contacts/:id / GET /v1/companies/:id. For deleted it is the last-known state, or null.
  • source is the origin of the change: an integration name (hubspot, apollo, …), or manual / form / api / system.
  • changed_fields lists the fields that changed when available — it can be empty (creates, deletes, and updates where a per-field diff isn’t computed). Treat object as the source of truth and reconcile by updated_at.

Events support URL filters — AND/OR substring patterns matched against the event URL (e.g. only deliver page_view events whose URL contains /pricing).

Contacts and companies support entity filters:

  • Changed-field filter — when a per-field diff is available, only fire on updates that touch one of the named fields (e.g. lifecyclestage). When no diff is available the filter fails open (the update is still delivered). Creates and deletes always fire.
  • Source filter — only fire when the change came from one of the named origins (e.g. hubspot).

Compute an HMAC-SHA256 of the raw request body (not the parsed JSON) with your signing secret, and compare it to X-Pathbound-Signature (after stripping sha256=). Use a constant-time comparison.

Node
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
if (!header?.startsWith('sha256=')) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const provided = header.slice('sha256='.length);
if (provided.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
}
Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
if not header.startswith('sha256='):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len('sha256='):])

If your framework already parsed the body, capture the raw bytes before parsing — re-stringifying won’t match byte-for-byte.

  • Timeout. Respond 2xx within 30 seconds.
  • Retries. A non-2xx, timeout, or network error is retried up to 3 times with backoff (1s, 10s, 60s). A 4xx is treated as a permanent client error and not retried.
  • Circuit breaker. After a long run of consecutive failures a subscription auto-pauses; re-enable it from the dashboard once your endpoint is healthy.
  • At-least-once. Deliveries can repeat — dedupe on id.
  • Latest-wins, not strictly ordered. Retries mean two changes to the same record can arrive out of order. Treat object as the latest state and reconcile by updated_at rather than relying on delivery order.
  • Coalescing. Many rapid changes to the same record within a short window (e.g. an integration sync touching it repeatedly) collapse into a single delivery carrying the net change.
  • Treat the destination URL as public. The signature is your only authenticator — verify it on every request.
  • Acknowledge fast, process async. Return 200 OK, then do the work in a background job.
  • Be idempotent. Use id as a dedup key.
  • Inspect deliveries in the dashboard — response code, latency, body, and attempt count per subscription.