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 — 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.jsonin CI — you don't have that file in CI. UsePLUGIPAY_API_KEYenv 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
- API reference — the HTTP API behind every command.
- Idempotency — the safety mechanism for retries.
- Rate limits — how to stay under them in batched scripts.