CRM queue and HubSpot events
CRM writes are not made inline. The application publishes messages to an SQS queue and a downstream Payability service applies them to HubSpot. Inbound HubSpot notifications travel the same path in reverse.
Publishers
lib/plugins/sqs-client.ts:
| Function | Purpose |
|---|---|
|
Generic publish to |
|
|
|
|
|
|
|
|
Event types
event_type |
Meaning | Producer |
|---|---|---|
|
An eligible AI call finished |
Retell |
|
Move a deal stage, e.g. |
Nooks |
|
An outbound touch was made |
|
|
Custom timeline event |
Resend handlers |
|
Create a HubSpot task |
Sequence |
|
A normalised inbound HubSpot notification |
|
|
Update contact properties |
Sequence |
|
Record an opt-out |
|
|
Classify an inbound reply |
Resend |
CrmWorkflowEventTypeEnum in lib/enums.ts covers Crm.DialerCallEnded and Crm.DealUpdate;
CRM_EVENT_TYPES in lib/sequences/crm-event.ts covers the types a sequence step may select
(Crm.TaskCreate, Contact.Update).
The conditional-publish rule
Handlers publish CRM messages only when a HubSpot contact id is available. This is consistent across producers:
-
Resend handlers call
publishCrmCustomEvent/publishCrmNewActivityonly when the contact has a HubSpot id. -
The sequence
crm_taskstep skips withno_hubspot_contactwhen the contact is not in HubSpot. -
The Nooks closed-lost flow logs a warning and sends nothing when it cannot resolve an id.
The reason is that the consumer has no way to resolve an unidentified contact, so an id-less message is guaranteed to fail downstream. Failing early and visibly is better than filling the queue with undeliverable work.
FIFO ordering
messageGroupId is set to the workspace id with dashes stripped (Crm.DialerCallEnded), or to the
HubSpot contact id falling back to the workspace id (sequence crm_event steps). Messages within a
group are processed in order, so two updates to the same contact cannot be applied out of sequence.
Inbound HubSpot webhooks
POST /api/webhooks/hubspot:
-
Read the raw body.
-
Validate the HubSpot HMAC or legacy signature using
HUBSPOT_WEBHOOK_CLIENT_SECRET(lib/utils/hubspot/webhook-signature.ts). Unlike the Resend route, this one rejects when the secret is unset. -
Normalise the payload (
lib/utils/hubspot/webhook-normalize.ts). -
Publish as
Crm.HubSpotWebhookthroughpublishCrmWorkflowEvent.
The application does not act on HubSpot notifications directly; it forwards them so a single downstream service owns all CRM reconciliation.
HubSpot client
lib/plugins/hubspot/pyb.ts wraps @payability/pyb-hubspot and @hubspot/api-client for the
best-effort engagement logs (sequence email step and the send_email MCP tool).
HubSpotListsClient in the same folder extends HubSpotClient for the daily-list pre-list
find-or-create. Existing imports from lib/plugins/pyb-hubspot still work.
Related constants in lib/constants.ts: HUBSPOT_PORTAL_ID, HUBSPOT_AI_OWNER_ID,
HUBSPOT_TASK_OWNER_ID, HUBSPOT_NEWSLETTER_SUBSCRIPTION_TYPE_ID,
HUBSPOT_LIFECYCLE_STAGE_PROPERTY, CRM_UNSUBSCRIBE_LEGAL_BASIS,
CRM_UNSUBSCRIBE_LEGAL_BASIS_EXPLANATION.