Scripting & automation

The Plugipay CLI is designed to be scriptable. JSON output, predictable exit codes, idempotency keys, and pipes-and-filters-friendly commands make it work well in CI, cron jobs, and one-off shell scripts.

This page covers the patterns you'll use.

Output for machines

Pass --json to get structured output:

plugipay customers list --json

Pipe through jq:

# Just the IDs
plugipay customers list --json | jq -r '.data[].id'

# Filter
plugipay customers list --json | jq -r '.data[] | select(.email | contains("@gmail.com")) | .id'

# Count
plugipay payments list --status succeeded --since 7d --json | jq '.data | length'

# Total revenue (smallest unit)
plugipay payments list --status succeeded --since 7d --json | jq '[.data[].amount] | add'

For CSV (good for spreadsheets):

plugipay payments list --since 30d --output csv > payments.csv

For just IDs, one per line:

plugipay payments list --since 7d --output ids > payment-ids.txt

Exit codes

Exit Means
0 Success
1 Generic error (network, validation, unauthorized)
2 Bad invocation (missing args, unknown command)
3 Auth not configured
64 Idempotency conflict (same key, different params)

Branch on it in shell:

if ! plugipay refunds create --payment-id pay_xxx --reason fraudulent; then
  echo "refund failed; alerting..." >&2
  notify-slack "Refund of pay_xxx failed"
  exit 1
fi

Idempotency for re-runnable scripts

Without an idempotency key, re-running a script that creates payments will create new ones each time. With a key, Plugipay returns the same result.

plugipay refunds create \
  --payment-id pay_xxx \
  --reason duplicate \
  --idempotency-key "nightly-cleanup-$(date +%F)-pay_xxx"

The key needs to be unique per intended operation, but stable across retries. Common patterns:

  • <job-name>-<date>-<resource-id> — same job + same date + same target = same key
  • <git-sha>-<step> — deploy-time scripts keyed to the deploy
  • <request-id>-<action> — serving a single user request

See API → Idempotency for the underlying semantics (same key, same result, 24h window).

Batching with xargs

Apply an operation to many resources:

# Refund every payment that succeeded in the last hour
plugipay payments list --status succeeded --since 1h --output ids | \
  xargs -n1 -P4 -I{} plugipay refunds create \
    --payment-id {} \
    --reason fraudulent \
    --idempotency-key "incident-2026-05-12-{}"

The -P4 runs 4 in parallel. Don't go too high — you'll hit rate limits (20 writes/sec default).

Pagination

List commands return up to limit items per call (default 50, max 100). For "list everything", use --all:

plugipay customers list --all --json | jq '.data | length'

--all paginates under the hood. For very large workspaces, the result can be slow — consider filtering instead:

plugipay customers list --since 30d --all

Or iterate manually:

cursor=""
while true; do
  resp=$(plugipay customers list --limit 100 ${cursor:+--cursor $cursor} --json)
  echo "$resp" | jq -r '.data[].id'
  has_more=$(echo "$resp" | jq -r '.meta.page.hasMore')
  cursor=$(echo "$resp" | jq -r '.meta.page.nextCursor')
  [ "$has_more" = "true" ] || break
done

CI examples

Daily reconciliation cron

#!/bin/bash
# Run as a daily cron. Reports yesterday's payments to a Slack webhook.
set -euo pipefail

export PLUGIPAY_API_KEY="${PLUGIPAY_API_KEY_SECRET}"  # injected by CI

YESTERDAY=$(date -d "yesterday" +%F)
COUNT=$(plugipay payments list --since 1d --status succeeded --json | jq '.data | length')
TOTAL=$(plugipay payments list --since 1d --status succeeded --json | jq '[.data[].amount] | add')

curl -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"text\":\"$YESTERDAY: $COUNT payments, total $TOTAL IDR\"}"

Rotate API keys on deploy

#!/bin/bash
# On every prod deploy, create a fresh API key and revoke yesterday's.
set -euo pipefail

NEW_KEY=$(plugipay api-keys create \
  --name "prod-$(git rev-parse --short HEAD)" \
  --role full_access \
  --json | jq -r '.token')

# Push the new key to your secret store
aws ssm put-parameter --name /prod/plugipay/key --value "$NEW_KEY" --type SecureString --overwrite

# Revoke yesterday's keys (older than 24h)
plugipay api-keys list --json | \
  jq -r '.data[] | select(.createdAt < (now - 86400 | strftime("%Y-%m-%dT%H:%M:%SZ"))) | .id' | \
  xargs -n1 plugipay api-keys revoke

Bulk refund on incident

#!/bin/bash
# Customer support workflow: refund every payment from a specific customer
# in the last day with reason "fraudulent". Idempotent &mdash; safe to re-run.
set -euo pipefail

CUSTOMER_ID="${1:?usage: $0 <customer-id>}"

plugipay payments list \
  --customer-id "$CUSTOMER_ID" \
  --since 1d \
  --status succeeded \
  --output ids | \
  while read -r payment_id; do
    plugipay refunds create \
      --payment-id "$payment_id" \
      --reason fraudulent \
      --idempotency-key "incident-$(date +%F)-$payment_id"
    echo "Refunded $payment_id"
  done

Webhook testing locally

Use the CLI to forward production (or test-mode) webhooks to your local dev server:

# Terminal 1: your local server
npm run dev   # listens on :3000

# Terminal 2: forward webhooks
plugipay webhooks listen --forward-to http://localhost:3000/webhooks

The CLI maintains a long-lived WebSocket to Plugipay; every webhook your account receives gets forwarded to your localhost endpoint. No public URL needed, no ngrok config.

When you're done, Ctrl-C the listener — the webhook delivery resumes to your configured endpoint(s).

To trigger test events without an actual payment:

plugipay webhooks trigger --event payment.succeeded --to webhep_01HXX

You can also stream events live without a forwarder — see what's firing in real time:

plugipay events tail --type 'payment.*'

Watch a value change

# Watch a subscription status every 10 seconds
watch -n10 'plugipay subscriptions get sub_xxx --json | jq -r .status'

Debug with verbose mode

When a command behaves unexpectedly:

plugipay -v --json customers get cus_xxx

The -v prints the full request and response to stderr, including:

  • The exact URL
  • All headers (secret signature redacted)
  • HTTP status
  • X-Request-Id — include this in support tickets
> GET https://api.plugipay.com/v1/customers/cus_xxx
> Authorization: HMAC-SHA256 pk_test_AKIAxxxxx:[redacted]
> X-Plugipay-Timestamp: 1715526783
< 404 Not Found
< X-Request-Id: req_01Hxxxxx
< {"error":{"code":"NOT_FOUND","message":"customer cus_xxx not found"}}

CI patterns

GitHub Actions

- name: Backfill payments
  env:
    PLUGIPAY_API_KEY: ${{ secrets.PLUGIPAY_API_KEY }}
  run: |
    npm install -g @forjio/plugipay-cli
    plugipay payments list --since 7d --output csv > artifact-payments.csv

- uses: actions/upload-artifact@v4
  with:
    name: payments-${{ github.run_id }}
    path: artifact-payments.csv

GitLab CI

nightly-reconcile:
  schedule: "0 2 * * *"
  script:
    - npm install -g @forjio/plugipay-cli
    - plugipay payments list --since 1d --output csv > reconciled.csv
    - upload-to-s3 reconciled.csv

Docker

FROM node:20-alpine
RUN npm install -g @forjio/plugipay-cli
ENTRYPOINT ["plugipay"]

Then docker run -e PLUGIPAY_API_KEY=... plugipay customers list.

Common pitfalls

  • Forgetting idempotency keys — scripts that retry can cause duplicates. Pass an explicit key derived from your inputs.
  • Hitting rate limits in parallel xargs — cap parallelism at 4-8 for writes, higher for reads.
  • Reading credentials from ~/.plugipay/config.json in CI — you don't have that file in CI. Use PLUGIPAY_API_KEY env var instead.
  • Mixing test and live keys — the CLI doesn't warn about this; the API rejects mismatched IDs. Use separate profiles or env vars.

Next

Plugipay — Payments that don't tax your success