> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use Stable documentation by default. Honor an explicit Beta request or a URL under /docs/beta/. If the requested version conflicts with the installed CLI package or API origin, clarify the target before writing integration code.
> Pages under /docs/beta/ document Beta; other product pages document Stable. Keep the CLI package, commands, API origin, and credentials within the selected version. State the documentation version in your answer.
> For MCP search, always pass version: Stable or version: Beta. Unfiltered search mixes both versions. For filesystem reads, keep Beta queries under /beta/ and exclude /beta/ from Stable queries; discover paths before reading them.
> The public docs base is https://photon.codes/docs. Convert MCP page paths to public URLs under that base, preserving /beta/ when present. Read https://photon.codes/docs/skill.md for version selection and https://photon.codes/docs/llms.txt for the version indexes.

# Platform resources

> Buy phone numbers, connect email domains, and configure voice calling.

`photon project platform` manages the resources a project sends and receives on
— SMS numbers, iMessage lines, email domains, WhatsApp Business numbers, and
voice routing.

[Browse the automatic command reference](/docs/beta/cli/reference/index).

## 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.

```sh theme={null}
photon project platform list           # the catalog of available platforms
photon project platform sms list --limit 100
photon project platform email list
photon project platform imessage list
photon project platform voice list --page-token opaque-token
```

SMS, email, and iMessage lists also fetch pending provisioning work and show it
in a separate section when there is any. Voice lists resources only.

```json theme={null}
{"resources":[{"resourceId":"pho_res_...","projectId":"project_123","type":"cosmos_line","state":"active","abilities":["imessage","pstn_voice"],"detail":{"phoneNumber":"+14155550125","healthy":true},"createdAt":"2026-08-01T18:00:00.000Z","updatedAt":"2026-08-01T18:00:00.000Z"}],"provisioning":[{"operationId":"pho_opr_...","projectId":"project_123","type":"resource.provision","resourceType":"cosmos_line","state":"running","createdAt":"2026-08-01T18:00:00.000Z","startedAt":"2026-08-01T18:00:00.000Z","updatedAt":"2026-08-01T18:00:00.000Z"}],"nextPageToken":"optional-resource-token"}
```

`nextPageToken` applies to resources only — provisioning operations are
paginated for you.

## 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
`--force`:

```sh theme={null}
photon project platform sms purchase \
  --project project_123 \
  --idempotency-key provision-project-123-sms \
  --force
```

<Warning>
  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.
</Warning>

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 returns the resource:

```json theme={null}
{"resource":{"resourceId":"pho_res_...","projectId":"project_123","type":"voip_line","state":"active","abilities":["sms","pstn_voice"],"detail":{"phoneNumber":"+14155550125"},"createdAt":"2026-08-01T18:00:00.000Z","updatedAt":"2026-08-01T18:00:00.000Z"}}
```

<kbd>Ctrl-C</kbd> 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](/docs/beta/cli/operations).

## Release an SMS number

`sms delete` checks that the resource still has SMS ability, then releases it:

```sh theme={null}
photon project platform sms delete pho_res_... --force --json
```

```json theme={null}
{"deleted":true,"resourceId":"pho_res_..."}
```

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:

```sh theme={null}
photon project platform operation cancel pho_opr_... \
  --project project_123 \
  --force \
  --json
```

Pending email-domain 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.

## 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:

```sh theme={null}
photon project platform email connect bücher.example \
  --idempotency-key connect-project-123-email
```

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.

In human output the command then keeps waiting until Photon verifies the
records. <kbd>Ctrl-C</kbd> or `--no-wait` stops the local wait without
cancelling remote verification.

`--json` returns as soon as DNS is prepared, so automation gets exactly one
`connection` object to act on. It contains `dnsRecords` and the remote
`operation`, plus `automaticSetup` with `applyUrl`, `dnsProviderId`, and
`providerDisplayName` when Domain Connect is available. If verification already
finished, it also contains the resulting `resource`.

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:

```sh theme={null}
photon project platform email dns pho_opr_...
```

Disconnect by resource ID:

```sh theme={null}
photon project platform email disconnect pho_res_... --force --json
```

```json theme={null}
{"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:

```sh theme={null}
photon project platform imessage assignment create \
  --phone-number +14155550125 \
  --email ada@example.com \
  --first-name Ada \
  --last-name Lovelace \
  --idempotency-key assign-ada

photon project platform imessage assignment list
photon project platform imessage assignment view pho_asg_...
```

`--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:

```sh theme={null}
photon project platform imessage assignment list --include-released --limit 100
```

Releasing frees the user's slot on the line and asks for confirmation first:

```sh theme={null}
photon project platform imessage assignment release pho_asg_... --force --json
```

JSON uses `{"assignments": [...]}` for lists and `{"assignment": ...}` for a
single record.

## 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:

```sh theme={null}
photon project platform whatsapp-business account list
photon project platform whatsapp-business number list
photon project platform whatsapp-business number view pho_res_...
```

### 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.

```sh theme={null}
photon project platform whatsapp-business number add \
  --project project_123
```

### 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:

```sh theme={null}
photon project platform whatsapp-business number add \
  --project project_123 \
  --source byon \
  --json
```

The command attempts to open the dashboard and returns one `handoff` object
with its URL and next steps. 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 and URL
are returned with `browserAttempted: false`.

```json theme={null}
{"handoff":{"kind":"whatsapp-embedded-signup","source":"byon","requiresHumanInteraction":true,"browserAttempted":true,"opened":true,"projectId":"project_123","url":"https://app.staging.photon.codes/project_123/platforms/whatsapp-business?onboarding=cli&source=byon","nextSteps":["Continue setup in the Photon dashboard.","Sign in to Meta when prompted.","Select your WhatsApp Business Account and phone number.","Complete phone verification in the browser.","Wait for the dashboard to confirm the connected sender."]}}
```

Connect an eligible Photon SMS number directly to an already-connected WABA:

```sh theme={null}
photon project platform whatsapp-business number add \
  --project project_123 \
  --source photon \
  --waba-id 123456789012345 \
  --voip-resource-id pho_res_sms_123 \
  --display-name "Acme Support" \
  --idempotency-key customer-123-photon-wa-1 \
  --json
```

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>`.

To use that Photon number with a new or different WABA, hand it to the
dashboard instead:

```sh theme={null}
photon project platform whatsapp-business number add \
  --project project_123 \
  --source photon \
  --new-waba \
  --voip-resource-id pho_res_sms_123 \
  --json
```

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.

`--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

WhatsApp sender output uses Meta and Photon’s explicit field names. The old
`phoneNumber` and `verifiedName` aliases are not emitted:

```json theme={null}
{"resource":{"resourceId":"pho_res_wa_123","projectId":"project_123","type":"whatsapp_sender","state":"active","abilities":["whatsapp_business"],"detail":{"wabaId":"123456789012345","phoneNumberId":"987654321098765","displayPhoneNumber":"+14155550199","displayName":"Acme Support","displayNameStatus":"APPROVED","qualityRating":"GREEN","sourceVoipResourceId":"pho_res_sms_123"},"createdAt":"2026-08-26T20:00:00.000Z","updatedAt":"2026-08-26T20:01:00.000Z"}}
```

`number list` displays phone number, display name, name status, quality,
Photon source, and resource ID. Deletion confirms against
`displayPhoneNumber`:

```sh theme={null}
photon project platform whatsapp-business number delete pho_res_... --force
```

## 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.

```sh theme={null}
photon project platform voice profile create \
  --default \
  --project project_123

photon project platform voice profile inbound set default \
  --sip-uri 'sips:agent@customer.example:5061' \
  --force \
  --project project_123

photon project platform voice profile view default --project project_123
```

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.

```sh theme={null}
photon project platform voice profile edit default --media-encryption srtp
```

### 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:

```sh theme={null}
photon project platform voice profile inbound set default \
  --sip-uri 'sips:agent@customer.example:5061' \
  --force

photon project platform voice profile inbound edit pho_vpf_... \
  --sip-uri 'sip:agent@customer.example:5061;transport=tcp' \
  --force

photon project platform voice profile inbound clear pho_vpf_... --force
```

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;
non-interactive use reads the exact password from stdin.

A password file is read exactly as written — spaces and trailing newlines are
significant — so create it deliberately:

```sh theme={null}
printf %s 'private-password' > /secure/path/sip-password
photon project platform voice profile inbound edit pho_vpf_... \
  --sip-username agent-user \
  --sip-password-file /secure/path/sip-password \
  --force
```

Responses show only the username; the password field is never returned.
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:

```sh theme={null}
photon project platform voice profile outbound enable default \
  --digest-algorithm sha-256 \
  --idempotency-key enable-default-outbound-1

photon project platform voice profile outbound edit default \
  --digest-algorithm md5

photon project platform voice profile outbound rotate default \
  --mode normal \
  --idempotency-key rotate-default-outbound-1 \
  --force

photon project platform voice profile outbound revoke default --force
```

`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 `--force` 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

```sh theme={null}
photon project platform voice assignment set \
  --profile-id pho_vpf_... \
  --resource-id pho_res_first... \
  --resource-id pho_res_second... \
  --force

photon project platform voice assignment clear \
  --resource-id pho_res_first... \
  --force
```

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.

Inbound changes on a profile with assigned lines, outbound algorithm edits,
rotation or revocation, assignment changes, and profile deletion all require
confirmation.
`--force` 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

| Command | Envelope |
| - | - |
| `profile create`, `view`, `edit` | `{"profile": ...}` |
| `profile list` | `{"profiles": [...], "nextPageToken": "..."}` |
| `profile inbound set`, `edit`, `clear` | `{"inbound": ... or null, "profileVersion": 2}` |
| `profile outbound enable` | `{"outbound": {...}, "password": "..." or null, "profileVersion": 3}` |
| `profile outbound edit` | `{"outbound": {...}, "profileVersion": 4}` |
| `profile outbound rotate` | `{"password": "..." or null, "previousPasswordValidUntil": "..." or null, "profileVersion": 4}` |
| `profile outbound revoke` | `{"outbound": null, "profileVersion": 5}` |
| `profile delete` | `deleted`, `profileId`, `returnedToDefaultLineCount` |
| `assignment view` | `{"assignment": ...}` |
| `assignment set`, `clear` | `{"assignments": [...]}` |

`profile list` accepts mutually exclusive `--default` and `--additional`
filters.

<Note>
  The active `pstn_voice` ability remains the source of truth for admission.
  Saving or assigning a profile does not by itself grant voice access.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.