Phone numbers

Buy a real phone number or bring your own, have an agent answer its calls, and have it call people with a goal.

A phone number you buy through NoClick rings one of your agents. The agent talks with the caller in a natural voice and can use its tools during the call. It can also place calls, working toward a goal you give it, and you get the transcript when it hangs up.

You can buy numbers in every country NoClick sells in: local, mobile, toll-free and national numbers, wherever each is sold. Each number has its own price, billed monthly, plus a one-time cost where the carrier charges one. Finding numbers is free, though like every call here it takes a key with the phone scope (available, coverage and places included; without it they raise noclick.PermissionDenied); buying one needs at least $5 in your account's balance, on any plan, and below it raises noclick.PaymentRequired. Where numbers aren't enabled on your account yet, they raise noclick.NotAvailable. A number you already have can answer too: see Bring your own number.

Buy a number

import noclick

[pick] = noclick.PhoneNumber.available(country="US", area_code="415", limit=1)
print(pick.number, pick.locality, pick.monthly_usd, pick.upfront_usd)

number = noclick.PhoneNumber.buy(pick, max_monthly_usd=5)

monthly_usd is what the number costs a month and upfront_usd what it costs once, when it's bought: the carrier's price for that number (a US local number is $1.00 a month and $1.00 once today). Both are charged when you buy it, and the number keeps its monthly price for as long as you hold it. max_monthly_usd is your ceiling on the month and max_upfront_usd on the one-time cost (by default, the upfront_usd the number you picked showed): above either, the purchase fails with noclick.PriceExceeded and nothing is bought.

Find one anywhere

available searches one country at a time (country, US by default) for one kind of number (type: local by default, or mobile, toll_free, national or shared_cost), narrowed by any of:

  • area_code, locality (a city) or region (a state or province);
  • contains (digits you want, letters for their keypad digits, or * for any digit), starts_with or ends_with (digits of the number after its country code);
  • features: voice (takes calls, the default) or voice_sms (calls and texts).
for country in noclick.PhoneNumber.coverage():
    print(country.country, country.types)          # where numbers are sold, and which kinds

places = noclick.PhoneNumber.places("GB")           # the cities with numbers for sale
london = noclick.PhoneNumber.available(country="GB", locality="London", contains="2024")

places lists cities, or area codes in the US and Canada, with numbers of a kind for sale.

What a country asks first

Many countries ask who holds a number before it's sold: an address, an ID, proof of address, a company registration. Each result says what its country asks in requirements: needs is none, address, documents or steps, and items lists each thing asked, with its id, kind (address, contact, textual, document or step), name, description, the carrier's example and, for a textual one that takes only some answers, those values.

  • none (the US and Canada, among others): buy it straight away.
  • address: pass the holder's name and address to buy, and NoClick files it with the carrier. Where a requirement's kind is contact (the holder's contact information, as for mobile numbers in the Netherlands and Mexico), pass a phone to reach them on, in E.164, too. The carrier checks it; if it refuses the address, nothing is bought and nothing is charged.
  • documents: upload each document first, then pass the holder (where it's asked) and requirements to buy: each textual and document requirement's answer by its id. The carrier reviews what you filed, which can take a few days, so buy returns a PhoneNumberOrder rather than the number.
  • steps: the country asks for a step the carrier takes with the holder itself (an identity check at its own link, a company it approved first). These numbers are listed, but can't be bought through NoClick yet; buy raises noclick.NotAvailable.
holder = noclick.NumberHolder(name="Dana Ruiz", street="Keizersgracht 1", city="Amsterdam",
                              postal_code="1015 CJ", country="NL", phone="+31612345670")
noclick.PhoneNumber.buy(pick, max_monthly_usd=5, address=holder)

coverage() says which kinds each country sells this way: documents_types are bought with documents, steps_types can't be bought here yet.

Numbers bought with documents

upload_document sends a file (a PDF, PNG or JPEG, up to 20 MB) to the carrier for one requirement; the carrier holds it, and NoClick keeps only its name and what it's for. Name it in requirements within 25 minutes; once a purchase has named it, it can be named again for your next number.

[pick] = noclick.PhoneNumber.available(country="IT", limit=1)
asked = {item["name"]: item for item in pick.requirements["items"]}
passport = noclick.PhoneNumber.upload_document(
    "passport.pdf", requirement=asked["Identity Document"], country="IT")
giulia = noclick.NumberHolder(name="Giulia Rossi", street="Via Roma 1", city="Milano",
                              postal_code="20121", country="IT", phone="+393471234567")
order = noclick.PhoneNumber.buy(
    pick, max_monthly_usd=5, max_upfront_usd=5, address=giulia,
    requirements={asked["Identity Document Type"]["id"]: "Passport",
                  asked["Identity Document"]["id"]: passport},
)
print(order.status)          # in_review

Nothing is charged while the order is in review. Its status follows the carrier:

  • in_review: the carrier is checking what you filed.
  • rejected: it sent the order back; reason is its words and deadline when the correction is due. resubmit files corrected answers (new uploads, a fixed address) on the same order, and it's in_review again.
  • completed: the number is yours, its first month and any one-time cost are charged now, and phone_number() returns it.
  • failed: it ended without the number (cancelled, or nobody corrected it in time). Nothing was charged.
order = noclick.PhoneNumberOrder.retrieve(order.id)
if order.status == "rejected":
    clearer = noclick.PhoneNumber.upload_document(
        "passport-full.pdf", requirement=asked["Identity Document"], country="IT")
    order.resubmit(address=giulia, requirements={asked["Identity Document Type"]["id"]: "Passport",
                                                 asked["Identity Document"]["id"]: clearer})
elif order.status == "completed":
    number = order.phone_number()

Every number takes calls the same way, wherever it is. Texting differs by country, and each result's texting says which applies: registration (a US number, which texts once your business is registered with the US carriers: see Text the caller), unavailable (texting from that country isn't available yet; its calls work) or none (the number can't text).

Name it in your code

A number can have a name your code knows it by, unique among its user's numbers (buy(..., name=...)). from_name with create_if_missing=True owns one idempotently: the first run buys a local number in area_code (any US area code when you leave it out) for at most max_monthly_usd a month and max_upfront_usd once, and every run after finds the same number.

number = noclick.PhoneNumber.from_name(
    "front-desk", create_if_missing=True, area_code="415", max_monthly_usd=5, max_upfront_usd=5,
)
number.update(name="reception")   # a new name; None removes it

A name another of the user's numbers has raises noclick.Conflict with that number's existing_id, before anything is bought, and a released number gives its name up. When no number is for sale in the area code, from_name raises noclick.NotAvailable and buys nothing. record_calls= in a definition keeps recording in step with your code, and connect= owns a number you keep with your own phone provider the same way.

Answer calls with an agent

receptionist = noclick.Agent.from_name(
    "receptionist",
    create_if_missing=True,
    instructions="You answer calls for Harbor Dental: opening hours, directions, and what to bring.",
    tools=["search"],
)
number.answer_with(receptionist)

From then on, every call to the number reaches that agent. A number rings one agent at a time; calling answer_with again moves it.

Any agent on the sdk harness can answer, with all its tools. Its connections work during a call as they do in a message: a user's agent books into that user's calendar, an account agent uses the account's connections. A caller is never handed a connect link: when a connection isn't connected yet, the agent tells the caller it can't do that right now, and the link is emailed to the connection's own person (its user, or your account's owner), at most once a day. Your policy applies to its calls too, approvals included. answer_with refuses an agent whose model connection isn't connected (noclick.NeedsConnection or noclick.CredentialDisconnected, with its connect_url); if it breaks later, callers hear "I can't take calls right now, please try again later" and the agent's person is emailed why, once a day. An agent on a coding harness can't answer, since a call waits on each turn's words and a coding harness replies later: answer_with raises noclick.NotAvailable.

Transfer a call, and know who's calling

answer_with takes two more options, a tool and a gate for the answering agent:

number.answer_with(
    receptionist,
    transfer_to=noclick.Human(email="dana@harbordental.test", phone="+14155550142"),
    callers="anyone",
)
  • transfer_to (a number in E.164 form, or a noclick.Human with a phone) gives the agent a transfer_call tool that hands the caller to that number, when the caller asks for a person or the agent can't help. The agent says it's putting them through, then the call rings the person from your number; they hear who's calling and why before they're connected. Once they pick up the call is theirs, and the agent's part of it ends. A transfer nobody picks up within about 25 seconds comes back to the agent, which is told and tells the caller. On the Free and Member plans, the call goes on being billed at your number's carrier rates while they talk: the caller's minutes, the call placed to the person, and the recording if the number records. An older plan's per-minute price covers only the agent's part, and on a connected number your provider bills these to you. Policy names the tool phone.transfer_call.
  • callers says who gets through: "anyone" (the default), "users" (only callers proven to be one of your users, as below) or "owner" (only the number's own user, or your account's owner for an account number, by the phone verified on their NoClick account). Anyone else hears that the number doesn't take their call, and the call is listed with status refused.

On a number of your account's, a caller whose number is one of your users' verified phones (user.verify_phone), with the carrier attesting the caller id (STIR/SHAKEN level A), runs as that user, in their connections, whatever callers says, and the call's user_id names them. So a line that answers anyone still gives a known customer what it gives them in your app, and a caller id alone, which anyone can fake, never does. Any other call runs as your account, and so does a call from a phone two of your users share. A user's own number runs every call as that user.

Calling answer_with again sets the number's answering afresh: an option you leave out goes back to its default.

Choose the conversation

Each caller's calls to the number are a conversation of their own with the agent. conversation= names the conversation instead, a template over {caller} (the caller's number) and {number} (this one), and it's the same conversation agent.message(..., conversation=...) reaches:

assistant = noclick.Agent.from_name("assistant")
number.answer_with(assistant, callers="owner", conversation="owner")
assistant.message("Remind me what we said on the phone.", conversation="owner")

During the call the agent reads what's already in the conversation, and at hang-up the call's transcript joins it. A call that runs as one of your users lands in that user's own conversation of that name. Calls from anyone else share one, so a fixed name such as "owner" belongs with callers="owner", and the owner's texts, and calls to NoClick's own number, and email can share it too.

Text the caller

answer_with(..., texts=True) gives the agent a send_text tool that texts the caller, and only the caller: a link, a confirmation, an address. The agent asks first ("May I text that to this number?") and sends only after a spoken yes; its tool says so. Each text is signed with your business's name (Harbor Dental: here's the link you asked for), the first one to a caller adds "Reply STOP to opt out.", and a call sends at most three. Texting anyone but the caller isn't offered.

Texting is for US numbers: a number in another country can't text through NoClick yet, and answer_with(..., texts=True) raises noclick.NotAvailable for one. Texting from a US number needs your business registered with the US carriers first, as the legal entity on its tax records. Register it once; NoClick writes the rest of what the carriers review (what you text, and how a caller agrees to it) from your business's facts, so you only supply those:

registration = noclick.TextingRegistration.create(
    legal_name="Harbor Dental LLC",
    brand_name="Harbor Dental",          # what texts are signed with
    entity_type="private_company",       # public_company, nonprofit, government, sole_proprietor
    tax_id="12-3456789",                 # the EIN; none for a sole proprietor
    industry="healthcare",
    website="https://harbordental.test",
    address={"street": "1 Ferry Building", "city": "San Francisco", "region": "CA", "postal_code": "94111"},
    contact={"name": "Dana Ruiz", "email": "dana@harbordental.test", "phone": "+14155550142"},
    privacy_url="https://harbordental.test/privacy",
    terms_url="https://harbordental.test/terms",
    max_setup_usd=40,
    max_monthly_usd=5,
)
print(registration.status, registration.setup_usd, registration.monthly_usd)

The website, privacy policy and terms must open without signing in, and the privacy policy must say phone numbers aren't shared. The contact is your business's own person, never NoClick: the registry checks that an email or address isn't reused across many businesses. Review takes days to weeks, and status follows it:

statusMeaning
pendingThe carriers' registry is checking the business
awaiting_humanWaiting on the contact: a public company's contact answers the registry's verification email within 7 days; a sole proprietor's contact gets a code by text, which verify(code) sends back
in_reviewThe business is verified; the carriers are reviewing what it texts
approvedTexting can start: answer with texts=True
rejectedRefused: status_reason says why and fields names the facts; update(**corrections) sends it again
suspendedStopped by the carriers after approval

The registry's verification email comes from The Campaign Registry, not from NoClick or your brand, and it may reach every business's contact, sometimes with an ID check, from January 2027. A phone.texting_registration.updated event tells you when the status moves. Until it's approved, answer_with(..., texts=True) raises noclick.ActionRequired naming the registration. A user's own number texts under its user's registration when there is one (with noclick.as_user(...): when your users are businesses, each registers as itself), else under the account's. One registration texts from up to 49 numbers.

number.answer_with(receptionist, texts=True)
for text in number.text_messages(direction="inbound"):
    print(text.from_number, text.text, text.keyword)

A reply to the number doesn't wake the agent. text_messages() lists what the number sent and received, and each received text arrives as a phone.text.received event. STOP (and UNSUBSCRIBE, CANCEL, END, QUIT) stops every text from your business to that person until they text START. HELP is answered with your business's name and contact. Both are answered in your business's name, and the keyword is on the text.

Texting costs what the carriers' registry charges: a one-time setup (setup_usd, $25.00: registering the business, then its review and first three months) and a monthly fee after that (monthly_usd, $2.00). Each text costs segment_usd a segment (160 characters), sent or received, plus the recipient's network fee as its own line (networks charge about $0.004 to $0.005 a segment, and change these several times a year). A registration like this sends up to about 2,000 texts a day to T-Mobile customers, shared with any other service your business texts through. delete() stops texting and the monthly fee. A number you keep with your own phone provider can't text through NoClick yet.

Record calls

number.update(record_calls=True)

From the next call, a recorded call holds both sides in one file, and the caller hears "This call is recorded." before the agent speaks, on calls it answers and calls it places. A recording is ready a minute or so after the call ends; each call's recording_url is then a signed link, valid for an hour from when you read the call. It's stored in the agent's files, where deleting it removes it for good. from_name(..., record_calls=True) keeps the setting in your code, and buy and connect take it too.

Place a call

call = number.call(
    "+14155550100",
    goal="Confirm Friday's 3pm booking, or find a new time that works.",
).wait()

print(call.status)   # completed, or not_connected
print(call.text)     # the transcript, one turn per line

call returns an operation that finishes once the call has hung up and its charge is recorded (usually at hang-up) with an noclick.PhoneCall: its transcript (turns of role, text and time), how long it lasted and what it was charged. The call goes out through the agent that answers the number, so route the number with answer_with first.

An app's production code calls and texts from a number its own code bought, within the app's budget: each call's minutes and turns and each text count toward it, and once it's used up a call isn't placed or let in. A preview's code never rings anyone. (What an app buys.)

See its calls

for call in number.calls():
    print(call.to, call.status, call.seconds, call.cost_usd)

calls() lists the calls the number answered and placed, newest first, each with its direction. direction="inbound" or "outbound" keeps one kind, since= (a timezone-aware datetime) those that started at or after it, and limit= the newest that many. A call that ran as one of your users names them in user_id, and a call the agent handed to a person says where in transfer. cost_usd is what a call was charged, as your usage shows it, once it has ended and the charge is recorded: usually at hang-up, and null until then.

from datetime import datetime, timedelta, timezone

week_ago = datetime.now(timezone.utc) - timedelta(days=7)
for call in number.calls(direction="inbound", since=week_ago, limit=50):
    print(call.from_number, call.seconds, call.recording_url, call.text)

Bring your own number

A number you already have can be answered by your agents three ways: connect it where it is, through your own phone provider account; port it to NoClick for good (invite-only beta); or forward its calls to a NoClick number. source says which a number is: bought, connected or ported.

Connect a number you keep with your phone provider

First connect the provider account, with the account's own id and secret: for the provider NoClick supports for calls today, twilio, its Account SID and Auth Token. NoClick checks every call the provider sends it against the account's signature, which only the account's own secret can do, so a restricted API key isn't enough: a connection holding one raises noclick.InvalidRequest naming connection.

provider = noclick.Connection.create(
    "phone-provider", integration="twilio", credential_type="twilio_account",
    data={"account_sid": "AC...", "auth_token": "..."},
)
for pick in noclick.PhoneNumber.connectable(provider):
    print(pick.number, pick.label, pick.routed_elsewhere)

number = noclick.PhoneNumber.connect("+14155550123", connection=provider, name="front-desk")
number.answer_with(noclick.Agent.from_name("receptionist"))

connectable lists the account's numbers (up to 1,000), each with its label, the account's own name for it; routed_elsewhere means a number's calls go somewhere today (a URL, an application or a SIP trunk), and a number already connected to NoClick names the number it's connected as (phone_number_id) and the agent answering it (agent_id). connect records the number and where its calls go, and leaves them there until answer_with routes them to an agent; disconnect() takes the number off its agent, puts its calls back where they went, and takes it off your list. Everything on this page works on a connected number: answer_with, call (placed through your provider account), calls and recording. from_name("front-desk", create_if_missing=True, connect="+14155550123", connection=provider) connects it idempotently. A number on a SIP trunk can't be connected (noclick.InvalidRequest): take it off the trunk first.

Your provider bills the number and its minutes to you directly, as it does today, so a connected number's monthly_usd is None. NoClick bills only what it adds: the agent's time on each call.

Inside as_user it's the user's number, on the user's own connection. A number that's connected already, or that another NoClick workflow answers, raises noclick.Conflict, whose existing_id names it when it's yours: one number rings one agent. Reconnecting the connection moves its numbers onto the new credential, and deleting it hands its numbers back first. If the connection stops working (its secret rotated, the account closed), callers hear that the number's provider account is no longer connected.

Port a number in

port = noclick.PhoneNumber.port_in(
    "+14155550188",
    account_holder="dana@harbordental.test",
    account_name="Harbor Dental LLC",
    service_address={"street": "1 Ferry Building", "city": "San Francisco", "region": "CA", "postal_code": "94111"},
    account_number="8812-4410-09",
    pin="4410",
    max_monthly_usd=5,
    name="front-desk",
)
print(port.status, port.sign_url)   # awaiting_human, the page Dana signs

Porting moves US local numbers from their carrier to NoClick for good: one, or several on one account (pass a list; one letter covers them). A carrier lets a number go only on a letter of authorization signed by the person authorized on the account, so port_in emails account_holder (a noclick.Human or an email) a link to a NoClick page, branded like your other pages, where they check the details, read the letter and sign; sign_url is that page, to send them yourself. Their signature is an electronic one, recorded with when, from where and what they saw.

The details must match the current carrier's records, as its bill shows them: account_name is the name on the account (often a business, not the person signing), service_address the address the service is at, account_number the account's number, billing_number its main telephone number (the first number when you leave it out), pin where the carrier has one (a mobile account's transfer PIN, from its app), and bill (a recent bill's PDF bytes) where it asks for one. requested_at asks for a date; the carrier sets the real one, usually about a week out for a simple port and three weeks or more for a complex one.

port_in returns an operation, a noclick.PortRequest:

statusMeaning
awaiting_humanWaiting for the account holder to sign (a letter unsigned after 14 days expires)
running, step submittedSigned and sent to the carrier
running, step scheduledThe carrier gave every number a date: the last moves at port_at
succeededMoved: wait() gives the PhoneNumber objects, each with source ported
failedThe carrier refused it, with its reason
canceledCanceled before the carrier committed to a date

A carrier can move a letter's numbers on different days: port.numbers gives each one's status and port_at. The numbers keep working with their carrier until they move, then ring your agent. From then on each is a NoClick number, billed monthly like a bought one at its carrier's fee (monthly_usd, $1.00 today), at most max_monthly_usd (noclick.PriceExceeded above it, and nothing is sent).

A carrier refuses a port whose details don't match its records, and many first tries are refused, most often for the service address, so check it against the bill. wait() raises noclick.InvalidRequest with the carrier's reason, its field naming what didn't match: account_name, account_holder, service_address, account_number, billing_number, pin, numbers (one isn't on that account, or can't be ported) or requested_at. retry() sends it again with the corrections, keeping the rest; a change to what the letter says (the holder, the account's name, the address, the account or billing number), or a refusal of the name it was signed with, asks the account holder to sign again, while a new pin, bill or requested_at goes straight back. A number that moved before the refusal stays yours.

try:
    numbers = port.wait()
except noclick.InvalidRequest as err:
    print(err.field, err.message)   # account_number, The account number doesn't match.
    port = port.retry(account_number="8812-4410-07")

port.cancel() stops it until the carrier has committed to a date (scheduled); after that it moves. noclick.PortRequest.list(status="live") lists the ports in flight, and takes the other operation filters.

A number you connected through your own phone provider is disconnected before it's ported in (noclick.InvalidRequest naming numbers otherwise); once ported, it's a new NoClick number to answer with an agent. For a number kept with Twilio, the name on the account is "Twilio, Inc.", its account number is the last 8 characters of your Account SID, and the PIN comes from Twilio's support (porting@twilio.com); Twilio also emails your account's owner to confirm the move, and the number's texting registration there doesn't come along.

Move a number away

details = number.transfer_out()
print(details.account_name, details.account_number, details.pin, details.service_address)

To move a NoClick number to another carrier, give that carrier what transfer_out() returns: the account the number is on, its service address and a transfer PIN for this number. For 30 days (until expires_at) another carrier may take it; a request outside that window, or quoting another PIN, is refused, so nobody takes your number without you. Each request arrives as a phone.port_out.requested event saying whether it goes ahead, and once the number moves it leaves your numbers, its billing stops, and phone.port_out.completed says so.

Forward a line to a NoClick number

Forwarding needs no API. At your current carrier or phone system, forward the line (always, or when it's busy or unanswered) to a NoClick number that answer_with routes to an agent. The caller hears that agent, as on any call to the number, after the extra ring or two the forward takes.

A forwarded call reaches NoClick from the forwarding line's carrier, which usually doesn't carry the caller's verified caller id (STIR/SHAKEN level A) over. So on an account number it runs as your account, never as one of your users, even when the caller's phone is a user's verified one, and callers="users" or "owner" turns it away: leave a forwarded line on "anyone". To have callers recognised as your users, connect or port the number instead.

Release a number

number.release()

The number stops ringing and stops being billed. A connected number isn't NoClick's to give back: release() raises noclick.Conflict, so disconnect() it.