Skip to main content
Component map, call graph, and synchronous-versus-asynchronous behavior of the integration.

System Landscape

System landscape A connected HubSpot account issues outbound HTTPS to the Chert messaging service from three places — the sidebar UI extension on a contact record, the workflow custom action, and the contact.creation webhook. Chert dispatches the send through its delivery infrastructure and posts replies back into HubSpot as Communication engagements on the contact timeline. There is no shared infrastructure between HubSpot accounts. Every request is verified against the per-account signing secret before any work runs.

Components

Server routes

RouteMethodSurfaceAuth
/api/hubspot/oauth/installGETBrowser entrySigned state cookie
/api/hubspot/oauth/callbackGETOAuth redirect targetSigned state cookie + code exchange
/api/hubspot/uninstallPOSTapp.uninstalled webhookHubSpot Signature v3
/api/hubspot/webhookPOSTcontact.creation webhookHubSpot Signature v3
/api/hubspot/sendPOSTWorkflow custom action callbackHubSpot Signature v3
/api/hubspot/widget/contextGETSidebar card loadHubSpot Signature v3
/api/hubspot/widget/sendPOSTSidebar card sendHubSpot Signature v3
/api/hubspot/widget/campaign-statusGETSidebar card campaign tabHubSpot Signature v3

Server modules

ModuleResponsibility
OAuthBuild the install URL, exchange the authorization code, refresh the access token with a 60-second leeway.
TenantsLookup of per-account install state by HubSpot account ID.
VerifyHubSpot Signature v3 validation with a 5-minute timestamp skew bound.
Contact resolveMatch a HubSpot contact to a Chert lead by normalized phone. Auto-create on workflow paths when the account opts in.
Push inboundCreate a Communication engagement on the contact when a reply arrives. Per-account circuit breaker after five consecutive failures.

Data model

TablePurpose
hubspot_tenantsOne row per connected HubSpot account. Holds OAuth tokens, the bound Chert project, the phone line override, auto-create and auto-send flags, and the inbound circuit-breaker counter.
hubspot_send_eventsAppend-only delivery log keyed by tenant and idempotency key. Powers dedup, retry-safe responses, and the admin dashboard.
leads, convo, campaigns, phone_linesExisting Chert tables. The integration is a thin adapter on top.

Outbound Send Sequence

Send sequence
  1. A workflow, the sidebar card, or the contact-creation webhook hits a Chert ingress route.
  2. The route reconstructs the full request URL and verifies the HubSpot Signature v3 header against the timestamp and the raw body.
  3. The route loads the tenant by HubSpot account ID. Inactive tenants short-circuit with unknown or inactive tenant.
  4. The route checks the idempotency key in hubspot_send_events. A prior row returns its stored response verbatim.
  5. The route resolves a Chert lead. Sidebar sends always auto-create; workflow sends auto-create only when the per-account flag is on.
  6. The route renders the message. The sidebar passes the operator’s text through unchanged. The workflow action passes a token-rendered string from HubSpot’s workflow editor. The contact-creation webhook renders the project’s CRM template against the new contact’s name and company.
  7. The route hands off to Chert’s internal send pipeline, which selects a phone line, enforces the 10-minute minimum gap, deduplicates within a 6-hour rolling window, and dispatches through Chert’s delivery infrastructure.
  8. The route writes the outcome to hubspot_send_events and returns the response shape required by the caller.
End-to-end latency in the synchronous path is dominated by Chert’s internal pipeline; typical observed latency is 200 to 600 ms.

Inbound Reply Sequence

Reply sequence
  1. A recipient replies. Chert’s internal reply pipeline resolves the lead and stamps the inbound message on its conversation log.
  2. The reply-notification cron picks up the new inbound and fans out to email, Slack, the customer webhook (if configured), and the HubSpot push.
  3. The HubSpot push loads the tenant and confirms the lead carries a hubspot_tenant_id and hubspot_contact_id. Without both, the push is a no-op.
  4. The push obtains a valid access token, refreshing if the cached token is within 60 seconds of expiry.
  5. The push POSTs a Communication engagement to /crm/v3/objects/communications with hs_communication_channel_type = SMS.
  6. The push associates the new Communication to the contact via /crm/v4/objects/communications/{id}/associations/default/contacts/{contactId}. The default association resolves the correct association type ID server-side.
  7. On success, the per-tenant inbound_consecutive_failures counter resets and inbound_last_success_at is stamped. On failure, the counter increments. Five consecutive failures trip the breaker and silence the push until the next successful send through the tenant.
The reply path is fire-and-forget from the cron’s perspective — a dead HubSpot account never blocks operator email or Slack notifications.

OAuth Token Refresh

HubSpot access tokens expire after roughly 30 minutes. Chert never refreshes ahead of time on a schedule; instead, every code path that calls HubSpot acquires a token through the same helper.
StepDetail
1Read the cached access token and expiry from the tenant row.
2If expiry is more than 60 seconds in the future, return the cached token.
3Otherwise, exchange the refresh token at HubSpot’s OAuth token endpoint.
4Persist the new access token, refresh token, and expiry in hubspot_tenants. HubSpot rotates the refresh token on every refresh; storing the new value is required for subsequent calls to succeed.
5Return the new access token.
A 401 response from HubSpot triggers a single one-shot refresh and retry, in case the cached token expired between read and call.

Synchronous versus Asynchronous Boundaries

ActionWhere it runs
Sidebar card load (/widget/context)Synchronous. Returns within one HTTP roundtrip.
Sidebar card send (/widget/send)Synchronous up to the dispatch handoff.
Workflow custom action (/send)Synchronous. HubSpot waits for the response and reads outputFields.
Contact creation webhook (/webhook)Synchronous up to dispatch. Always returns 200 to HubSpot to avoid retries on per-event errors.
Reply push to HubSpotAsynchronous. Triggered by the reply-notify cron, never blocks operator notifications.
OAuth token refreshLazy. Triggered on the first call that finds the cached token within 60 seconds of expiry.

Idempotency

PathIdempotency keyWindow
Workflow custom actionHubSpot’s callbackIdForever — same callbackId always returns the prior response.
Sidebar card sendwidget:{tenantId}:{contactId}:{secondBucket}One second. Collapses double-clicks; allows operator retries the next second.
Contact creation webhookwebhook:contact_creation:{objectId}Forever — same contact creation event never re-fires.
Beneath these per-route keys, Chert’s internal send pipeline applies its own 6-hour rolling dedup on (phone_line, recipient, message).

See Also

  • Configuration — settings exposed in the Chert console.
  • Security — signature verification, token storage, scope rationale.
  • Limits — rate limits, daily caps, retry semantics.