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 |
|---|---|---|
|
|
Available for selection |
|
|
Soft-deleted; excluded from selection |
|
|
Returned to the carrier; |
|
|
Marks a default number for the workspace |
|
|
Eligible as the |
|
|
|
|
|
US state code used for geographic matching |
|
|
Daily cap; |
|
|
Reset nightly by the Inngest function |
|
|
Lifetime usage, never reset |
|
|
Drives the release optimizer |
|
|
MessagingApp identifier |
|
|
Retell identifier after import |
|
|
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:
-
Daily cap — skip when
maxAllowedCallsPerDayis set anddailyCallCount >= maxAllowedCallsPerDay. -
Per-target reuse —
countCallsFromSourceToTarget(numberId, targetPhone)must be belowMAX_USES_PER_SOURCE_PER_TARGET, which is1. 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. -
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. |
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 |
|---|---|
|
Master switch for automatic purchasing |
|
Cap on purchases per day |
Automatic purchasing requires both to be enabled.
Scheduled maintenance
| Job | Schedule | Effect |
|---|---|---|
|
Daily 00:00 UTC |
|
|
Monday 11:15 UTC |
|
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.