Carriers, MessagingApp, Nooks and JustCall

Carrier registry

All carrier settings live in one place, EXTERNAL_PROVIDER_CONFIGS in lib/constants.ts:

export interface ExternalProviderConfig {
  label: string          // shown in the UI
  source: string         // MessagingApp `source` value on purchase/release
  trunkSid: string       // SIP trunk identifier
  terminationUrl: string // Retell SIP termination URL used on import
}

EXTERNAL_PROVIDER_CONFIGS = {
  twilio: { label: 'Twilio', source: 'twilio', trunkSid: NEXT_TWILIO_TRUNK_SID, terminationUrl: NEXT_TWILIO_TERMINATION_URL },
  plivo:  { label: 'Plivo',  source: 'plivo',  trunkSid: NEXT_PLIVO_TRUNK_SID,  terminationUrl: NEXT_PLIVO_TERMINATION_URL },
}

EXTERNAL_PROVIDER_KEYS orders them for dropdowns; DEFAULT_EXTERNAL_PROVIDER is twilio. Individual exports (TWILIO_TRUNK_SID, PLIVO_TERMINATION_URL, …) exist for backward compatibility.

Adding a carrier is a single new entry — PhoneNumberCallingService, MessagingAppPhoneService, the purchase route and the Phone Numbers page all read the registry dynamically.

NEXT_TWILIO_TRUNK_SID=
NEXT_TWILIO_TERMINATION_URL=
NEXT_PLIVO_TRUNK_SID=
NEXT_PLIVO_TERMINATION_URL=   # defaults to 14928144285976266.zt.plivo.com

MessagingApp

An internal Payability microservice that owns carrier accounts. The application never calls Twilio or Plivo directly — it asks MessagingApp to buy and release numbers, then imports them into Retell over SIP.

Concern File

Base service with retry helper

lib/phone-service/base.ts

MessagingApp adapter

lib/phone-service/messaging-app.ts

Types

lib/phone-service/types.ts

NEXT_MESSAGING_APP_API_URL=
NEXT_MESSAGING_APP_API_KEY=
PHONE_NUMBER_DEFAULT_COUNTRY=US

MessagingAppPhoneService extends BasePhoneNumberService:

Method Purpose

purchasePhoneNumber(…​)

Buy a number for a state or area code from the given provider

releasePhoneNumber(phoneNumber, externalProvider)

Return it to the carrier

getPhoneNumberInfo(phoneNumber)

Carrier metadata, falling back to deriving it from the number itself

Both write paths go through withRetry, because a transient failure while buying or releasing a number would otherwise leak carrier spend.

`getPhoneNumberInfo’s fallback to number-derived data matters for the dialer: state extraction must work even when the metadata service is unavailable, or the selection algorithm has nothing to match on.

Acquisition flow

  1. POST /api/phone-numbers with a state or area code and a provider.

  2. MessagingApp buys the number and returns its phoneNumberSid.

  3. RetellClient.importNumber(phoneNumber, terminationUrl, outboundAgentId) imports it over the provider’s SIP trunk.

  4. The PhoneNumbers row is written with both phoneNumberSid and retellPhoneNumberId.

Release is the reverse: unregister at Retell, release at MessagingApp, then mark isReleased with releasedAt. The row is kept so historical calls keep a resolvable phoneNumberId.

Automatic purchasing during dialing is currently disabled — see Analysis, purchase and utility workflows.

Nooks

Nooks is the dialer human reps use. Only inbound webhooks are involved; the application does not call the Nooks API for call data.

NOOKS_WEBHOOK_SECRET=   # optional; enables Bearer / X-Nooks-Signature verification
NOOKS_API_KEY=          # present in the environment; not used by the webhook path

Calls arrive at POST /api/webhooks/nooks as call.logged and are stored with channelType = "nooks_call", campaignId = null and targetId = null. Attribution is by SalesRep.nooksUserId with an email fallback. Two disposition-gated flows follow: AI analysis, and a Crm.DealUpdate closed-lost publish. See Nooks events.

FollowUpTask.nooksTaskId and nooksSequenceStateId exist for tasks and sequence enrollments mirrored into Nooks, and SalesRep.nooksUserId is what makes that assignment possible.

Five9

POST /api/webhooks/five9 accepts a payload, stores it as a WebhookEvent, and returns. There is no handler and no dispatch.

The route writes with source = "Nooks" and eventType = "five_9", generating a random eventId and providerCallId. Its rows are therefore not idempotent on replay and share the Nooks source with genuine Nooks events. Filter on eventType when querying.

JustCall (legacy)

JustCall predates the AI dialer and survives in three places:

  • Profile.justcallUserId, isAvailable, activeCalls, maxConcurrentCalls — the human agent availability model, and GET|PATCH /api/agents.

  • Call.justCallId on historical rows.

  • The handle_justcall_call_initiated() database trigger on trg_webhookevent_call_initiated, which watches for JustCall call.initiated rows in WebhookEvent, extracts the JustCall user id, and marks the matching profile unavailable while incrementing its active-call count.

JustCallEventTypeEnum in lib/enums.ts enumerates the event names (justcall_call.initiated, .answered, .incoming, .completed, .updated).

Availability tracking happens entirely in the trigger, with no application handler. New work should not extend this path — current rep identity lives on SalesRep.