Phone numbers

PhoneNumbers is the workspace’s pool of outbound (dial-from) numbers. Do not confuse it with TargetPhoneNumbers, the registry of dial-to numbers — see Registries.

Number state

Field Default Meaning

isActive

true

Available for selection

isDeleted

false

Soft-deleted; excluded from selection

isReleased

false

Returned to the carrier; releasedAt records when

isDefault

false

Marks a default number for the workspace

smsEnabled

false

Eligible as the from number for outbound SMS

provider

external

external, retell_twilio or retell_telnyx

state

null

US state code used for geographic matching

maxAllowedCallsPerDay

null

Daily cap; null means unlimited

dailyCallCount

0

Reset nightly by the Inngest function phoneNumberDailyResetCron

totalCallCount

0

Lifetime usage, never reset

lastUsedAt

null

Drives the release optimizer

phoneNumberSid

null

MessagingApp identifier

retellPhoneNumberId

null

Retell identifier after import

costPerSecond, monthlyCost

null

Cost tracking

phoneNumber is globally unique, so a carrier number belongs to exactly one workspace.

Selection algorithm

PhoneNumberSelector.selectPhoneNumberForTarget(targetPhone, campaignId, subscriptionId, targetId) is state-first: matching the callee’s state matters more than matching their exact area code, because a familiar state code lifts pickup rates while an exact area-code match is rarely available.

flowchart TD
  Start["selectPhoneNumberForTarget"] --> ExtractState["Extract state from target number"]
  ExtractState -->|failed| Fallback
  ExtractState --> ExtractArea["Extract area code (+1XXX)"]
  ExtractArea -->|failed| Fallback
  ExtractArea --> A["Step A: numbers in the same state\n(any area code)"]
  A --> FilterA["Filter: under daily cap\nAND used < 1 time for this target"]
  FilterA -->|match| PickA["Random pick — isFallback: false"]
  FilterA -->|none| B["Step B: numbers with the same area code"]
  B --> FilterB["Same filter"]
  FilterB -->|match| PickB["Random pick — isFallback: false"]
  FilterB -->|none| Fallback["getFallbackPhoneNumber(campaignId)"]
  Fallback --> Any["Any available number, random\nisFallback: true"]
  Any -->|pool empty| Pref["Campaign preferedPhone"]

Eligibility filter

Applied identically in steps A and B:

  1. Daily cap — skip when maxAllowedCallsPerDay is set and dailyCallCount >= maxAllowedCallsPerDay.

  2. Per-target reuse — countCallsFromSourceToTarget(numberId, targetPhone) must be below MAX_USES_PER_SOURCE_PER_TARGET, which is 1. A given source number therefore calls a given target once, ever. Repeated calls from the same number to a number that did not answer look like harassment to carrier spam filters and to the callee.

  3. Random pick among survivors, spreading load across the pool.

Fallback

getFallbackPhoneNumber(campaignId) picks a random number from the whole available pool with isFallback: true. If the pool is empty it returns the campaign’s preferedPhone, logging a warning if that is also unset.

Phone-number purchasing is currently manual. The selector no longer requests an automatic purchase and never skips a call for lack of a local number — it always falls back to any available number. CallSkipReason.NO_PHONE_NUMBER_AVAILABLE and the skipForPurchase branch remain in the code for when automatic purchasing is re-enabled, and the body of the purchasePhoneNumber Inngest function is commented out.

Purchase and release

PhoneNumberCallingService and lib/phone-service/ handle carrier lifecycle:

  • purchasePhoneNumber() buys a number through MessagingApp.

  • registerWithRetell() imports it into Retell using the provider’s SIP trunk and termination URL.

  • releasePhoneNumberComplete(id, number, retellPhoneNumberId) removes it from Retell and returns it to the carrier.

Carrier settings live in one registry, EXTERNAL_PROVIDER_CONFIGS in lib/constants.ts:

EXTERNAL_PROVIDER_CONFIGS = {
  twilio: { label: 'Twilio', source: 'twilio', trunkSid: …, terminationUrl: … },
  plivo:  { label: 'Plivo',  source: 'plivo',  trunkSid: …, terminationUrl: … },
}

EXTERNAL_PROVIDER_KEYS orders them for UI dropdowns; DEFAULT_EXTERNAL_PROVIDER is twilio. Adding a carrier is a single new entry — the calling service, MessagingAppPhoneService, the purchase route and the Phone Numbers page all read from the registry.

Purchase gating is controlled by two Configs keys (CONFIG_KEYS in lib/constants.ts):

Key Effect

purchase_new_phone_numbers

Master switch for automatic purchasing

daily_phone_number_purchase_limit

Cap on purchases per day

Automatic purchasing requires both to be enabled.

Scheduled maintenance

Job Schedule Effect

phoneNumberDailyResetCron (Inngest)

Daily 00:00 UTC

resetDailyCounts() zeroes dailyCallCount on every number

phoneNumberOptimizerCron (Inngest)

Monday 11:15 UTC

findReleasableNumbers(3, 10), then releasePhoneNumberComplete per number unused for 3+ days with fewer than 10 lifetime calls

The optimizer’s parameters are narrower than its defaults (30 days, 10 calls) suggest — it is invoked with 3 days. Each release is its own Inngest step and passes the number’s provider, so Retell-managed numbers are deleted from Retell only, and external numbers are also returned to the carrier.

SMS

A number with smsEnabled = true can be selected as the from address for outbound SMS. CallRepository.findAvailableSmsPhoneNumber(subscriptionId) picks one regardless of provider, and RetellClient.createSmsChat() starts the conversation with the fixed agent RETELL_SMS_AGENT_ID (agent_dc036c59f7365da0f463b16e72). Operators toggle the flag from the SMS icon on the Phone Numbers page. See the send_sms MCP tool.

Blocked numbers

BlockedPhoneNumbers is a per-workspace deny list consulted by the dialer’s DNC guard. It is separate from TargetPhoneNumbers.isBlocked: the registry flag blocks a known contact endpoint, while this table blocks a raw number that may never have been a target.

Reporting

GET /api/reports/phone-number-pickup-rate compares pickup rates per number, filterable by state and provider — the input to decisions about which numbers to keep, rest or release. See Reporting and activity.