Skip to main content
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.
SMS, email, iMessage, and Telegram lists also fetch pending provisioning work and show it in a separate section when there is any. Their JSON is Photon’s page of resources as it sends it, with the operations still setting up a resource of that kind under 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:
An idempotency key identifies one logical resource forever. Reusing a key resumes that same purchase rather than buying a second number. Use a fresh key when you want another resource.
The CLI sends the purchase once, then polls the resulting operation until it succeeds, fails, or is cancelled. iMessage provisioning can take hours, so there is no client-side timeout. Success prints Photon’s last operation read, with the number or line in resource. When that read has no resource yet, the CLI reads the operation once more and prints that read:
Ctrl-C stops the local wait only — the purchase keeps running on Photon. The journal keeps the remote operation ID, so 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:
When Photon answers the release as already ended, there’s nothing to poll, and the command prints that answer under 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:
Pending email-domain, Telegram bot, and dedicated iMessage provisioning can be cancelled; SMS provider orders cannot. Photon returns 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:
It submits once, then polls until Photon publishes six DNS records and prints them straight away. If your DNS provider supports Domain Connect, the CLI also prints a signed automatic-setup URL and opens it in a browser — pass --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:
With --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:
When 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:
The command prints a setup link, shows it as a QR code when it runs in a terminal, and opens it in a browser — pass --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:
The JSON holds the link as Photon sends it. Text shows the link, and the command opens it, only after the CLI checks that it’s a Telegram setup link.
Treat setupUrl as a secret. Whoever opens it can connect a bot to the project, so keep it out of logs and shared channels.
A project has one Telegram setup at a time. If one is already in progress, 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:
Inspect a connected bot by resource ID. With --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:
Disconnect by resource ID. The bot stops sending and receiving through Photon; it is not deleted from Telegram and stays yours. The command reads the resource to check that it’s a Telegram bot, releases it with DELETE /v1/projects/{id}/platforms/resources/{resourceId}, polls the release until it ends, and prints Photon’s last operation read:
When 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:
Releasing frees the user’s slot on the line and asks for confirmation first:
With --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:
When 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. The account 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:
With --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

Run number 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:
The command attempts to open the dashboard and prints a guide for the person who finishes setup: the dashboard URL and the numbered steps. Without --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.
Connect an eligible Photon SMS number directly to an already-connected WABA:
Photon owns the SMS-code request, verification, and Meta registration for this direct API path. The CLI records the accepted operation and polls it to a terminal result, so interrupted work can be resumed with photon operation replay <local-operation-id>. Success prints Photon’s last operation read, with the sender in resource, as sms purchase does:
To use that Photon number with a new or different WABA, hand it to the dashboard instead:
This command validates that the number is active, SMS-capable, belongs to the project, and is not already linked. It then opens the dashboard and exits. The dashboard owns both Embedded Signup and OTP display; use the dashboard or rerun the interactive CLI if you need guided OTP handling. No provisioning mutation or local operation record is created by this handoff. The 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:
When Photon answers the release as already ended, the command prints that answer under 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.
A project has one default profile and any number of additional profiles. Use the literal 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: bare sip: 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:
Every command reads the current profile first and sends its latest version. Route changes on a profile with assigned lines require confirmation because they take effect for those lines immediately.

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:
Photon’s responses carry the username, not the password. Passwords, SIP destinations, and profile bodies are redacted from --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

The CLI reads each current assignment before changing it and sends its version, so a concurrent change surfaces as a conflict rather than a silent overwrite — view the assignment and repeat the command. One changed line uses the single-line endpoint; 2 to 100 use the atomic batch endpoint. Duplicate IDs and batches over 100 are rejected locally, and already-correct lines are no-ops. Assignment output shows 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 except profile 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.