Local webhooks
The proxy CLI relays the same signed webhook request that Proxy would send to your HTTPS endpoint into a loopback development server. The source HTTPS endpoint keeps receiving its normal delivery; the temporary listener receives an additional canonical delivery with normal attempt, retry, replay, and audit history.
Prerequisites
Section titled “Prerequisites”- Node.js 22 or newer.
- A Proxy organization with an active test merchant.
- An active HTTPS Proxy webhook endpoint for that merchant. The local listener clones its event selection, schema version, and signing-secret identity without returning the secret to the CLI.
- The source endpoint’s Proxy signing secret in your local application’s existing secret store.
- For
--stripe, the Stripe CLI installed and authenticated to the test Stripe account connected to the merchant.
Install the public package:
npm install --global @proxy-checkout/cliproxy --versionInspect the generated path contract before credentials or listeners are involved:
proxy stripe doctor --path OT-PI --format jsonproxy stripe doctor --path OT-CO-H --format jsonproxy stripe doctor --path OT-CO-E --format jsonproxy stripe doctor --path OT-CO-C --format jsonstripe doctor is static and credential-free. It reports the required capabilities, read operations, restricted-key permissions, event profile, minimum versions, helper, unsupported options, and current availability status. It never contacts Stripe or Proxy. When you later run proxy listen --stripe, the authenticated Stripe configuration determines the enabled capabilities and event profile.
To validate embedded or custom Checkout, run a real merchant-server openCheckout route and mount and submit the returned discriminated client-secret presentation in two clean browser contexts. stripe trigger or a server-only Session create does not validate browser rendering, shared-session interaction, or the number of Stripe creates.
Authenticate
Section titled “Authenticate”-
Start a device login and name the profile. The optional merchant and mode flags assert the target you expect the browser approval to return; they do not silently select or elevate a merchant.
Terminal window proxy auth login \--profile my-app-test \--merchant merch_... \--mode test -
The CLI opens a Proxy approval page and prints a non-secret pairing code and URL. Review the organization, merchant, test mode, capability bundle, and expiry, then approve. Use
--no-browserwhen another person or process will open the printed URL. -
Confirm the server-verified profile before running a command.
Terminal window proxy auth status \--profile my-app-test \--merchant merch_... \--mode test
The temporary browser session exists only long enough to exchange it for a distinct pcli_test_... CLI credential. Proxy revokes that temporary session during the exchange. The CLI never persists a Portal session or receives a merchant sk_test_* key.
On macOS the credential is stored in Keychain; supported Linux desktops use Secret Service. If neither is available, the CLI prints a warning and uses an owner-only 0600 credential file under the user’s data directory. Profile metadata is owner-only and binds the credential to its saved API origin, organization, merchant, and mode.
Add a local handler
Section titled “Add a local handler”Keep parsing and signature verification on the exact raw body. Do not parse and reserialize JSON before verification.
import { createServer } from "node:http";import { constructProxyWebhookEvent } from "@proxy-checkout/server-js";
createServer(async (request, response) => { if (request.method !== "POST" || request.url !== "/proxy-webhooks") { response.writeHead(404).end(); return; }
const chunks: Buffer[] = []; for await (const chunk of request) chunks.push(Buffer.from(chunk)); const body = Buffer.concat(chunks); const signature = request.headers["proxy-signature"];
if (typeof signature !== "string") { response.writeHead(400).end(); return; }
try { const event = constructProxyWebhookEvent({ body, header: signature, secret: process.env.PROXY_WEBHOOK_SIGNING_SECRET!, }); console.log(event.id, event.type); response.writeHead(200).end(); } catch { response.writeHead(400).end(); }}).listen(4242, "127.0.0.1");PROXY_WEBHOOK_SIGNING_SECRET is the existing source endpoint secret. The CLI does not print, download, or store it. Use the higher-level proxy.webhooks.handle(...) API in an application that also resolves current state and applies idempotent fulfillment.
Listen
Section titled “Listen”List eligible source endpoints, then start the automatic relay:
proxy webhooks endpoints list --profile my-app-test --format json
proxy listen \ --profile my-app-test \ --merchant merch_... \ --mode test \ --endpoint whend_... \ --forward-to http://127.0.0.1:4242/proxy-webhooksWhen exactly one source endpoint is active, an interactive terminal can omit --endpoint. Pass it explicitly for agents, CI, or --no-input. The destination must be HTTP or HTTPS on localhost, 127.0.0.1, or ::1; remote forwarding, URL credentials, and fragments are rejected.
The listener renews a two-minute server lease every 30 seconds. It claims canonical due work, forwards Proxy’s exact prepared body and headers, reports the local HTTP or network result, and lets the server apply the product retry transition. A 2xx response completes the attempt. A retryable response, timeout, or network error remains in the canonical retry schedule. Use --max-events 1 for a bounded smoke test; the default runs until interrupted.
Press Control-C to close the listener. Logout also closes resources owned by that credential. If the process crashes or loses the network, the server expires the short lease and archives the listener without changing the source HTTPS endpoint.
Add Stripe orchestration
Section titled “Add Stripe orchestration”Authenticate Stripe CLI first:
stripe loginThen add --stripe:
proxy listen \ --stripe \ --profile my-app-test \ --merchant merch_... \ --mode test \ --endpoint whend_... \ --forward-to http://127.0.0.1:4242/proxy-webhooksBefore asking Stripe CLI for a signing secret, Proxy selects an eligible test Stripe configuration and returns the exact event profile required by its enabled capabilities. The CLI uses that profile for Stripe forwarding and rejects startup if the configuration changes before the temporary ingress is ready. You do not need to keep a separate event list in the CLI or add dashboard configuration for the local listener.
The CLI obtains Stripe CLI’s test webhook signing secret in child-process memory without echoing it, creates a credential-owned temporary Stripe ingress, and forwards only the event types selected by the Proxy API. A manifest-major mismatch fails closed with upgrade guidance. The signing secret stays encrypted in the PSP configuration, and the CLI never receives the Stripe API credential.
Use @proxy-checkout/cli 0.1.1 or newer for Stripe profiles that include capabilities such as refund or dispute observation. Older versions support only the base event profile and are rejected when the selected configuration requires additional events. Upgrade the CLI instead of keeping a separate event list.
With --format jsonl, the safe stripe.started record includes merchant_psp_config_id, effective_capability_ids, event_types, manifest_version, profile_hash, the temporary ingress ID, and its expiry. The hash is only a change detector; it is not a credential or authentication mechanism. Save those fields when diagnosing a test run, but never save Stripe or Proxy secrets.
Pass --stripe-psp-config pspcfg_... to assert a specific active Stripe configuration. Agents and CI should always pass it when a merchant has more than one Stripe configuration. An active configuration is eligible; a draining configuration is eligible only while Proxy still has lifecycle work pinned to it. Archived, live-mode, wrong-merchant, unsupported endpoint-generation, or event-empty profiles fail before a durable temporary ingress is created. Interrupting the command closes both hops; lease expiry cleans them up after a crash.
Inspect, order, and replay deliveries
Section titled “Inspect, order, and replay deliveries”proxy listen is the normal automatic path. Use explicit commands when a test needs manual ordering or a replay:
proxy webhooks listeners create \ --profile my-app-test \ --endpoint whend_...
proxy webhooks deliveries list \ --profile my-app-test \ --listener whend_... \ --format json
proxy webhooks deliveries forward \ --profile my-app-test \ --listener whend_... \ --delivery whdel_... \ --forward-to http://127.0.0.1:4242/proxy-webhooks
proxy webhooks deliveries replay \ --profile my-app-test \ --listener whend_... \ --delivery whdel_... \ --reason "verify duplicate handling"
# Read replay.outbox_event_id from the previous JSON result.proxy webhooks deliveries forward \ --profile my-app-test \ --listener whend_... \ --replay-outbox out_... \ --forward-to http://127.0.0.1:4242/proxy-webhooks
proxy webhooks listeners close \ --profile my-app-test \ --listener whend_...A manually managed listener does not run a background heartbeat. Call proxy webhooks listeners heartbeat --listener whend_... at least every 30 seconds while a longer test is active. deliveries forward atomically claims only the selected due delivery or replay outbox, so choosing delivery IDs lets you test out-of-order handling. Replay is available only after a local delivery completes; it schedules fresh canonical work with a new signature timestamp while preserving the original history and recording the reason. A running proxy listen claims scheduled replay work automatically; for a manual listener, pass the returned replay outbox ID to deliveries forward --replay-outbox.
Your webhook handler must remain idempotent. A replayed successful event is intentionally another delivery of the same domain event, not permission to apply the business mutation twice.
Agent and CI use
Section titled “Agent and CI use”Every command is defined in one versioned command registry shared by help and programmatic callers:
proxy commands schema --format jsonproxy auth status --schema --format jsonproxy --llms-fullNon-TTY output defaults to JSON Lines. Use --format json for a one-shot result and --format jsonl for streaming commands such as listen and auth login. Add --no-input, --endpoint, --merchant, and --mode test for deterministic automation.
PROXY_CLI_TOKEN supplies a non-persisted credential for agents or CI. Treat it as a secret, inject it through the runner’s secret mechanism, and never put it in command arguments, logs, images, or source files. An environment credential may explicitly override an API origin; a stored profile credential cannot be sent to an origin other than the one saved during login.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Success |
2 |
Usage or validation error |
3 |
Authentication required, denied, expired, or invalid |
4 |
Proxy API/network failure |
5 |
Capability, merchant, or mode not allowed |
6 |
Resource conflict or non-claimable state |
7 |
Required dependency such as Stripe CLI is missing or unauthenticated |
8 |
Interrupted |
10 |
Unexpected CLI failure |
Machine-readable errors always include a stable error.code. Do not branch automation on prose messages.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Resolution |
|---|---|
cli_not_authenticated |
Run proxy auth login for the requested profile, or inject PROXY_CLI_TOKEN. |
| No source endpoint is available | Create and activate an HTTPS Proxy webhook endpoint for the test merchant first. |
| Multiple source endpoints are available | Pass the intended --endpoint whend_..., especially with --no-input. |
| Local signatures fail | Verify the exact raw body with the signing secret for the selected source endpoint; do not use a Stripe whsec_... for a Proxy outbound signature. |
stripe_cli_not_found |
Install Stripe CLI and make sure stripe is on PATH. |
stripe_cli_auth_required |
Run stripe login for the connected test Stripe account, then retry. |
stripe_psp_config_not_eligible |
Pass an active test Stripe configuration, or a draining configuration that still owns pinned lifecycle work. Use --stripe-psp-config to remove ambiguity. |
stripe_ingress_profile_empty |
The selected configuration has no locally deliverable Stripe events. Enable a supported capability before using --stripe. |
stripe_ingress_profile_stale |
Configuration or capability state changed while startup was in progress. Retry so the CLI preflights a fresh profile. No stale temporary ingress was created. |
stripe_ingress_profile_unsupported |
Upgrade the CLI for a manifest-major change, or use a supported classic Stripe Webhook Endpoint/API version. Thin events and Event Destinations v2 are not supported by this listener. |
cli_live_mode_not_enabled |
Use a test merchant and --mode test; live resources are not supported. |
| Cleanup could not be confirmed | Keep the listener ID from structured output. Its two-minute lease expires server-side; listeners close is safe to retry while the credential remains valid. |
Revoke the profile when the local workflow is finished:
proxy auth logout --profile my-app-test --merchant merch_... --mode test