How it works
Each mutation command is one logical request:- Omit
--idempotency-keyand the CLI generates one. - Supply a stable key to identify the same logical request across invocations.
- Re-running with the same key and the same input continues the unresolved operation and returns ordinary success output, not a replay envelope.
- Re-running with the same key and different input fails before any request
with
IDEMPOTENCY_KEY_REUSED. - Use
operation replay <id>when the original arguments or private input are inconvenient or gone. Replay reads the saved input from the journal.
Photon is the final authority on idempotency. Even if the local record is
gone, sending the same key and input can still return the service’s cached
result.
Inspect the queue
list returns unresolved records only, newest first, and deliberately omits
idempotency keys:
view adds the key when there is one, plus safe input metadata, the attempt
count, the latest failure, and a remote operation ID when server work has
already been accepted. Retained private payloads — invitation tokens, avatar
bytes, 10DLC campaign text — are redacted.
Statuses
resolved appears only in the immediate result of an explicit replay — it is
not a journal status or a valid --status filter. Neither is pending; most
commands keep polling rather than returning a pending success.
Idempotency keys
Supplied keys are 1–255 printable, non-whitespace ASCII characters unless a command narrows that further. Key matching happens only for keys you supply; a generated key always starts a new record. When an unresolved record matches your key and the request fingerprint agrees, the CLI takes over that record and runs your current input under the original key. Different input is rejected locally:Replay
Replay dispatches a typed operation using the saved input and key. A per-operation lease stops two processes replaying the same record at once. It does not re-read an input file or ask you for the original values.The CLI never stores or executes shell text. A record holds a typed operation
definition, not the command line you typed.
Waiting on server work
Commands never return a successful{"status": "pending"} envelope. They show
a spinner in an interactive terminal, stay silent in JSON or redirected output,
and keep polling until a terminal result or an interruption.
Voice outbound algorithm edits preserve the username and password and use no
operation journal or idempotency key. If an edit reports
VOICE_OUTBOUND_OPERATION_PENDING, resume the original credential request with
its original idempotency key or recovery record, then read the current profile
and retry the edit.
Voice outbound enable and rotation use the same journal for their optimistic
profile version. A same-key continuation of an unresolved request reuses the
stored expectedVersion; it does not fetch the changed profile and construct a
new request body. Their display-once password is never stored. If the server
already applied the mutation, recovery succeeds with password: null and the
CLI directs you to rotate with a new key when another password is required.
A network or protocol error while polling is not treated as pending — it goes
straight to the recovery path. Replay resumes a stored remote operation with a
GET and never cancels it. There is no client-side total timeout, so interrupt
the process with a signal if you need to stop waiting.
Record lifecycle
The record is durable before the first mutation request. If that write fails, no request is sent at all.- A final success deletes the record.
- A definitive rejection deletes the record and stays an ordinary command
error — it never shows up in
operation list. - Ctrl-C, network interruption, protocol failure, and other ambiguous outcomes keep a record with recovery metadata.
- One-time service key, API key, webhook-secret, and Voice outbound password
output stays in
awaiting-output-ackuntil stdout flushes successfully.
What is stored
A replayable record keeps only what is needed to rebuild the request: project and resource IDs, normalized change sets, invitation tokens, a private copy of a validated avatar before upload completes, and normalized 10DLC campaign input. Call Registry records keep the automatic-registration boolean or canonical phone-resource IDs — never a profile body or a phone number. Voice outbound records keep the user-facing profile reference, resolved stableprofileId, exact expectedVersion, enablement digest algorithm, and rotation
mode when present.
Records are scoped to the API origin and the authenticated identity, and stored
with owner-only permissions in Photon’s configuration directory. Symlinks,
non-regular files, unsafe permissions, and a scope mismatch all fail closed.
Unreadable records
Records use only the current strict schema, so an unresolved record written by an earlier incompatible release cannot be read.operation list and ordinary mutations skip unreadable records with a warning
so unrelated work continues. operation view and operation replay report
OPERATION_JOURNAL_CORRUPT for a record you select directly.
operation remove validates only the ID and deletes the file without parsing
it, so it can always clear a corrupt record:
operation remove deletes only the local record. It never cancels, reverses,
or deletes anything on the server.Operations saved before an API update
If a saved request lacks newly required fields, replay reportsOPERATION_CONTRACT_CHANGED and leaves the record intact. Inspect the remote
result before starting a new operation. The CLI cannot safely invent a project
slug, API-key permissions, campaign compliance URLs, or a campaign ETag for an
old request. An old SMS purchase with a known remote operation ID can still be
polled without sending the purchase again.
Operations saved before organizations — project member, invitation, and leave
operations, and project creation or 10DLC operations without an organization —
also report OPERATION_CONTRACT_CHANGED. Check the organization with
photon org member list, photon org invitation list, or photon project list
before starting a new operation.
With --json, errors are written to stderr inside an error object, and the
command exits with a nonzero status. Selected fields from an incompatible
replay error: