Skip to main content
Every command accepts the global --json flag. With it, stdout holds exactly one JSON object: the result, or the error when the command fails. Progress, browser instructions, warnings, and suggested next commands go to stderr as text, so a result never has a next field.
Without --json, stdout also holds only the answer. A list with nothing in it prints its message (for example No projects found.) on stderr, a list with more pages ends with a More results: line on stderr that names the command for the next page, and a declined confirmation prints Cancelled. on stderr and exits 1. Only --json prints JSON. Without it the output is text, at a terminal or not: off a terminal the text drops color and spinners but says the same thing. Piping the output doesn’t switch to JSON, and no environment variable or configuration setting chooses the format. Every --help ends with a line that tells agents and scripts to add --json. The format never changes what a command does. A command that waits, such as project platform email connect or project platform telegram connect, waits with or without --json; only --wait and --no-wait change that. With --json, what it prints before it waits goes to stderr, so stdout holds only the final JSON object. A spinner shows on stderr only when stdout and stderr are both terminals, CI isn’t on, TERM isn’t dumb, and there’s no --json.

Envelopes

Most commands print the API’s response as Photon sends it, as described below. The documents the CLI defines are in the table further down. Some put the API’s object under a name of their own, such as project, organization or operation, and others add keys beside the API’s response, such as provisioning. Paginated collections carry the API’s token for the next page under the API’s name for it: nextPageToken when more records remain, nextCursor for invoices and listMetadata.after for OAuth connections, both null on the last page. A list that keeps the API’s envelope, as described below, and whose --limit takes more than one page holds every page’s items, the last page’s nextPageToken when more records remain, and the last page’s other top-level fields. Of the documents the CLI defines, the ones that say where the CLI found or saved a selection, or where the sign-in comes from, add source. When there’s no API body to print, because Photon sends none or the command makes no API call, the document names what changed with a boolean such as deleted, revoked, removed or cleared. project list, count, create, view, edit and delete, account view and edit, the project agent-profile, project api-key, project webhook, org oauth-client, org member, org invitation, account avatar, account oauth and account service-key commands, org leave, org billing payment-method status, and the project platform commands that read one response (list, operation cancel, email dns, imessage view, imessage assignment create, list, view and release, telegram view, and whatsapp-business number list and number view), the ones that start an operation and wait for it (sms purchase and delete, imessage purchase, email connect and disconnect, telegram connect and disconnect, and whatsapp-business number add --waba-id and number delete), the project platform voice commands except profile delete, assignment set and assignment clear, and project billing invoice list, billing operation view and billing plan purchase, print the API’s response as Photon sends it instead: a list keeps the API’s envelope ({"projects": [...], "nextPageToken": "..."}, {"apiKeys": [...]}, {"destinations": [...], "nextPageToken": "..."}, {"oauthClients": [...]}, {"serviceKeys": [...]}, {"authorizedApplications": [...], "listMetadata": {...}}, {"scopes": [...]}, {"platforms": [...]}, {"assignments": [...], "nextPageToken": "..."}, {"resources": [...], "nextPageToken": "..."}, {"profiles": [...], "nextPageToken": "..."}, {"members": [...]}, {"invitations": [...]}, {"invoices": [...], "nextCursor": "..."}), a single item is the bare object, a deletion, release, removal or revocation prints the API’s response with no deleted, left, released, removed or revoked boolean, and a field the API adds shows up in the JSON. org oauth-client update reads the client before it changes it and prints the response to the change, project delete reads the project, to ask for its name, and prints the response to the deletion, and org member edit, org member remove and org leave read the organization’s members (and org leave your account) to find the membership’s version, and print the response to the change. voice profile edit and the voice profile inbound and outbound commands read the profile for its version, and print the response to the change. billing operation view and billing plan purchase wait for the billing operation, and print the first answer Photon gives that isn’t pending: for a purchase, the purchase’s own response when it’s already final, or else the operation read while waiting. A purchase whose operation failed prints the error document instead, with BILLING_OPERATION_REJECTED. operation view prints a failed operation like any other and exits 0; its status is failed. The project platform commands that start an operation wait for it and print the last operation read, {"operation": {...}, "resource": {...}}, with the number, line, domain, bot or WhatsApp sender that a purchase, connect or number add sets up in resource. A purchase whose last operation read has no resource reads the operation once more and prints that read. sms delete, email disconnect, telegram disconnect and whatsapp-business number delete read the resource to check it (that the number can send SMS, that it’s an email domain or a Telegram bot, or that it’s an active WhatsApp Business sender), then release it with DELETE /v1/projects/{id}/platforms/resources/{resourceId}. With --no-wait, while the setup runs, email connect and telegram connect print Photon’s answer to the request that started it, which holds the DNS instructions or the setup link in operation.detail. email connect prints the operation read that holds the records instead when that answer doesn’t hold them yet, and telegram connect prints its read of the setup when it continues one already in progress. An answer to a request, like the answer to a release that has already ended, is the operation itself rather than an operation read, so the command prints it under operation. It’s the one place the CLI shapes these documents, so the command keeps the operation read’s {operation, resource} shape on every path. telegram connect prints no resumed field when it continues a setup already in progress: no Photon response carries it, and the rule is to print the response as sent. Only its text says it’s continuing, as kubectl apply -o json prints the object and says created or configured only in text. project agent-profile avatar upload prints the profile Photon returns when it commits the upload, and account avatar upload the account. Thirty-five commands print a document the CLI defines. Photon answers the two revocations and voice profile delete with no body, so they name what they revoked or deleted, voice assignment set and clear change one line or several, so they list one assignment for each --resource-id, as the table below describes, billing overview puts the project’s billing next to its effective terms, billing subscription cancel and resume put the billing operation of the plan change next to what it means for the subscription, or say that nothing changed, authorize builds an authorization request of its own, project current, project create --link and link (and their deprecated old names, project create --use and the hidden project use) put the API’s project next to where the CLI found it or saved it, org show and org use put the API’s organization next to where the CLI found it or saved it (path is there for source: "folder" only), org list adds the ID of the organization the CLI would use, activeOrganizationId, to the API’s response, org clear and unlink (and the hidden project clear, its deprecated old name) make no API call, payment-method setup puts the API’s checkout next to whether the browser opened it, the email, imessage, sms and telegram list commands put the API’s resource page next to the provisioning operations a second read finds, whatsapp-business account list lists the project’s one account, or none, so a project without one prints an empty list and exits 0, as gh pr list --json and kubectl get -o json do, and whatsapp-business number add with --source byon or --new-waba hands off to the dashboard, which no API response describes. The sign-in commands describe the CLI’s own sign-in: login and whoami name the account it signs in as, auth status says where the sign-in comes from and checks it with Photon when it can, and logout makes no API call. update asks the package registry, or GitHub’s releases for a standalone binary, instead of Photon’s API, and the operation commands read the local journal: operation replay defines fourteen more. When it replays a project delete, a project webhook delete or an iMessage assignment release that already went through, Photon answers 404 and sends nothing to print, so the replay’s result names what is gone: {"deleted": true, "projectId": "..."} for the project, {"deleted": true, "destinationId": "..."} for the destination and {"assignmentId": "...", "released": true} for the assignment. The same goes for an SMS delete, a WhatsApp Business number delete, an email disconnect or a Telegram disconnect that Photon answers with 404, for the number, sender, domain or bot, its release or the release’s operation, when the number, sender, domain or bot is gone (or the number, sender or domain is retired): {"deleted": true, "resourceId": "..."} for the number or sender and {"disconnected": true, "resourceId": "..."} for the domain or bot. A replayed SMS delete or WhatsApp Business number delete that has no release operation to read, because the number or sender was already retired, prints it as Photon sends it: {"resource": {...}}. A replayed project create --link can’t save the folder’s file, because the journal doesn’t record the folder, so its result is {"project": {...}} with the API’s project, and it prints the photon link step that saves the file (on stderr with --json). When an account service-key revoke went through but the CLI stopped before it finished printing the result, the replay doesn’t revoke the key again, and the journal doesn’t keep Photon’s response, so its result names the key: {"revoked": true, "serviceKeyId": "..."}. The journal records the plan change of a billing subscription cancel or resume as the plan purchase it is, so its replay’s result is what billing plan purchase prints: the billing operation as Photon sends it, which the command itself prints under operation, without subscription. A replayed email connect or telegram connect doesn’t wait for Photon to verify the records or for the bot to connect. Its result is Photon’s answer as the replay got it, with the setup under operation. While the setup is still running, that’s what the command prints with --no-wait: the DNS records or the setup link, under operation. When Photon sends the resource beside the setup, as it does for an email domain whose setup has finished, it’s under resource.

Errors

Without --json, an error is text on stderr. With --json, it’s the one JSON object on stdout, in place of the result, and the exit code is the same as without --json. The one exception is a command that fails after its result is already on stdout, or while writing it there: the error object goes to stderr, so stdout never holds two objects.
Each step in next has a description, plus a run with the full command or a url to open when there is one, and person: true when only a person can do it, such as adding a payment card:
For an API error, next holds the fix the API sends in the problem’s remediation. The CLI doesn’t write steps of its own for API errors. For invalid input (ARGUMENT_VALIDATION), next holds the steps written for that error. When a command finds invalid input and the error has no steps of its own, it gets one step instead: the command with --help, which lists its valid options. An unknown command, an unknown option, a missing argument or an ID in the wrong format is rejected before the command runs, and gets one step without a run: run the command with --help for its valid options. A --waba-id that isn’t digits is the exception: when it’s the only input rejected, its error gives the command that lists the project’s connected WhatsApp Business Account. A full API key given to project api-key revoke isn’t rejected before the command runs: the command takes it, and its error gives the command to run with the key’s ID. The terminal shows the same fields as text: the message with the code and reason, any issues, the steps under Next, then the request ID and the docs link. Each step shows its description, then its command or link:
An unknown command or long option also gets one line in text naming the closest name at the same level, when one is within two edits. The CLI never runs it, and the JSON error has no suggestion:
In a terminal, problem appears only with --debug. JSON output includes problem whenever Photon sent one, and --debug adds a debug field. An error with no step ends with a general hint chosen by retryable, the HTTP status and whether the error has a detail. A retryable error that’s safe to repeat gets none.
retryable and replayable answer different questions and can legitimately disagree. A retryable transport error on a non-replayable mutation means “try the command again”; a non-retryable error on a replayable mutation means “use photon operation replay”.

When stdout fails

When the CLI can’t write its output to stdout, as with a full disk, it prints one line on stderr and exits 1. With --json, the line is the error object:
When the reader has closed the pipe, the command exits 1 without a message, and so does a command whose stderr can’t be written either. A command that keeps its result in the operation journal until stdout takes it, such as a new API key, reports a closed pipe too, since running it again would make a second key: its error names the replay that prints the result again.

Prompts and confirmation

The CLI asks a question only when stdin, stdout and stderr are all terminals, CI isn’t on, and there’s no --json, --no-input or PHOTON_NO_INPUT. Questions go to stderr. When it can’t ask, the command fails before it changes anything, names the option that supplies the answer, and says why it couldn’t ask. A command that costs money or can’t be undone asks first. --yes, or -y, confirms without being asked, and --yes=false is the same as leaving it out. At a terminal the command asks [y/N]; deleting a project means typing its name. Answering no changes nothing, prints Cancelled. on stderr and exits 1. Without a terminal and without --yes, the command changes nothing and fails with CONFIRMATION_REQUIRED and exit code 2. The message says what would have happened, and next is the same command with --yes:
--force is only for overriding a safety check: login --force replaces a healthy session, and logout --force removes the local files of a credential the CLI can’t read. A command reads stdin only when you ask it to: login --service-key, or an option given -, such as --token-file -. Piped input is never read otherwise; a command that needs a value it wasn’t given fails and names the option.

Exit codes

Exit code 2 is what a script gets when it leaves out --yes on a command that confirms. It’s a usage error, not an approval: nothing was changed. No exit code means “a person must act”: that’s a step in next with person: true. Whether trying again can help is retryable, not an exit code. A signal ends a command with 128 plus the signal’s number: 130 for Ctrl-C (SIGINT), 143 for SIGTERM and 129 for SIGHUP. Each winds the command down the same way: it stops, prints its interrupted error (COMMAND_INTERRUPTED, or a command’s own, such as LOGIN_INTERRUPTED), and leaves the operation journal as Ctrl-C leaves it. A second signal ends the process at once with 128 plus that signal’s number. A GitHub Actions cancel sends SIGINT, then SIGTERM if the step is still running 7.5 seconds later, so a command that hasn’t finished winding down by then ends in 143.

Help

--help prints a command’s help, and so does photon help before the command. A group run without one of its commands prints its help too:
With --json, a group run on its own prints the same reference as --help --json (see Beta commands), and photon --json prints the CLI’s name and version.

Stability

From 3.0.0, the 3.x promise covers only what the CLI defines itself: command names, flags, exit codes, error codes, and the JSON the CLI defines (errors, login, whoami, update and workflow commands). Output that comes from the API follows the API: the golden tests on SDK update pull requests catch a change, and the release notes name it. A workflow command is one that makes more than one call, or reads local state such as org list’s active organization. When a workflow command prints an API response as Photon sends it, as project delete and org member edit do, what’s fixed is which response it prints, and the response’s fields follow the API. For a command that prints an API response, the golden tests type the response against Photon’s API SDK, so the pull request that updates the SDK fails when it removes or changes a field those tests use. The JSON the CLI defines is the error object, the documents in the table under Envelopes and, from the prose there, the {"operation": {...}, "resource": {...}} and {"operation": {...}} documents of the project platform commands that start an operation, the --help --json reference and photon --json. The debug trace isn’t part of it: its shape may change between releases. What’s fixed doesn’t change or go away in a 3.x release, but a 3.x release may add fields, commands, flags, error codes and enum values. So a script should ignore a field it doesn’t know and expect an enum value it hasn’t seen. A beta command can still change itself.

Beta commands

Commands that call API operations Photon hasn’t finished documenting are labelled beta. Their help description ends in (beta), in the command’s own help and in its parent’s command list:
--help --json prints a command’s reference as one JSON object on stdout, with "beta": true on beta commands:
Beta means the command itself may change in a 3.x release: its name, its flags, or whether it exists. A command loses the label once every operation it calls is published in @photon-ai/api with a description, and each label is reviewed every 90 days.

Debug traces

Global --debug buffers sanitized HTTP exchanges and emits one ordered trace to stderr when the command finishes:
A successful --json command keeps its single result object on stdout and writes a separate {"debug":{"http":...}} object to stderr. A failed JSON command stays one error object, with the exchanges and sanitized cause chain under error.debug. Commands that make no HTTP requests emit no trace. Each exchange records the method, sanitized endpoint, query-parameter names, safe headers, JSON bodies when safe, status, request ID, and duration. Long traces keep the first three and latest 22 exchanges and report how many were dropped in between.
Review debug output before sharing it. Photon redacts authorization headers, cookies, API-key headers, query values, tokens, device and invitation codes, payment capabilities, presigned upload paths and form data, tax identifiers, phone numbers, OTP and PIN values, registration feedback, and personal fields — but debug payloads are diagnostic and their shape may change between releases.

Text from the API

Text output writes every control and format character in a string from the API as visible text, so a name or message can’t move the cursor, change colors, break a line or reorder what follows. Control characters print in caret notation (\u001b[31m prints as ^[[31m, a line break as ^J), and format characters such as a right-to-left override print as \u{202e}. Joiners are format characters too, so an emoji sequence such as 👩‍💻 prints as 👩\u{200d}💻. JSON keeps the string as the API sent it: the encoder escapes U+0000 to U+001F, quotes and backslashes, and writes every other character as it is.

Color

Human output uses compact tables and labeled detail views. ANSI styling is enabled only for terminal streams, and text labels and symbols always carry the same meaning without color. Styling is disabled for JSON, for redirected output, for TERM=dumb, for a non-empty NO_COLOR or NO_COLORS, and for --no-color. FORCE_COLOR=1 enables it for a non-terminal stream unless --no-color or --json disables it. An explicit --color styles a terminal even when NO_COLOR, NO_COLORS, NODE_DISABLE_COLORS or FORCE_COLOR=0 is set, unless TERM is dumb.

Environment variables

PHOTON_NO_INPUT, PHOTON_NO_UPDATE_NOTIFIER and CI are on when set to anything except empty, 0 or false.