photon project platform manages the resources a project sends and receives on
— SMS numbers, iMessage lines, email domains, Telegram bots, WhatsApp Business
numbers, and voice routing.
List inventory
Each list command asks for a currently enabled ability, so one resource can appear in more than one list — a line that does both iMessage and voice shows up in both.provisioning, each as Photon sends it:
nextPageToken applies to resources only — provisioning operations are
paginated for you.
Voice lists resources only, and its JSON is Photon’s response as it sends it:
{"resources": [...], "nextPageToken": "..."}.
platform list prints Photon’s catalog of platforms as the API sends it:
Buy a number
sms purchase provisions one US local SMS number. --country US is the default;
use --area-code 415 to request a particular area code. imessage purchase
provisions one dedicated iMessage line. Both confirm first unless you pass
--yes:
resource. When that read has no resource yet, the
CLI reads the operation once more and prints that read:
photon operation replay <local-id> resumes polling without buying anything
else. See Recoverable operations.
Release an SMS number
sms delete reads the resource to check that it still has SMS ability, then
releases it with DELETE /v1/projects/{id}/platforms/resources/{resourceId},
polls the release until it ends, and prints Photon’s last operation read:
operation, as an operation read holds
it. It’s the one place the CLI shapes these documents, so the command keeps
the operation read’s {operation, resource} shape on every path. The
--no-wait answers of email connect and telegram connect print the same
way.
When operation replay gets a 404 from Photon
because the number is gone, or because the release went through and Photon no
longer has its operation, the replay’s result is
{"deleted": true, "resourceId": "pho_res_..."}. When the replay
has no release operation to read because the number was already retired, its
result is the number as Photon sends it, {"resource": {...}}.
There is no dedicated iMessage or voice release command.
Cancel provisioning in progress
platform operation cancel asks Photon to withdraw an in-progress remote
provision:
OPERATION_NOT_CANCELLABLE if the
operation is past the point of no return or finished first. Cancelling here
does not remove the local journal record.
With --json it prints the operation as Photon returns it:
Connect an email domain
email connect starts SES and DNS verification. The CLI normalizes case, a
trailing root dot, and internationalized names before sending, and rejects
schemes, paths, ports, wildcards, IP addresses, and malformed labels locally:
--no-browser to print it without opening. The manual records are always
printed as a fallback.
The command then keeps waiting until Photon verifies the records, with or
without --json. Ctrl-C or --no-wait stops the local wait
without cancelling remote verification.
With --json, the records and the waiting line go to stderr, and stdout gets
Photon’s last operation read when the command ends,
{"operation": {...}, "resource": {...}}, with the records in
operation.detail.dnsRecords and the email domain in resource. Add
--no-wait to get Photon’s answer as soon as DNS is prepared: the setup, under
operation, with the six records in operation.detail.dnsRecords and
automaticSetup with applyUrl, dnsProviderId, and providerDisplayName
next to them when Domain Connect is available. When Photon’s answer doesn’t
hold the records yet, the CLI reads the operation until it does and prints that
read, with the resource when the read holds it. With or without
--json, the CLI checks the records first and fails with
PROJECT_PROTOCOL_ERROR when they’re invalid.
Re-read the published instructions at any time with the remote operation ID —
this hits Photon directly and needs neither the original idempotency key nor
the local journal:
--json it prints Photon’s response for the operation,
{"operation": {...}, "resource": {...}}. The six records are in
operation.detail.dnsRecords, next to automaticSetup and verification
when Photon has them, and resource is the email domain once verification
has finished. With or without --json, the CLI checks the records first and
fails with PROJECT_PROTOCOL_ERROR when they’re invalid.
Disconnect by resource ID. The command reads the resource to check that it’s
an email domain, releases it with
DELETE /v1/projects/{id}/platforms/resources/{resourceId}, polls the release
until it ends, and prints Photon’s last operation read:
operation replay gets a 404 from Photon
because the domain is gone, or because the release went through and Photon no
longer has its operation, the replay’s result is
{"disconnected": true, "resourceId": "pho_res_..."}.
Connect a Telegram bot
A project sends and receives on Telegram through a bot that you own.telegram connect starts a setup and hands it over to Telegram, where you choose one of
your bots or create a new one and confirm the project:
--no-browser to print it without
opening. Open the link on any device that has Telegram; the link stays usable
until the setup finishes.
connect always adds a bot. If the project already has one, the command says
so and asks before it starts a setup. Without a terminal, pass --yes:
without it the command exits 2 with CONFIRMATION_REQUIRED and starts
nothing. Use telegram list to see the bots a project has.
The command then waits until the bot is connected and prints it, with or
without --json. Ctrl-C or --no-wait stops the local wait
without cancelling the setup. With --json, the link and the waiting line go
to stderr and stdout gets Photon’s last operation read when the command ends,
{"operation": {...}, "resource": {...}}, with the bot in resource.
With --no-wait --json, the command returns as soon as the setup is accepted,
so automation gets the setup to act on, under operation, with the link in
operation.detail.setupUrl: Photon’s answer to the request that started it,
or Photon’s read of the setup when the command continues one already in
progress. It still opens the link unless you pass --no-browser:
telegram connect continues it — it prints the same link again and says
Continuing the Telegram bot setup already in progress. — so running the
command again is also how you pick a setup back up. The JSON has no resumed
field: no Photon response carries it, and the rule is to print the response as
sent. kubectl apply -o json does the same: it prints the object, and
created or configured appears only in text. To start over, cancel the
setup and connect again:
--json it prints the resource
as Photon sends it. detail holds its name, username, and public link, plus
connectionIssue when the connection needs attention — for example after the
bot changed owner in Telegram:
DELETE /v1/projects/{id}/platforms/resources/{resourceId}, polls the release
until it ends, and prints Photon’s last operation read:
operation replay gets a 404 from Photon
because the bot is gone, or because the release went through and Photon no
longer has its operation, the replay’s result is
{"disconnected": true, "resourceId": "pho_res_..."}.
iMessage shared-line assignments
A dedicated iMessage line can be shared. An assignment binds one of your users to the line so their messages are attributed to them:--phone-number is required and must be E.164. The name and email fields are
optional and only affect how the user is presented.
assignment list shows active assignments. Add --include-released to see
released ones too:
--json, each command prints Photon’s response as the API sends it.
assignment list prints the API’s envelope,
{"assignments": [...], "nextPageToken": "..."}, with nextPageToken only
when more assignments remain. create, view and release print the
assignment itself. Its handle is the user’s phone number, and after
release its releasedAt is set:
operation replay finds that a release
already went through, Photon answers 404 and sends no assignment, so the
replay’s result is {"assignmentId": "pho_asg_...", "released": true}.
imessage view shows whether the project’s iMessage runs on a shared or a
dedicated line. With --json it prints Photon’s settings, such as
{"mode": "shared", "maxUserAssignments": 10, "dedicatedLineAvailableNow": true}.
WhatsApp Business
Each project has one connected WhatsApp Business account. Theaccount list
command returns zero or one account and includes its capacity, review status,
restrictions, and alerts. Account pagination is no longer supported.
List the connected account and active senders:
--json, account list prints the account as Photon sends it, in a
list: {"accounts": [...]}. Photon answers with the project’s one account,
or with an error when none is connected, and then the list is empty,
{"accounts": []}, and the command exits 0 with --json or without
it, as gh pr list --json prints [] and kubectl get -o json prints a
List with empty items.
number list lists active senders only: it asks Photon for them, and its JSON
is the page Photon sends, so it lists the same senders as the text:
--limit counts the senders Photon returns before the CLI leaves out the
ones that aren’t active, so a page can list fewer senders than --limit.
Human onboarding wizard
Runnumber add without --source in an interactive terminal. The wizard
first asks for one simple choice: Bring Your Own Number or Use a Photon
SMS Number.
- Bring Your Own Number opens focused dashboard onboarding and stops. Finish Meta Embedded Signup and verification in the browser; the CLI does not wait or refresh inventory.
- Use a Photon SMS Number lets you select an eligible existing number or buy a new US local SMS number. Purchased numbers remain in the project if you stop onboarding later.
- The wizard always asks which connected WABA to use, even when there is only one. Selecting a connected WABA provisions directly. Selecting Connect another WABA with Meta opens Embedded Signup while the terminal retrieves and displays the SMS OTP.
Automation and agent flows
Automation must pass--source; --json without it fails instead of opening
the human wizard.
Bring-your-own-number onboarding is a browser handoff:
--json
the guide is the command’s output, on stdout. With --json the guide goes to
stderr as text, and stdout holds one handoff object with the dashboard URL,
which an agent can pass to that person. No Photon response describes a browser
handoff, so handoff is the CLI’s own document. Embedded Signup requires human browser
interaction, and the dashboard owns verification and final connection. Use
--no-browser when an agent cannot launch a browser; the same guide is
printed, and the object has browserAttempted: false.
photon operation replay <local-operation-id>. Success prints Photon’s last operation read,
with the sender in resource, as sms purchase does:
handoff object
holds the SMS number as Photon sends it in resource, as the CLI’s own
documents hold API objects whole (billing overview, link and
payment-method setup do too):
--new-waba and --waba-id are mutually exclusive. Photon source always
requires --voip-resource-id. BYON rejects WABA, Photon-resource,
display-name, and idempotency flags. --no-browser applies only to browser
handoffs.
WhatsApp sender JSON
number view prints the sender as Photon sends it, a retired one too. Text
shows phoneNumber in E.164 form, and JSON as Photon sends it:
number list displays phone number, display name, name status, quality,
Photon source, and resource ID. Deletion confirms against
phoneNumber:
number delete reads the sender to check that it’s an active WhatsApp
Business sender, then releases it with
DELETE /v1/projects/{id}/platforms/resources/{resourceId}, polls the release
until it ends, and prints Photon’s last operation read, with operation.detail
as Photon sends it:
operation, as sms delete does.
When operation replay gets a 404 from Photon
because the sender is gone, or because the release went through and Photon no
longer has its operation, the replay’s result is
{"deleted": true, "resourceId": "pho_res_..."}. When the replay has no
release operation to read because the sender was already retired, its
result is the sender as Photon sends it, {"resource": {...}}.
Voice routing profiles
Voice inventory and voice routing answer different questions.voice list
shows resources with an active pstn_voice ability. A profile describes
where Photon should send an inbound call once that check passes.
default with profile view, profile edit, or any inbound or
outbound command.
The CLI uses /profiles/default to read that profile, then sends directional
changes through /profiles/{profileId} with the stable ID returned by the
read. The default cannot be renamed, deleted, or reassigned, and it must exist
before you can create an additional profile.
A line with no override follows the project default. Assigning an additional
profile creates an override; clearing it returns the line to the current
default. profile edit can rename an additional profile and change media
encryption on either an additional or the default profile. Inbound and
outbound delivery have dedicated commands beneath the profile. Editing requires
at least one of --name or --media-encryption; the default cannot be renamed.
SIP routes
A profile can carry one inbound SIP destination. The scheme picks the transport: baresip: normalizes to UDP, ;transport=tcp selects TCP,
;transport=udp selects UDP, and sips: selects TLS. Photon owns the
authoritative SIP syntax and public-host validation, so there are no separate
transport options. Use --media-encryption auto or --media-encryption srtp
on profile creation or editing; srtp requires encrypted media.
Default creation needs only --default; Photon assigns the immutable name
Default Profile. Additional profile creation requires --name. Both create
direction-neutral profiles, so configure delivery afterward. Use
inbound set to create or fully replace the destination, inbound edit to
change selected fields, and inbound clear to disable inbound delivery:
Endpoint credentials
Credentials are replaced as an atomic username and password pair. Passwords are never accepted as command-line options. With--sip-username and no
--sip-password-file, an interactive terminal prompts with hidden input.
Without a terminal, pass --sip-password-file, or --sip-password-file - to
read the exact password from stdin.
A password file is read exactly as written — spaces and trailing newlines are
significant — so create it deliberately. Take the password from your secret
store rather than typing it into the command, so it stays out of shell history,
and create the file readable only by you:
--debug
output and never written to the local journal. Omitting credential flags
preserves the existing write-only secret, --clear-sip-credentials clears both
fields, and --sip-username replaces the username and password together. The
CLI never sends a masked password from a previous response back to Photon.
Outbound credentials
Outbound calling supports credential lifecycle commands and password-preserving authentication edits:outbound enable creates the first credential for a profile with no active
outbound credential. It defaults to SHA-256 and always sends that explicit
choice to Photon. Select --digest-algorithm md5 only for weaker legacy
compatibility. Photon supports MD5 over the UDP, TCP, and TLS transports
returned in the outbound endpoint list. Enable output reports the active
algorithm and each endpoint’s transport, host, and port so the SIP client can
use the connection guidance returned by Photon.
outbound edit changes the active Digest algorithm while preserving the
username and password, including existing validity and rotation grace metadata.
Use it to switch algorithms on an enabled profile. MD5 is the weaker legacy
option; select it only for compatibility. Selecting SHA-256 can stop a PBX that
does not support it from authenticating. The command asks for confirmation;
use --yes to skip the prompt in automation.
The CLI resolves default to its stable profile ID and sends the current
profile’s top-level version as expectedVersion. Even a same-value edit goes
to Photon so the server can validate that version and return its authoritative
result. A stale version produces VOICE_PROFILE_VERSION_CONFLICT: read the
current profile and retry. VOICE_OUTBOUND_OPERATION_PENDING instead means
credential setup or rotation must finish first. Resume the original enable or
rotate request with its original idempotency key, or replay its recovery
record; after it completes, read the current profile and retry the edit.
Edit output contains the returned outbound connection and authentication
metadata and profileVersion, with no password or display-once secret warning.
Algorithm edits require no idempotency key, operation journal entry, or secret
acknowledgement.
outbound rotate replaces an existing credential while preserving the
active Digest algorithm, so rotation intentionally has no algorithm flag;
normal keeps the previous credential valid for one hour, while emergency
removes that grace period. outbound revoke disables outbound calling. It does
not delete the profile, inbound route, or line assignments.
Enable and rotation output password at most once. Save it from stdout
immediately: the CLI never persists it or writes it to debug output. Before the
request, the CLI privately journals the exact profile reference, selected
enablement digest algorithm, rotation mode, and expectedVersion. If the
result is unresolved, rerunning the command with the same supplied idempotency
key or using operation replay resends that exact body without rereading the
now-mutated profile. An idempotent replay returns
password: null; the mutation succeeded, but the display-once password cannot
be reproduced.
After successful output is flushed, the recovery record is removed. Do not
rerun an already-confirmed success with its old key; deliberately rotate with
the returned profileVersion and a new idempotency key to issue another
password. An omitted idempotency key is generated for the current invocation.
A supplied key must identify only that same unresolved enable or rotation
action. The CLI also retries one transport interruption within an invocation
with the same key and unchanged request body.
Assignments
default when there is no override.
Each change goes to the --resource-id given. If Photon answers the read of a
line with another line’s assignment, the command fails with
VOICE_PROTOCOL_ERROR and changes nothing. If Photon’s answer to a change
leaves out a line it changed, the command fails with VOICE_PROTOCOL_ERROR and
names the line, because the change may have been made: check it with
assignment view.
Inbound changes on a profile with assigned lines, outbound algorithm edits,
rotation or revocation, assignment changes, and profile deletion all require
confirmation.
--yes skips only that prompt. Deleting an assigned profile additionally
needs --unassign-lines, which asks Photon to atomically clear the overrides,
return those lines to the default, and delete the profile.
JSON envelopes
Every Voice command exceptprofile delete, assignment set and
assignment clear prints Photon’s response as it sends it, and a field Photon
adds shows up in the JSON. The commands that read the profile for its version
first print the response to the change. Photon answers profile delete with no
body, so it names the profile it deleted. assignment set and clear change
one line or several, so they list one assignment for each --resource-id, in
that order. For a line the command changed, it’s the assignment Photon returned
for the change, or, after a clear that changes one line, which Photon answers
with no body, the line as read after the change. A line that already uses the
profile (for clear, one that’s already unassigned) isn’t changed, and shows
as read before the change.
profile list accepts mutually exclusive --default and --additional
filters.
The active
pstn_voice ability remains the source of truth for admission.
Saving or assigning a profile does not by itself grant voice access.