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.
What you can stream
Section titled “What you can stream”Each subscription targets one data type:
| Data type | Change types | Fires when |
|---|---|---|
event | a tracked event name, or * | the tracker or an integration records an event |
contact | created, updated, deleted, or * | a unified contact is created, changed, or removed |
company | created, 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.
Subscribing
Section titled “Subscribing”In the dashboard, open Events → Streaming → New stream, then:
- Pick the data type (events, contacts, or companies).
- Pick the change type — for events, the event name or
*; for contacts/companies,created/updated/deleted/*. - Set the destination URL. Must be
https://and publicly reachable — internal IP ranges are rejected. - Optionally add filters (see below).
- Optionally add custom headers (encrypted at rest) and a signing secret.
- Save.
Delivery format
Section titled “Delivery format”Pathbound sends a POST with a JSON body and these headers:
Content-Type: application/jsonUser-Agent: Pathbound-Webhooks/1.0X-Pathbound-Signature: sha256=<hex> (only if a signing secret is set)Plus any custom headers you configured.
Event payload
Section titled “Event payload”{ "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"}Contact / company payload
Section titled “Contact / company payload”{ "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"]}objectis the full record, identical in shape toGET /v1/contacts/:id/GET /v1/companies/:id. Fordeletedit is the last-known state, ornull.sourceis the origin of the change: an integration name (hubspot,apollo, …), ormanual/form/api/system.changed_fieldslists the fields that changed when available — it can be empty (creates, deletes, and updates where a per-field diff isn’t computed). Treatobjectas the source of truth and reconcile byupdated_at.
Filters
Section titled “Filters”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).
Verifying the signature
Section titled “Verifying the signature”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.
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));}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.
Delivery semantics
Section titled “Delivery semantics”- Timeout. Respond
2xxwithin 30 seconds. - Retries. A non-
2xx, timeout, or network error is retried up to 3 times with backoff (1s, 10s, 60s). A4xxis 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
objectas the latest state and reconcile byupdated_atrather 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.
Best practices
Section titled “Best practices”- 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
idas a dedup key. - Inspect deliveries in the dashboard — response code, latency, body, and attempt count per subscription.