Developers · CLI

The getpay command line

getpayruns the same operations as the GetPay copilot and API, with your role's permissions. Money, legal and outbound actions always stop for an explicit confirmation.

Using ChatGPT or Claude instead of a terminal? Connect GetPay over MCP.

Install (Node.js 20+)

curl -fsSL https://getpay.cc/cli/install.sh | sh
getpay login
getpay help

Version 0.2.1. sha256 of getpay.mjs: 694b41a1268cc8ecf7c5ffda4429df096a0cefbc1cbde8fb06233490f6eccd68 The installer checks this hash before installing. Read the installer.

getpay runs GetPay from a terminal, a script or a coding agent. Every command maps to one GetPay operation (getpay invoices list runs invoices_list). The same operations power the copilot and the API, so the CLI can do what your role can do in the app, and nothing more.

This page is the source of getpay.cc/developers/cli.

Install

You need Node.js 20 or newer.

curl -fsSL https://getpay.cc/cli/install.sh | sh

The installer downloads https://getpay.cc/cli/getpay.mjs, checks it against https://getpay.cc/cli/getpay.mjs.sha256, and installs two files:

  • ~/.local/share/getpay/getpay.mjs: the CLI, one file with no dependencies;
  • ~/.local/bin/getpay: a small launcher.

If ~/.local/bin is not on your PATH, the installer tells you what to add. It never uses sudo and writes nothing outside your home directory.

The checksum file comes from the same server as the CLI, so it catches a broken or truncated download. If you have the expected hash from somewhere else, pin it with GETPAY_CLI_SHA256=<hex> and the installer refuses anything else. To install by hand:

curl -fsSLO https://getpay.cc/cli/getpay.mjs
curl -fsSL https://getpay.cc/cli/getpay.mjs.sha256 | shasum -a 256 -c -
node getpay.mjs version

To uninstall, run getpay logout, then delete ~/.local/bin/getpay, ~/.local/share/getpay and ~/.config/getpay.

Sign in

getpay login
getpay whoami

getpay login opens your browser at GetPay's sign-in and consent page. You choose the company the CLI may act in, and an owner can allow it to confirm money actions itself. The CLI then stores its tokens and prints who you are.

How sign-in works (OAuth 2.1):

  • The CLI finds GetPay's authorization server at /.well-known/oauth-authorization-server and signs in as GetPay's built-in first-party public client (gpapp_getpay-cli-first-party-v1). It does not register itself. The consent page shows it as a GetPay app, and it is the only kind of connection an owner or admin may allow to confirm money actions itself (MCP connectors and other apps never can).
  • It uses the authorization code flow with PKCE (S256). The redirect goes to a one-time server on http://127.0.0.1:<random port>/callback. That server listens on the loopback address only and checks the Host header. It ignores any request that does not carry this login's random state, closes after the first valid redirect, and gives up after 5 minutes (--timeout SECONDS changes this).
  • The browser does not open over SSH or on a machine without a display. Use getpay login --no-browser and open the printed link yourself. Because the redirect goes to 127.0.0.1, the browser must run on the same machine as the CLI. On a remote machine, use an API key instead.
  • The access token lasts 15 minutes. The CLI refreshes it without asking you, and one refresh runs at a time, even across several getpay processes. Each refresh token works only once. Two refreshes of the same token within 10 seconds (a race, a retry) get the same new tokens. If an old refresh token is presented again later, GetPay ends the whole sign-in. The CLI then says so, deletes the stored tokens, and getpay login starts a new sign-in. GetPay also allows at most 30 refreshes a minute per sign-in; over that the CLI gets a retryable error and keeps its tokens.
  • The tokens work only on GetPay's agent endpoint (/api/v1/ops). They do not work anywhere else in GetPay.

getpay logout revokes the sign-in at GetPay and deletes the stored tokens. You can also disconnect the CLI in Settings → Connected apps.

A profile holds one sign-in. Running getpay login again (also against another server) keeps the current sign-in untouched until the new one succeeds; then it revokes the old one at the server that issued it. A cancelled or failed login changes nothing.

getpay login --new-client registers a separate app through dynamic client registration instead of using the built-in client (for a GetPay server that does not have it yet). GetPay treats that app like any third-party app: it is shown as "Unverified app" and can never confirm money actions itself.

Where things are stored

PathContents
~/.config/getpay/config.jsonprofiles and the current profile
~/.config/getpay/credentials.jsontokens, per profile (secret)
~/.cache/getpay/the command catalog (no secrets)

The directory is created with mode 0700 and the files with 0600, written atomically. The CLI refuses to read a credentials file that is a symlink or owned by another user. If the file is readable by other users, the CLI resets it to 0600 and warns you. XDG_CONFIG_HOME and XDG_CACHE_HOME are honoured, and GETPAY_CONFIG_DIR / GETPAY_CACHE_DIR override both.

The CLI never prints a token. --debug traces each request, but it shows Authorization: Bearer [redacted] and never logs a request or response body.

Profiles and servers

SettingEffect
getpay profile listlist profiles (API URL, signed in or not)
getpay profile use <name> [--api-url URL]switch profile, optionally setting its server
--profile <name> or GETPAY_PROFILEuse a profile for one command
GETPAY_API_URLoverride the server (default https://getpay.cc)
getpay configshow the profile, server, and file locations in use

Each profile has its own sign-in. Tokens are sent only to the server that issued them: if GETPAY_API_URL points somewhere else, the CLI treats you as signed out there. Plain http:// is refused except for 127.0.0.1 (local development).

API keys (CI and servers)

export GETPAY_API_KEY=gp_live_...
getpay invoices list --status-filter OVERDUE

With GETPAY_API_KEY set, the CLI sends the key on every request, stores nothing, and does not need getpay login. Keys are created in Settings → API Keys with ops:* scopes. A key speaks for the REST surface, so it sees the operations its scopes allow. Confirming money actions with a key needs the ops:confirm scope.

ops:read lets a key run the read operations that any team member can run. The reads that need an owner or admin (billing-layer setup, Google Sheets reads, employee and payroll-form records, marketplace import status) also need the ops:read:admin scope, which you add to ops:read and choose deliberately when you create the key. Keys created before 2026-10-07 keep their old access until an owner reviews them; every key created from that date needs the extra scope. Without it, those operations answer SCOPE_REQUIRED and name the missing scope.

Running commands

getpay help                      # resources you can use
getpay help invoices             # verbs for one resource
getpay invoices list --help      # flags for one command
getpay invoices list --status-filter ISSUED,OVERDUE --limit 20
getpay invoices get 3f2b8c1e-5d4a-4e7b-9c1f-2a6d8e0b4c71 --select invoice_number,status,total
  • Help is built from the command catalog (GET /api/v1/ops), cached under ~/.cache/getpay and revalidated with its ETag. Signed in, you see the commands your account can run. Signed out, you see the public list.
  • The CLI checks the command line before sending anything, with the same parser GetPay's server uses. An unknown command or flag fails at once with a suggestion ("did you mean --limit?"). The server checks everything again.
  • Output is JSON when stdout is not a terminal, or with --json. On a terminal you get a table or a list of fields (--no-json forces it in a pipe).
  • --select a,b.c keeps only some fields, and --limit N keeps the first N rows.
  • --idempotency-key KEY makes a retried write safe on the operations that support it.
  • getpay run <operation_name> [flags] runs an operation by its registry name.
  • getpay ops prints the machine-readable catalog.

Some operations move money, file with LHDN, or send something to a customer. GetPay never runs these straight away. It answers with a single-use confirmation and a preview, and runs nothing until the confirmation is redeemed. The CLI never prints, logs or stores the confirmation token.

SituationWhat happens
On a terminalThe CLI shows the preview and asks Confirm? [y/N]. Only y or yes runs it; anything else exits 11 and nothing runs.
With --yesThe CLI confirms straight away, if GetPay allows your sign-in to confirm its own requests (an API key needs ops:confirm). If it does not, the CLI shows the refusal, prints the confirmation id and exits 10.
In a script or agent, without --yesThe CLI prints the confirmation id and exits 10.
Sign-in not allowed to confirmNo prompt. An owner or admin approves it in GetPay → Approvals within 10 minutes.

A browser sign-in may confirm its own requests only when an owner or admin allowed it at consent, and only with the built-in client (getpay whoami tells you). An API key needs the ops:confirm scope.

getpay approvals list               # pending confirmations you asked for
getpay confirm <confirmation-id> --wait   # wait for approval, then show the outcome
getpay confirm - < token.txt        # redeem a token you hold (read from stdin, not argv)

Storekeeper commands

Owners and admins can run the Storekeeper from the CLI: read the inbox, approve or decline chat orders, reply to a customer, take a conversation over or hand it back to the AI, and change settings, ordering, loyalty, FAQs and promos. Team members can run only the inbox and analytics reads. getpay help lists the storekeeper-* resources. The commands use the same server routes as the app, so your role and modules decide what runs, with one stricter rule: every Storekeeper read (the inbox and analytics reads, and the settings, ordering, loyalty, FAQ and promo reads) also needs the Invoices module, so anyone but an owner who is kept off Invoices is refused all of them (MODULE_ACCESS_DENIED), even where the app's own page would open. An admin kept off Invoices can still change settings, FAQs and promos; an owner is never refused for a module. Approving or declining an order, replying to a customer and changing the ordering mode always ask for confirmation, as described above, and so does every settings, loyalty, FAQ or promo create or update (customers see it); deleting an FAQ or promo does not. Results never contain an invoice pay link, and text written by a customer is marked untrusted. An API key cannot run these commands. The settings, ordering, loyalty, FAQ and promo commands are also in the copilot, and settings, FAQs and promos in the ChatGPT and Claude connector; the inbox commands are in neither.

getpay storekeeper-threads list --filter needs_approval
getpay storekeeper-orders approve 3f2b8c1e-5d4a-4e7b-9c1f-2a6d8e0b4c71   # asks you to confirm

Counter, shop and e-invoice commands

The in-store till, the shop page and the monthly LHDN consolidation run from the CLI with the same server routes, roles, modules and plan as the app (getpay help lists them):

ResourceCommandsAsks to confirm
counter-shiftslist, get (X report while open — owners and admins — or Z report once closed), open (--register-id or --cash-account-id, not both), closeclose always; open only with --float-account-code
counter-salescreate (catalogue products, tender, --expected-total); refund <invoice-id> --reason; void-last <invoice-id> --reason (the open shift's latest sale only)always (they book or reverse a paid sale)
counter-outletslist (outlets, their registers and the plan capacity), create, update (rename, address, pause / resume with --is-active)never (plan-capped; pause, never delete; create returns the existing outlet of the same name)
counter-registerscreate (--outlet-id, --name, then --gl-account-code for a new drawer or --cash-account-id for an existing one), update (rename, pause / resume, move to another --outlet-id)never (create returns the existing register of the same name)
counter-staff-outletsget, set (--member-id, --outlet-ids '[...]': the whole set a cashier may sell at)never
counter-sales report--from YYYY-MM-DD --to YYYY-MM-DD [--outlet-id]: sales by outlet and register (owner / admin; JSON, the CSV is in the app)never
lhdn-outlet-settingsget, set (owner only: --lhdn-consolidation-grouping, --lhdn-supplier-address-source; the outlet address also needs --confirm-accountant-reviewed)set always
productsfind-by-code (--barcode, --sku or --code: the Counter scan box's lookup)never
shop-postslist, create, update, deletecreate and update (public)
shop-followers, shop-waveslist; waves also send (wave back) and dismisssend
loyalty-rewardsredeemalways
shop-customersdelete (forget a customer)always
seller-listingsget, update (the pending copy only; submitting stays in the app)never
channel-order-importslist (newest imports and their stores)never
consolidated-einvoiceslist, preview --period YYYY-MM, draft, delete-draft, submitdelete-draft and submit

Counter sales, refunds, voids, shift opens and closes take --idempotency-key: the same key replays the first result instead of selling, opening or closing twice (a key is generated when you leave it out). API keys cannot run Counter commands.

getpay counter-shifts list
getpay counter-sales create --shift-id <shift-id> --items '[{"product_id":12,"quantity":2}]' \
  --tender '{"method":"cash","tendered":50}' --expected-total 39.80 --idempotency-key till1-0001
getpay consolidated-einvoices preview --period 2026-09

Exit codes

Scripts can rely on these (getpay help exit-codes prints them):

CodeNameMeaning
0OKsuccess
1INTERNALunexpected CLI failure (please report it)
2USAGEthe command line did not parse
3TENANTcompany unresolved, not a member, or a company mismatch
4AUTHnot signed in, sign-in ended, or missing scope, role or plan
5VALIDATIONthe server rejected the arguments or the request state
6NOT_FOUNDthe record, operation or confirmation does not exist
7OPERATIONALnetwork or server failure. After a write, the outcome may be unknown, so check before retrying
10CONFIRMATION_REQUIREDnot executed: waiting for confirmation
11CONFIRMATION_DECLINEDnot executed: you answered no

Codes 3 to 7 match accounting/cli.py and the code field of GetPay's API errors. In JSON mode an error is printed to stdout as {"error": {"code", "reason", "message", "fix", "exit_code"}}.

<!-- Maintainer notes (not rendered on the public page): docs/cli-maintainers.md -->