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-serverand 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 theHostheader. It ignores any request that does not carry this login's randomstate, closes after the first valid redirect, and gives up after 5 minutes (--timeout SECONDSchanges this). - The browser does not open over SSH or on a machine without a display. Use
getpay login --no-browserand 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
getpayprocesses. 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, andgetpay loginstarts 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
| Path | Contents |
|---|---|
~/.config/getpay/config.json | profiles and the current profile |
~/.config/getpay/credentials.json | tokens, 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
| Setting | Effect |
|---|---|
getpay profile list | list profiles (API URL, signed in or not) |
getpay profile use <name> [--api-url URL] | switch profile, optionally setting its server |
--profile <name> or GETPAY_PROFILE | use a profile for one command |
GETPAY_API_URL | override the server (default https://getpay.cc) |
getpay config | show 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/getpayand 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-jsonforces it in a pipe). --select a,b.ckeeps only some fields, and--limit Nkeeps the first N rows.--idempotency-key KEYmakes a retried write safe on the operations that support it.getpay run <operation_name> [flags]runs an operation by its registry name.getpay opsprints the machine-readable catalog.
Money, legal and outbound actions
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.
| Situation | What happens |
|---|---|
| On a terminal | The CLI shows the preview and asks Confirm? [y/N]. Only y or yes runs it; anything else exits 11 and nothing runs. |
With --yes | The 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 --yes | The CLI prints the confirmation id and exits 10. |
| Sign-in not allowed to confirm | No 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):
| Resource | Commands | Asks to confirm |
|---|---|---|
counter-shifts | list, get (X report while open — owners and admins — or Z report once closed), open (--register-id or --cash-account-id, not both), close | close always; open only with --float-account-code |
counter-sales | create (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-outlets | list (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-registers | create (--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-outlets | get, 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-settings | get, set (owner only: --lhdn-consolidation-grouping, --lhdn-supplier-address-source; the outlet address also needs --confirm-accountant-reviewed) | set always |
products | find-by-code (--barcode, --sku or --code: the Counter scan box's lookup) | never |
shop-posts | list, create, update, delete | create and update (public) |
shop-followers, shop-waves | list; waves also send (wave back) and dismiss | send |
loyalty-rewards | redeem | always |
shop-customers | delete (forget a customer) | always |
seller-listings | get, update (the pending copy only; submitting stays in the app) | never |
channel-order-imports | list (newest imports and their stores) | never |
consolidated-einvoices | list, preview --period YYYY-MM, draft, delete-draft, submit | delete-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):
| Code | Name | Meaning |
|---|---|---|
| 0 | OK | success |
| 1 | INTERNAL | unexpected CLI failure (please report it) |
| 2 | USAGE | the command line did not parse |
| 3 | TENANT | company unresolved, not a member, or a company mismatch |
| 4 | AUTH | not signed in, sign-in ended, or missing scope, role or plan |
| 5 | VALIDATION | the server rejected the arguments or the request state |
| 6 | NOT_FOUND | the record, operation or confirmation does not exist |
| 7 | OPERATIONAL | network or server failure. After a write, the outcome may be unknown, so check before retrying |
| 10 | CONFIRMATION_REQUIRED | not executed: waiting for confirmation |
| 11 | CONFIRMATION_DECLINED | not 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"}}.