Nooks events
Nooks is the dialer human reps use. Its call outcomes are ingested through a dedicated endpoint and a parallel dispatcher — no campaign or target rows are involved.
- Endpoint
-
POST /api/webhooks/nooks - Dispatcher
-
NooksWebhookDispatcherinlib/events/nooks-event-handler.ts - Persistence
-
lib/repositories/nooks-repository.ts
Calls are stored with channelType = "nooks_call", campaignId = null and targetId = null — they
are rep activity, not campaign activity.
Route flow
-
Optional secret check —
Authorization: BearerorX-Nooks-SignatureagainstNOOKS_WEBHOOK_SECRET. -
Upsert a
WebhookEventrow withsource = "Nooks", idempotent oneventId. -
Dispatch to the registered handler.
| Event | Handler | File |
|---|---|---|
|
|
|
Payload
{
"event": "call.logged",
"eventId": "evt_123",
"occurredAt": "2026-01-01T00:00:00Z",
"callData": {
"callId": "nooks_call_123",
"status": "ended",
"durationSeconds": 120,
"disposition": { "id": "disp_1", "name": "Connected" },
"userData": { "userId": "rep_abc", "email": "rep@co.com", "name": "Jane Rep" },
"prospectData": { "prospectId": "hs_contact_id", "phoneNumber": "+15555550100", "name": "John Prospect" },
"recordingUrl": "https://…",
"transcriptUrl": "https://…"
}
}
Rep attribution
| Nooks field | Matched against | Priority |
|---|---|---|
|
|
1st — workspace-scoped, then global |
|
|
2nd — case-insensitive |
SalesRep.nooksUserId must be populated for the primary match; email is a fallback that breaks when a
rep uses a different address in Nooks.
Post-processing
After NooksRepository.upsertCallFromCallLogged, two disposition-gated flows run in parallel.
call.logged received
│
▼
NooksRepository.upsertCallFromCallLogged()
│
├── disposition NOT in NOOKS_ANALYSIS_SKIP_DISPOSITIONS
│ └── emit nooks/call.logged → nooksCallAnalysis
│
└── disposition IN NOOKS_CLOSED_LOST_DISPOSITIONS
└── resolveHubspotContactId() → publishCrmWorkflowEvent(Crm.DealUpdate)
AI analysis
Skipped for NOOKS_ANALYSIS_SKIP_DISPOSITIONS: "No Answer", "Voicemail", "Busy" — the call never
connected, so there is nothing to grade.
Otherwise nooksCallAnalysis runs CallAnalysisService.runAndSave(callId, null), persisting a
CallAiAgentAnalysisResult with campaignId = null. There is no campaign, so the workspace’s default
analysis agent is used, and because there is no caller agent either, the system prompt is
DEFAULT_HUMAN_CALL_ANALYSIS_SYSTEM_PROMPT — which reframes the task as sales coaching: does the
logged disposition match the conversation, how were objections handled, are there compliance or tone
issues.
Closed-lost CRM update
Triggered for NOOKS_CLOSED_LOST_DISPOSITIONS: "Not Interested", "Do Not Call",
"Marketplace not supported", "No longer selling/shut down".
NooksRepository.resolveHubspotContactId resolves the contact:
-
callData.prospectData.prospectId, used directly when Nooks provides it. -
Phone registry fallback —
TargetPhoneNumbers→Contact.hubspotContactId.
Then publishes to CRM_QUEUE_URL:
{
"event_type": "Crm.DealUpdate",
"hubspot_contact_id": "<resolved>",
"deal_stage": "closedlost",
"note": "Outreach: Closed deal because of a disposition from Nooks: <disposition>. Call ID: <callId>"
}
If no HubSpot contact id can be resolved, a warning is logged and nothing is sent — an unresolvable CRM payload is worse than none.
Environment
NOOKS_WEBHOOK_SECRET= # optional; enables Bearer / X-Nooks-Signature verification
CRM_QUEUE_URL= # required for Crm.DealUpdate
AWS_REGION= # required for SQS
Adding an event type
-
Create
lib/events/handlers/nooks/<event-name>.tsextendingNooksEventHandler. -
Export it from
lib/events/handlers/nooks/index.ts. -
Call
this.register("<event.type>", HandlerClass)in theNooksWebhookDispatcherconstructor.
Five9
POST /api/webhooks/five9 stores its payload and returns. It writes with source = "Nooks" and
eventType = "five_9", generating a random eventId and providerCallId, so its rows are not
idempotent on replay and share the Nooks source with genuine Nooks events. Filter on eventType to
separate them. No dispatch occurs.