---
title: Pine Labs CLI
slug: cli
excerpt: >-
  Official command-line interface for the Pine Labs Plural platform. Create
  orders, listen for and replay webhooks, run deterministic API calls, and
  automate workflows from your shell or CI.
hidden: false
sidebar_label: CLI
metadata:
  title: Pine Labs CLI — Official Developer Documentation
  description: >-
    Complete reference documentation for the Pine Labs CLI. Covers installation,
    authentication, webhook management, event handling, and full Plural API
    integration from the command line.
  keywords: >-
    Pine Labs CLI, Plural CLI, payments CLI, command-line tool, webhook testing,
    developer CLI, payment automation
  robots: index
---
The **Pine Labs CLI** (`pinelabs`) is the official command-line interface for the Pine Labs Plural platform. It is a thin, deterministic, fully auditable client over the Pine Labs Online APIs — the same APIs your production code calls.

**Use it to:**

- Create and inspect orders, payments, refunds, payouts, subscriptions, and settlements.
- Receive Pine Labs webhook events on a local URL over a secure WebSocket relay.
- Sign, verify, and replay webhook events against your handlers.
- Drive the same workflows from CI with structured JSON output.
- Drive the same workflows through an MCP-backed `ask` command for assistive exploration.

> **Versioning.** The CLI follows semver from `1.0.0`. Breaking changes bump the major version with a deprecation cycle. Pin to an exact version in CI: `npm install -g @pinelabs/cli@1.0.1`.

This page is split in two:

1. **[Get started fast](#get-started-fast)** — install, log in, first order, listen, trigger. Read this first if you have 2 minutes.
2. **[CLI reference](#cli-reference)** — flags, authentication, every command group, webhooks/events, MCP, security, troubleshooting. Deep dive.

---

## Quickstart in 60 seconds

```bash
npm install -g @pinelabs/cli

export PINELABS_CLIENT_ID="your_client_id_here"
export PINELABS_CLIENT_SECRET="your_client_secret_here"

pinelabs login
pinelabs orders create --order-amount 100 --currency INR \
  --merchant-order-reference quickstart_$(date +%s)
```

On **Windows (PowerShell)**, use `$env:PINELABS_CLIENT_ID = "..."` and `$env:PINELABS_CLIENT_SECRET = "..."` instead of `export`.

Four lines from a clean shell to a real sandbox order. If `npm` isn't an option, see [Install](#install) for alternative install paths. The longer [Get started fast](#get-started-fast) section adds webhooks, replay, and CI patterns.

---

## Common tasks

Developers think in tasks, not commands. The most common ones map directly:

| Task | Command |
| ---- | ------- |
| Create a test order | `pinelabs orders create --order-amount 49900 --currency INR --merchant-order-reference order_$(date +%s)` |
| Look up an order | `pinelabs orders get ord_abc` |
| Refund an order | `pinelabs refunds create --order-id ord_abc --amount 10000` |
| Listen for webhooks locally | `pinelabs listen --forward-to http://localhost:3000/webhooks` |
| Replay failed deliveries | `pinelabs events replay-spool --forward-to http://localhost:3000/webhooks` |
| Trigger a sandbox event | `pinelabs trigger payment.success` |
| Verify a webhook signature | `pinelabs webhooks verify --webhook-id evt_x --webhook-timestamp 17… --webhook-signature … --body @payload.json` |
| Check a settlement by UTR | `pinelabs settlements get-by-utr <utr>` |
| Confirm which environment you're on | `pinelabs whoami` |
| Self-diagnose a broken setup | `pinelabs doctor` |

Each row is documented in full in the [CLI reference](#cli-reference).

---

## How the CLI works (mental model)

The CLI is intentionally a thin layer. Holding the right mental model makes every flag below obvious:



```mermaid
flowchart LR
    CLI["Pinelabs CLI<br/>(your laptop)"]
    API["Pine Labs APIs<br/>(Plural backend)"]
    APP["Your Local App<br/>(localhost:3000)"]
    RELAY["Pine Labs Relay<br/>(event broker)"]

    CLI -- "HTTPS" --> API
    API -- "HTTPS Response" --> CLI

    APP -- "Local POST<br/>(audit log, spool, replay)" --> CLI

    API -- "Emits Webhook" --> RELAY
    RELAY -- "WSS<br/>pinelabs listen" --> APP
```





- `pinelabs <resource>` calls Pine Labs APIs directly. No proxy, no local rewriting — the CLI is a faithful HTTP client with authentication, idempotency keys, and audit logging bolted on.
- `pinelabs listen` opens an outbound WebSocket to the Pine Labs relay and forwards events to your local URL. There is no inbound port, public tunnel, or localhost exposure.
- State on disk is small and well-known: `~/.pinelabs/config.json` (credentials, profiles), `~/.pinelabs/audit.log` (every command + request id), and `~/.pinelabs/spool.jsonl` (failed-delivery queue). Delete the directory and the CLI is reset.
- Replay is real, not mocked. `events replay-spool` re-POSTs spool entries with the original body and `webhook-id`, but with a freshly signed `webhook-timestamp` / `webhook-signature` so spec-compliant verifiers accept them. Your handler runs against signatures it would see in production.

---

## Sandbox vs production

The CLI talks to whichever environment your active profile points at. There is no `--live` / `--mode` flag yet; the active `api_base_url` is the source of truth. Always verify before any destructive command:

```bash
pinelabs whoami
# look at "api_base_url"
#   sandbox    → https://pluraluat.v2.pinepg.in/...
#   production → https://api.pluralpay.in/... (or your tenanted host)
```

A safer pattern is to keep separate named profiles and let the profile name carry the environment:

```bash
pinelabs --profile sandbox login
pinelabs --profile prod    login

pinelabs --profile sandbox orders create …   # daily testing
pinelabs --profile prod    orders get  ord_x  # read-only investigation
```

> **Warning.** Never run `pinelabs trigger` against a production profile unless you fully understand the downstream side effects — see the warning under [`trigger`](#trigger).

---

## Why Pine Labs CLI is different

- **Deterministic workflows.** Every write carries an `Idempotency-Key` so retries are safe; the documented per-endpoint contract tells you exactly what replays vs. dedups vs. surfaces.
- **Signature-faithful webhook replay.** Failed deliveries land in a local spool, are re-signed with a fresh timestamp on replay, and pass spec-compliant 5-minute tolerance verifiers. No "strip signatures to make it work" hacks.
- **Audit log by default.** Every command, request id, and response status goes to `~/.pinelabs/audit.log` with PCI/PII redacted before write — the artifact Support actually asks for.
- **AI-assisted with guardrails.** The MCP-backed `ask` planner proposes tool calls; the CLI validates, derives idempotency keys deterministically, and prompts before any non-readonly action. The planner cannot run payments directly.
- **`jq`-safe output.** Strict stdout/stderr split means prompts, warnings, and progress lines never leak into machine-readable JSON.

### Idempotency behaviour by endpoint

The CLI sends an `Idempotency-Key` header on every write, but **the server handles it differently per endpoint.** Understand the contract before you retry:

| Endpoint | Dedupe primitive | Retry behaviour | What the CLI does |
| -------- | ---------------- | --------------- | ----------------- |
| `POST /orders` | `merchant_order_reference` | Returns HTTP 422 `DUPLICATE_REQUEST` on conflict — **not** a replay of the original response. | Auto-generates a unique reference per invocation. If you pass `--idempotency-key`, it is also used as the reference so header and body agree. |
| `POST /refunds` | `merchant_order_reference` | Replays the original 2xx response on retry. Safe to retry on transport errors. | Auto-generates a key per invocation; override with `--idempotency-key`. |
| `POST /payouts` | `clientReferenceId` | Replays the original 2xx response on retry. | Same as refunds. |
| `raw POST …` | `Idempotency-Key` header | Depends on the underlying endpoint. | Sends the key you pass via `--idempotency-key`, or auto-generates one. |

> **Note.** The CLI retries on `502`, `503`, and `504` (transport/gateway errors). A `500` is surfaced immediately because the server may have already committed the side effect.

If you've been writing payments code with `curl`, Postman collections, or generic HTTP clients, this is what you're trading them for.

### How it compares

| Capability | Pine Labs CLI | `curl` / Postman | Generic CLIs |
| ---------- | :-----------: | :--------------: | :----------: |
| Native auth + token refresh | ✅ | ❌ | varies |
| Deterministic idempotency keys per write | ✅ | manual | partial |
| Webhook relay over outbound WSS (no tunnels) | ✅ | ❌ | partial |
| On-disk spool with signature-faithful replay | ✅ | ❌ | partial |
| Audit log with PCI/PII redaction | ✅ | ❌ | partial |
| AI-assisted (`ask`) with deterministic guardrails | ✅ | ❌ | ❌ |
| Strict stdout/stderr split (`… \| jq` always works) | ✅ | n/a | varies |
| First-class Pine Labs Plural endpoints | ✅ | manual | ❌ |

The table is qualitative — individual tools differ — but the design delta is real: this CLI treats reliability primitives (idempotency, audit, replay) as defaults, not opt-ins.

---

## When to use the CLI — and when not to

**Use it for:**

- Building and testing integrations against the Plural sandbox.
- CI smoke tests and release verification (`pinelabs orders create | jq -e`).
- Debugging live integration issues — audit log, `whoami`, `doctor`.
- Replaying webhooks against your handler under realistic signatures.
- Ad-hoc operator tasks (refund a known order, check a settlement UTR).

**Do NOT use it for:**

- **End-user authentication flows.** The CLI uses OAuth2 client credentials — it is not an end-user login mechanism, has no browser/device-code flow, and is not designed to run in a customer's browser or app.
- **High-frequency production traffic.** The CLI is a thin client over HTTP with sequential semantics; production payment volume should go through your own server-to-server integration of the Plural APIs, not through shelling out to a binary.
- **Storing long-lived credentials on shared / multi-user machines.** `~/.pinelabs/config.json` is `0600`; that's enough for a single-user workstation or ephemeral CI runner, not a shared bastion host.

---

## Get started fast

### Install

The CLI ships **pre-built native binaries** for macOS, Linux, and Windows via npm. No build tools, compilers, or runtime dependencies required.

```bash
# Recommended (any OS with Node.js 16+)
npm install -g @pinelabs/cli

# Zero-install (one-off commands and CI)
npx @pinelabs/cli version

# Yarn
yarn global add @pinelabs/cli

# pnpm
pnpm add -g @pinelabs/cli
```

The package downloads the correct binary for your platform and architecture automatically.

**Supported platforms:**

| Platform | Architectures |
| -------- | ------------- |
| macOS | Apple Silicon (arm64), Intel (x64) |
| Linux | x64, arm64 |
| Windows | x64 |

**Verify:**

```bash
pinelabs version
# → pinelabs 1.0.1 (abc1234, built 2026-05-12T10:00:00Z)
```

### Log in

The CLI authenticates with OAuth2 client credentials — the server-to-server flow. It is not designed for user login flows (there is no browser-based or device-code OAuth flow), so do not embed it in customer-facing sign-in paths.

You'll need your **Client ID** and **Client Secret** from the [Pine Labs merchant dashboard](https://developer.pinelabsonline.com/) (**Developers → API Keys**).

```bash
export PINELABS_CLIENT_ID="your_client_id_here"
export PINELABS_CLIENT_SECRET="your_client_secret_here"
pinelabs login
```

Expected output:

```text
logged in. Token preview: eyJhbG…****
```

> **OAuth2 Client Credentials.** The CLI uses OAuth2 client credentials. There is no browser-based or device-code flow today; this auth model is intended for server-to-server integrations and CI runners, not interactive end-user sign-in. See [Authentication](#authentication) for details.

By default `login` performs a token exchange against the Plural API and persists only the short-lived `access_token` (and `expires_at`). The client secret is read from the environment, used in memory, and **never written to disk**. See [Authentication](#authentication) for `--direct`, `--store-secret`, and CI patterns.

### First order

```bash
pinelabs orders create \
  --order-amount 49900 \
  --currency INR \
  --merchant-order-reference order_$(date +%s) \
  --customer-email jane@example.com
```

**Sample response (JSON, abbreviated):**

```json
{
  "order_id": "ord_NQ4j8xK2pZmL1a",
  "status": "CREATED",
  "order_amount": {
    "value": 49900,
    "currency": "INR"
  },
  "merchant_order_reference": "order_1717420000",
  "redirect_url": "https://pay.pluralonline.com/p/NQ4j8xK2pZmL1a"
}
```

Note that `order_amount` is a nested object (`{ value, currency }`), not a top-level `amount`/`currency` pair — pipe accordingly:

```bash
pinelabs orders create ... | jq '.order_id, .order_amount.value'
```

Add `--open` to launch the redirect URL in your default browser.

### Listen for webhooks

```bash
# one-time: store the webhook secret from your Pine Labs dashboard
pinelabs config set webhook_secret "your_webhook_secret_here"

pinelabs listen --forward-to http://localhost:3000/webhooks
```

Expected output:

```text
relay connected -> forwarding to http://localhost:3000/webhooks (verify=true)
payment.authorized evt_abc123 -> 200 (delivered)
```

The CLI dials a secure outbound WebSocket to the Pine Labs relay and forwards matching events to your local URL. `listen` verifies signatures by default and exits non-zero if no `webhook_secret` is configured — pass `--skip-verify` for local dev only. See [`listen`](#listen) for the full flag reference.

### Trigger an event

```bash
pinelabs trigger payment.success
pinelabs trigger --list                       # see available fixtures
pinelabs trigger payment.success --add amount.value=15000
```

Triggered events are dispatched server-side; they flow back through the same relay and Pine Labs signature pipeline used by `listen`. `--add` overrides and `--raw` payloads are forwarded as the event body, so an active `pinelabs listen` will see exactly the payload you constructed.

---

## CLI reference

### Global flags

These flags are registered on the root command and are inherited by every subcommand:

| Flag | Default | Description |
| ---- | ------- | ----------- |
| `--profile <name>` | `default` | Config profile to use (see [Profiles](#profiles)). |
| `--api-base <url>` | profile value | Override the active profile's `api_base_url` for this invocation only. Does not persist. |
| `--timeout <duration>` | `30s` | Override the per-request HTTP timeout, for example `60s` or `2m`. |

Subcommands add their own flags — see each command. Common subcommand flags include `--idempotency-key`, `--yes`, and resource-specific identifiers.
`--help` is available on every command.

### Environment variables

The CLI is conservative about implicit configuration. Every Pine Labs or LLM-specific environment variable it reads directly is listed below. Standard OS/runtime variables such as `HOME` and HTTP proxy settings may still affect file locations or network transport.

#### Authentication

| Variable | Used by | Description |
| -------- | ------- | ----------- |
| `PINELABS_CLIENT_ID` | `login` | OAuth client id (required for `login`). |
| `PINELABS_CLIENT_SECRET` | `login`; runtime by every command in two cases | (a) `--direct` profile without `--store-secret`, or (b) OAuth profile whose access token has expired — the CLI does not store a refresh token, so it must re-exchange `client_id` + `client_secret` to mint a fresh bearer. Keep this exported in any shell that runs CLI commands beyond the access-token lifetime, or accept a one-time `pinelabs login` to re-mint. Never persisted unless `--store-secret` is passed. |

#### Webhooks

| Variable | Used by | Description |
| -------- | ------- | ----------- |
| `PINELABS_WEBHOOK_SECRET` | `webhooks verify`, `webhooks sign` | Default signing secret when `--secret` is omitted. Base64-encoded. |

#### MCP relay (opt-in, AI workflows only)

| Variable | Used by | Description |
| -------- | ------- | ----------- |
| `PINELABS_MCP_ENDPOINT` | `mcp`, `ask` | URL of the MCP server. |
| `PINELABS_MCP_CLIENT_ID` | `mcp`, `ask` | MCP OAuth client id (falls back to `PINELABS_CLIENT_ID`). |
| `PINELABS_MCP_CLIENT_SECRET` | `mcp`, `ask` | MCP OAuth client secret (falls back to `PINELABS_CLIENT_SECRET`). |
| `PINELABS_MCP_API_KEY` | `mcp`, `ask` | Static API-key auth, used when client credentials are absent. |

#### `ask` planner (LLM selection)

| Variable | Used by | Description |
| -------- | ------- | ----------- |
| `PINELABS_ASK_PROVIDER` | `ask` | Override LLM provider (`openai`, `anthropic`, `groq`, `ollama`). |
| `PINELABS_ASK_MODEL` | `ask` | Override model name. |
| `PINELABS_ASK_MAX_TOOLS` | `ask` | Cap on the number of candidate tools sent to the planner per question. |
| `PINELABS_ASK_DEBUG` | `ask` | When set to `1`/`true`, prints planner trace to stderr. |
| `OPENAI_API_KEY` | `ask` | API key for the OpenAI provider. |
| `ANTHROPIC_API_KEY` | `ask` | API key for the Anthropic provider. |
| `GROQ_API_KEY` | `ask` | API key for the Groq provider. |
| `OLLAMA_HOST` | `ask` | Base URL for a local Ollama instance. |

Other settings (API base URL, relay URL, merchant id, webhook secret, client id/secret) live in `~/.pinelabs/config.json` and are managed via `pinelabs config set`.

### Authentication

The CLI supports two credential paths.

#### 1. OAuth client credentials (default, recommended)

```bash
export PINELABS_CLIENT_ID="your_client_id_here"
export PINELABS_CLIENT_SECRET="your_client_secret_here"
pinelabs login
```

This performs a `POST` to the Plural token endpoint and stores **only** the resulting short-lived `access_token` plus `expires_at` in `~/.pinelabs/config.json` (mode `0600`). The client secret is **never written to disk** in this mode.

#### 2. Direct header auth (`X-Client-Id` / `X-Client-Secret`)

For environments that prefer header-based auth and want to skip token exchange:

```bash
pinelabs login --direct                # stores client_id only
pinelabs login --direct --store-secret # ALSO persists client_secret (CI only)
```

Without `--store-secret`, the client secret must be re-exported in every shell that runs the CLI. With `--store-secret`, the secret is written in plaintext to `~/.pinelabs/config.json` (mode `0600`) and the CLI prints a loud warning. **Use this only on ephemeral CI runners.**

#### Logout

```bash
pinelabs logout                  # clears tokens for the active profile
pinelabs logout --profile staging
```

Clears `access_token`, `refresh_token`, and `expires_at` but preserves `client_id` so `pinelabs login` can resume without re-entering credentials.

#### Profiles

The CLI supports multiple named profiles in a single config file.

```bash
pinelabs --profile staging login
pinelabs --profile staging orders create ...
```

Profiles are isolated: each has its own `api_base_url`, `relay_url`, `client_id`/`client_secret`, tokens, `merchant_id`, and `webhook_secret`.

### Configuration

Config lives at `~/.pinelabs/config.json` (mode `0600`, parent dir `0700`). The format is JSON, not TOML.

```json
{
  "profiles": {
    "default": {
      "api_base_url": "https://pluraluat.v2.pinepg.in/api/pay/v1",
      "relay_url": "wss://relay.pinelabs.com/v1/listen",
      "auth_mode": "oauth",
      "client_id": "...",
      "access_token": "...",
      "expires_at": 1746630000,
      "merchant_id": "...",
      "webhook_secret": "..."
    }
  }
}
```

Manage it through the CLI (recommended) or hand-edit:

- `pinelabs config get <key>`
- `pinelabs config set <key> <value>`
- `pinelabs config list`

**Editable keys:** `api_base_url`, `relay_url`, `client_id`, `client_secret`, `merchant_id`, `webhook_secret`. The session quartet (`access_token`, `refresh_token`, `expires_at`, `auth_mode`) is owned by `login`/`logout` and cannot be set directly.

### Command groups

Every command supports `--help`. The lists below reflect what is wired up in the current CLI and in the generated command reference.

#### `orders`

Manage checkout orders — the top-level container for one or more payment attempts.

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs orders create` | `POST /api/checkout/v1/orders` | Create an order with redirect URL. **Flags:** `--order-amount`, `--currency`, `--merchant-order-reference`, `--mode`, `--callback-url`, `--customer-email`, `--customer-mobile`, `--open`, `--idempotency-key` |
| `pinelabs orders get <id>` | `GET /orders/{id}` | Fetch order details |
| `pinelabs orders capture` | `POST /orders/{id}/capture` | Capture a pre-authorized order. **Flags:** `--order-id`, `--merchant-capture-reference`, `--capture-amount` (`0` = full capture), `--currency`, `--idempotency-key` |
| `pinelabs orders cancel` | `POST /orders/{id}/cancel` | Cancel an order. **Flags:** `--order-id`, `--yes`, `--idempotency-key` |

```bash
pinelabs orders create \
  --order-amount 49900 --currency INR \
  --merchant-order-reference order_42 \
  --callback-url https://example.com/return
```

#### `payments`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs payments list` | `GET /orders/{id}` | List payments embedded in an order. **Flags:** `--order-id` |
| `pinelabs payments upi` | `POST /orders/{id}/payments` | UPI Collect/Intent flow. **Flags:** `--order-id`, `--txn-mode INTENT|COLLECT` (default `INTENT`), `--vpa` (required for `COLLECT`), `--amount`, `--currency`, `--merchant-payment-reference`, `--idempotency-key` |

Card payments and OTP flows are not yet first-class payment commands; use [`pinelabs raw`](#raw--escape-hatch) against the corresponding endpoints.

#### `refunds`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs refunds create` | `POST /refunds/{order-id}` | Refund an order (refunds are scoped to orders, not payments). **Flags:** `--order-id`, `--amount`, `--currency`, `--merchant-order-reference` (auto-generated if blank), `--yes`, `--idempotency-key` |

```bash
pinelabs refunds create --order-id ord_abc --amount 10000
```

Refunds prompt for confirmation by default. Pass `--yes` in scripts.

#### `payouts`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs payouts create` | `POST /payouts` | Create a payout (**HIGH RISK**). **Flags:** `--amount`, `--currency`, `--beneficiary-id`, `--mode IMPS|NEFT|RTGS|UPI` (default `IMPS`), `--reference`, `--purpose`, `--idempotency-key` |
| `pinelabs payouts cancel <id>` | `POST /payouts/{id}/cancel` | Cancel a payout. **Flags:** `--yes`, `--idempotency-key` |
| `pinelabs payouts get <id>` | `GET /payouts/{id}` | Fetch payout details |
| `pinelabs payouts balance` | `GET /payouts/balance` | Check payout balance |

#### `subscriptions` and `plans`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs subscriptions plans create` | `POST /subscriptions/plans` | Create a plan. **Flags:** `--name`, `--frequency Day|Week|Month|Year` (default `Month`), `--amount`, `--max-limit`, `--currency`, `--end-date`, `--reference`, `--idempotency-key` |
| `pinelabs subscriptions plans list` | `GET /subscriptions/plans` | List plans. **Flags:** `--page`, `--size` |
| `pinelabs subscriptions plans delete <id>` | `DELETE /subscriptions/plans/{id}` | Delete a plan (**HIGH RISK**). **Flags:** `--yes`, `--idempotency-key` |
| `pinelabs subscriptions create` | `POST /subscriptions` | Create a subscription. **Flags:** `--plan-id`, `--customer-id`, `--reference`, `--idempotency-key` |
| `pinelabs subscriptions get <id>` | `GET /subscriptions/{id}` | Fetch subscription details |
| `pinelabs subscriptions cancel <id>` | `POST /subscriptions/{id}/cancel` | Cancel a subscription (**HIGH RISK**). **Flags:** `--yes`, `--idempotency-key` |
| `pinelabs subscriptions presentations create` | `POST /subscriptions/{id}/presentations` | Create a presentation (**HIGH RISK**). **Flags:** `--subscription-id`, `--due-date`, `--amount`, `--currency`, `--reference`, `--yes`, `--idempotency-key` |
| `pinelabs subscriptions presentations get <id>` | `GET /presentations/{id}` | Fetch presentation |
| `pinelabs subscriptions presentations retry <id>` | `POST /presentations/{id}/retry` | Retry a presentation (**HIGH RISK**). **Flags:** `--yes`, `--idempotency-key` |
| `pinelabs subscriptions presentations delete <id>` | `DELETE /presentations/{id}` | Delete a presentation (**HIGH RISK**). **Flags:** `--yes`, `--idempotency-key` |

#### `settlements`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs settlements get-by-utr <utr>` | `GET /settlements/utr/{utr}` | Fetch settlement by UTR number |

#### `analytics`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs analytics success-rate` | `GET /analytics/success-rate` | Get payment success rate. **Flags:** `--since` (default `24h`, max `168h`) |

#### `search`

| Command | Endpoint | Description |
| ------- | -------- | ----------- |
| `pinelabs search transaction <id>` | `GET /transactions/{id}` | Search for a transaction |

#### Diagnostics: `whoami` and `doctor`

**`pinelabs whoami`** — Print the active profile's identity as JSON. Includes `profile`, `api_base_url`, `relay_url`, `auth_mode`, masked `client_id` and `access_token`, `token_expired`, `webhook_secret_set`, `merchant_id`, and `logged_out` status. Read-only, no API call.

**`pinelabs doctor`** — Run self-checks and print PASS/WARN/FAIL with an actionable fix string. Exits non-zero if any check fails so CI can gate on it. **Flags:** `--json` for machine-readable output.

```text
$ pinelabs doctor
[PASS] config.path             ~/.pinelabs/config.json
[PASS] config.mode             0600
[PASS] auth.state              oauth (bearer token present)
[PASS] auth.token              valid (expires in 47m12s)
[WARN] webhook.secret          no webhook_secret on profile
       fix: `pinelabs config set webhook_secret <base64>` …
[PASS] api.reachable           https://pluraluat.v2.pinepg.in/api/pay/v1 → HTTP 404
[PASS] relay.reachable         TLS handshake ok (relay.pinelabs.com:443)

7 check(s): 0 failed, 1 warned
```

#### `raw` — escape hatch

For endpoints that don't yet have a first-class command, `raw` lets you call any path with the active profile's auth applied:

```bash
pinelabs raw GET  /orders/ord_abc
pinelabs raw POST /refunds/ord_abc --data '{"order_amount":{"value":10000,"currency":"INR"}}'
pinelabs raw PUT  /subscriptions/plans/plan_xyz --data @plan.json
```

### Webhooks

The `webhooks` group covers signature operations and event-type discovery. For receiving live events on `localhost`, use [`listen`](#listen).

| Command | Description |
| ------- | ----------- |
| `pinelabs webhooks verify` | Verify a webhook signature. **Flags:** `--secret`, `--file`/`--body`/stdin, `--webhook-id`, `--webhook-timestamp`, `--webhook-signature`, `--now` |
| `pinelabs webhooks sign` | Produce signed headers for a payload. **Flags:** `--secret`, `--webhook-id`, `--webhook-timestamp`, `--body` |
| `pinelabs webhooks events` | List supported event types. **Flags:** `--prefix`, `--json` |

`pinelabs webhooks list` and `pinelabs webhooks get` (endpoint management) are not implemented today.

#### `listen`

Open a secure WebSocket to the Pine Labs relay and forward events to a local URL.

```bash
pinelabs listen --forward-to http://localhost:3000/webhooks
```

| Flag | Default | Description |
| ---- | ------- | ----------- |
| `--forward-to <url>` | required | Local URL to POST events to. |
| `--events <list>` | all events | Comma-separated event-type filter. Trailing `.*` is supported. |
| `--skip-verify` | `false` | Skip signature verification (development only). |
| `--header, -H k:v` | — | Extra headers to add on every forwarded request (repeatable). |
| `--print-secret` | `false` | Print the signing secret on connect, then exit. |
| `--print-json` | `false` | Mirror each accepted event as one JSON line on stdout. |

**Signature scheme.** Pine Labs webhooks use three headers — `webhook-id`, `webhook-timestamp`, `webhook-signature` — with HMAC-SHA256 over `id.timestamp.body` and a base64-encoded secret.

**Fail-fast on missing secret.** If signature verification is enabled (the default) and no signing secret is available, `listen` exits non-zero immediately with a remediation message.

**Reliability.** Failed deliveries are written to a local on-disk spool (see [Events](#events-replay)). On `pinelabs events replay-spool`:

- **Body** and **`webhook-id`** are preserved verbatim.
- When `webhook_secret` is configured, replay **re-signs** with the current timestamp (old timestamps are rejected by the 5-minute tolerance window).
- Without `webhook_secret`, original headers are forwarded verbatim.
- Sends **at most one copy per `event_id` per invocation** (duplicates pruned before any POST).

#### `trigger`

Synthesize a webhook event by name. Triggers run server-side and the event flows back through the relay.

```bash
pinelabs trigger payment.success
pinelabs trigger --list                              # available fixtures
pinelabs trigger payment.success --add amount.value=15000
pinelabs trigger payment.success --raw '{"id":"evt_x","data":{"order_id":"ord_abc"}}'
```

| Flag | Default | Description |
| ---- | ------- | ----------- |
| `--list` | `false` | List available fixtures and exit. |
| `--add k=v` | — | Add or override a field on the fixture. Dotted keys nest. Repeatable. |
| `--raw <json>` | — | Replace the entire payload with this JSON (advanced). |

> **Use sandbox only.** `pinelabs trigger` calls the configured API's `/events/trigger` endpoint. Depending on the sandbox/backend fixture, that endpoint may create real sandbox transactions, refunds, or other objects. **Never run against a live/production profile** unless you fully understand the downstream side effects. Confirm `pinelabs whoami` shows a sandbox base URL first.

### Events (replay)

Failed forward attempts during `listen` are appended to a local spool at `~/.pinelabs/spool.jsonl` (JSON Lines, de-duplicated by `event_id`).

| Command | Description |
| ------- | ----------- |
| `pinelabs events resend <event-id>` | Ask Pine Labs to re-deliver a previously-sent event |
| `pinelabs events replay-spool` | Re-POST spool entries to a URL with re-signed headers. **Flags:** `--forward-to <url>` |

### Logs

Every CLI invocation appends a structured entry to `~/.pinelabs/audit.log`. Bearer tokens, client secrets, card numbers, CVVs, and other PCI/PII are redacted before the entry is written.

| Command | Description |
| ------- | ----------- |
| `pinelabs logs tail` | Follow the local audit log. **Flags:** `--remote`, `--method`, `--interval` |
| `pinelabs logs path` | Print the audit log path |
| `pinelabs logs show` | Print the last 50 audit entries |

This is the artifact Pine Labs Support will ask for when triaging an integration issue.

### MCP

> Advanced feature for AI-assisted workflows. Optional — every command group above works without it.

The CLI ships an MCP (Model Context Protocol) JSON-RPC client for tooling integrations.

| Command | Description |
| ------- | ----------- |
| `pinelabs mcp ping` | Health-check the configured MCP server |
| `pinelabs mcp tools` | List tools exposed by the MCP server |
| `pinelabs mcp call <tool-name>` | Invoke a tool. **Flags:** `--input`, `--yes` |

#### `ask` (AI-assisted, advanced)

```bash
pinelabs ask "find the most recent failed payment for order ord_abc"
```

| Flag | Default | Description |
| ---- | ------- | ----------- |
| `--plan` | `false` | Show the planned tool calls without executing them. |
| `--yes` | `false` | Skip confirmation for non-readonly tools (CI use). |

> **Confirmation required.** The planner proposes tool calls; the CLI validates and you confirm before any non-readonly tool runs. Idempotency keys for write operations are deterministically derived so plans are safely re-runnable. Refunds, payouts, and subscription cancellation always require confirmation.

### Output format

The CLI follows a strict stdout/stderr split:

- Resource commands emit pretty-printed JSON on **stdout**.
- Status / progress commands print a short status line to **stderr**.
- Confirmation prompts and warnings go to **stderr**.

This means `… | jq` is always safe on resource commands:

```bash
pinelabs orders create ... | jq '.order_id'
pinelabs refunds create --yes ... | jq '.refund_id'
pinelabs payments list --order-id ord_abc | jq '.[0]'
```

There is no `--output`/`-o` flag and no table/yaml/csv formatter today.

### CI usage

```yaml
- name: Smoke test against Pine Labs sandbox
  env:
    PINELABS_CLIENT_ID:     ${{ secrets.PINELABS_CLIENT_ID }}
    PINELABS_CLIENT_SECRET: ${{ secrets.PINELABS_CLIENT_SECRET }}
    PINELABS_CLI_VERSION:   1.0.1
  run: |
    npm install -g @pinelabs/cli@${PINELABS_CLI_VERSION}
    pinelabs login
    pinelabs orders create \
      --order-amount 100 --currency INR \
      --merchant-order-reference ci_${{ github.run_id }} \
      --idempotency-key ci_${{ github.run_id }} | jq -e '.order_id'
```

Alternatively, use `npx` for zero-install CI:

```yaml
- name: Smoke test (npx, no install step)
  env:
    PINELABS_CLIENT_ID:     ${{ secrets.PINELABS_CLIENT_ID }}
    PINELABS_CLIENT_SECRET: ${{ secrets.PINELABS_CLIENT_SECRET }}
  run: |
    npx @pinelabs/cli@1.0.1 login
    npx @pinelabs/cli@1.0.1 orders create \
      --order-amount 100 --currency INR \
      --merchant-order-reference ci_${{ github.run_id }} \
      --idempotency-key ci_${{ github.run_id }} | jq -e '.order_id'
```

For non-interactive runs:

- Pass `--yes` to skip confirmation prompts on destructive commands.
- Pass `--idempotency-key` explicitly so retries are safe.
- Pin a specific version (`@1.0.1`) so unexpected releases don't break your pipeline.
- Log `pinelabs version` with every run for an audit trail.

### Security model

- **Credentials.** OAuth `client_secret` is **never persisted by default**; only a short-lived `access_token` is. `--direct` mode stores the `client_id` only; `--direct --store-secret` is opt-in and warns loudly.
- **File modes.** `~/.pinelabs/config.json` is `0600`; the parent `~/.pinelabs/` is `0700`. Writes are atomic (`tmp` + `rename`).
- **Secret redaction.** The audit log masks bearer tokens, client secrets, card numbers, CVVs, and other PCI/PII before write.
- **Egress-only network.** The relay is outbound WSS — no inbound ports, no public tunnels, no localhost exposure.
- **Idempotency.** Write operations carry an `Idempotency-Key`. The MCP executor derives keys deterministically from tool inputs.
- **Never commit your config directory.** Add `~/.pinelabs/` to `.gitignore`:

```text
# Pine Labs CLI local state — never commit
.pinelabs/
```

Found a vulnerability? Email [security@pinelabs.com](mailto:security@pinelabs.com).

### Troubleshooting

#### Debug checklist

When something looks wrong, run these four in order:

1. `pinelabs whoami` — confirms the active profile, masked credentials, and environment.
2. `pinelabs doctor` — 7-check self-diagnosis. Exits non-zero on failure.
3. `pinelabs config get webhook_secret` — confirms the signing secret matches your dashboard.
4. `pinelabs logs tail` — follow the local audit log.

#### `pinelabs login` fails with "set PINELABS_CLIENT_ID and PINELABS_CLIENT_SECRET"

Both env vars are required. Export them in the same shell:

```bash
export PINELABS_CLIENT_ID="your_client_id_here"
export PINELABS_CLIENT_SECRET="your_client_secret_here"
pinelabs login
```

#### `token exchange failed` on `login`

Your account may not support OAuth token exchange. Fall back to direct header auth:

```bash
pinelabs login --direct
```

#### Subsequent commands fail with auth errors after `--direct`

Without `--store-secret`, the secret is not on disk. Re-export `PINELABS_CLIENT_SECRET` in your shell, or re-run `pinelabs login --direct --store-secret` (CI use only).

#### `listen` drops events with signature errors

The webhook secret stored on the profile doesn't match what the relay is using. For local dev only, pass `--skip-verify`. In real environments:

```bash
pinelabs config set webhook_secret "your_webhook_secret_here"
```

#### Replay didn't drain my spool

`pinelabs events replay-spool --forward-to ...` only acknowledges entries that get a 2xx from your handler. Check `~/.pinelabs/spool.jsonl` and `pinelabs logs tail` to see why entries are failing.

### Limitations and roadmap

The Pine Labs CLI follows semver from `1.0.0`. The underlying Plural APIs are stable; the deterministic command paths, webhook relay/replay flow, audit trail, and MCP guardrails are built for real integration workflows.

**Versioning:**

- **Plural APIs:** stable, governed by the upstream API versioning policy.
- **CLI surface:** semver from `1.0.0`. Breaking changes bump the major version. Pin to a specific version in CI.
- **Audit log / spool format:** considered internal, may change between minor versions.

**What ships today:**

- Pre-built native binaries (macOS, Linux, Windows) via npm
- Orders, payments, refunds, payouts, subscriptions, settlements
- Webhook relay with signature verification, spool, and replay
- OAuth + direct-header auth with profile isolation
- Audit log with PCI/PII redaction
- Shell completion via `pinelabs completion bash|zsh|fish|powershell`
- MCP-backed `ask` with deterministic guardrules
- `raw` escape hatch for any Plural endpoint

**What's next:**

Upcoming work includes output formatting (`--output table|csv`), `--verbose`/`--debug` flags, and additional install channels (Homebrew, Docker). Email **developer-tools@pinelabs.com** to influence priority.

### Resources

- **npm package:** [@pinelabs/cli](https://www.npmjs.com/package/@pinelabs/cli)
- **Developer portal:** [developer.pinelabsonline.com](https://developer.pinelabsonline.com/)
- **API documentation:** [Plural API reference](https://developer.pinelabsonline.com/)
- **Security:** report vulnerabilities to [security@pinelabs.com](mailto:security@pinelabs.com)
- **Support:** [developer-tools@pinelabs.com](mailto:developer-tools@pinelabs.com)

### License

Apache 2.0.
