System Landscape
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
Server modules
Data model
Outbound Send Sequence
- A workflow, the sidebar card, or the contact-creation webhook hits a Chert ingress route.
- The route reconstructs the full request URL and verifies the HubSpot Signature v3 header against the timestamp and the raw body.
- The route loads the tenant by HubSpot account ID. Inactive tenants short-circuit with
unknown or inactive tenant. - The route checks the idempotency key in
hubspot_send_events. A prior row returns its stored response verbatim. - The route resolves a Chert lead. Sidebar sends always auto-create; workflow sends auto-create only when the per-account flag is on.
- 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.
- 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.
- The route writes the outcome to
hubspot_send_eventsand returns the response shape required by the caller.
Inbound Reply Sequence
- A recipient replies. Chert’s internal reply pipeline resolves the lead and stamps the inbound message on its conversation log.
- The reply-notification cron picks up the new inbound and fans out to email, Slack, the customer webhook (if configured), and the HubSpot push.
- The HubSpot push loads the tenant and confirms the lead carries a
hubspot_tenant_idandhubspot_contact_id. Without both, the push is a no-op. - The push obtains a valid access token, refreshing if the cached token is within 60 seconds of expiry.
- The push POSTs a Communication engagement to
/crm/v3/objects/communicationswithhs_communication_channel_type = SMS. - 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. - On success, the per-tenant
inbound_consecutive_failurescounter resets andinbound_last_success_atis stamped. On failure, the counter increments. Five consecutive failures trip the breaker and silence the push until the next successful send through the tenant.
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.
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
Idempotency
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.

