Webhooks

There are two independent webhook systems, and it helps to keep them straight:

  • Inbound — Stripe calls the app so HubSpot stays current in real time.
  • Outbound — the app calls your servers so you can react to syncs.

Inbound: Stripe → app #

Once Stripe is connected, the app receives Stripe events and applies each change to HubSpot as it happens — the real-time layer described in Running syncs. There are two endpoints, depending on how you connected Stripe:

Mode Endpoint Org resolution Signing secret
Stripe Connect POST /api/webhooks/stripe from event.account platform secret
Direct key POST /api/webhooks/stripe/:connectionId the connection that connection’s own secret

You don’t normally configure these by hand — connecting Stripe wires them up.

Signature verification #

Every inbound request is verified against the relevant Stripe signing secret using the raw request body. A request that fails verification is rejected with 400 and never touches HubSpot.

Deduplication #

Events are deduplicated by their Stripe event id. The first delivery wins; any duplicate redelivery is recorded and skipped, so reprocessing is safe.

Deletions #

Deletion events apply the per-object When deleted in Stripe policy. With the default Keep policy, the synced HubSpot record and its mapping are left untouched. If you set the policy to Archive, the record is archived; if you set it to Mark, it’s tombstoned in place. For example, how customer.deleted treats the synced Company/Contact — and price.deleted the synced Product — follows whichever policy you’ve chosen.

Suspended organizations still record inbound events for audit, but no HubSpot writes are performed until the organization is reactivated.

Outbound: app → your servers #

Register your own HTTPS endpoints and the app will POST a signed JSON event each time something happens. Manage endpoints on the Webhooks screen or via the API.

POST /v1/webhook-endpoints
Authorization: Bearer sk_test_...
Content-Type: application/json

{ "url": "https://example.com/hooks", "events": ["record.synced", "sync.completed"] }

List endpoints with GET /v1/webhook-endpoints and remove one with DELETE /v1/webhook-endpoints/:id. Use ["*"] to subscribe to every event type.

Event types #

Event When Payload data
record.synced a single record was synced object, stripeId, hubspotId, eventType
sync.completed a backfill or reconcile run finished the run summary

Each delivery body has this envelope:

{
  "id": "whd_...",
  "type": "record.synced",
  "created": "2026-06-25T12:00:00.000Z",
  "data": { "object": "customer", "stripeId": "cus_123", "hubspotId": "...", "eventType": "customer.updated" }
}

Verifying the signature #

Every delivery is signed with HMAC-SHA256 over the timestamped raw body. Two headers accompany the request:

Header Value
X-Webhook-Signature t=<unix_ts>,v1=<hmac_hex>
X-Webhook-Event the event type, e.g. record.synced

To verify, recompute the HMAC of "<t>.<raw_body>" using your endpoint’s secret and compare it to the v1 value with a constant-time comparison:

parse t and v1 from X-Webhook-Signature
expected = hex( HMAC_SHA256( endpoint_secret, t + "." + raw_request_body ) )
valid = constant_time_equals(expected, v1)

Always verify against the raw body bytes, before any JSON parsing.

Retries and the delivery log #

Deliveries are made by a durable worker with a 10-second timeout. A non-2xx response (or a network error) is retried with exponential backoff, up to 5 attempts, after which the delivery is marked failed. Each attempt records the HTTP status and the last error.

Inspect outbound history — status, attempts, and response codes — with:

GET /v1/webhook-deliveries
Authorization: Bearer sk_test_...