CLI
Forward live events to localhost, fire test events, and hit any route from your terminal.
A single static Go binary. It speaks BillKit’s own API, auth and BillKit-Signature scheme, so what you see locally is what your server will see.
New to it? Develop locally with the CLI walks through install, login and the listen loop end to end. This page is the reference for the command surface.
Install
Homebrew is the shortest route on macOS, and it keeps the CLI up to date alongside everything else you have installed.
The cask is macOS only, and the macOS builds are Developer ID signed and notarized by Apple, so Gatekeeper accepts them with no xattr incantation.
On Linux, and on macOS without Homebrew, the install script picks the right build for your OS and architecture and drops it in /usr/local/bin. Override the destination with BILLKIT_INSTALL_DIR, or pin a release with BILLKIT_VERSION.
On Windows, download the build yourself. Every release on the releases page carries a .zip for windows_amd64 and windows_arm64, plus .tar.gz archives for macOS and Linux and a checksums.txt to check them against. Unpack it and put billkit.exe on your PATH.
If you already have a Go toolchain, you can build it from source instead.
Go names the binary after the package directory, which is cmd/billkit, so it arrives as billkit like every other install route.
Log in
billkit login # prompts for a bk_test_… or bk_live_… key
billkit config list # the profiles, with * on the default
billkit config use live # switch the default, keeping both keys
billkit logout # remove the default profile, or --profile NAME / --all The prompt does not echo the key. Echo is only suppressed when stdin is a terminal, so a piped key, as in cat key.txt | billkit login, still works.
Keys go in ~/.billkit/config.json, and the profile is chosen by the key prefix, so test and live keys never share a slot. The file is rewritten atomically on every save. On macOS and Linux it is mode 0600, re-tightened each time, and a config anyone else on the machine can read is called out on stderr. On Windows a file mode means nothing, so the protection is the permissions on your user profile folder, and the CLI tells you if BILLKIT_CONFIG_HOME has moved the file outside it.
The profile you just logged into becomes the default, so the mode login reports is the mode your next command uses. billkit config use <profile> moves the default later without deleting credentials.
Credentials in a script
Every command reads BILLKIT_API_KEY, BILLKIT_BASE_URL and BILLKIT_PROFILE, so a CI job needs no config file and no prompt.
export BILLKIT_API_KEY=bk_test_...
billkit api GET /v1/customers Each variable is the same setting as the flag of that name: --api-key, --base-url, --profile. The order is flag, then environment, then the stored profile. A flag is what you typed on this invocation, so it overrides a variable the shell has been carrying since login.
Prefer the variable to —api-key for anything live. A command line is readable by
other processes while it runs, CI logs echo it, and your shell records it in its history file.
A variable is none of those.
A key from the environment gets no more trust than one from a flag: both are checked before a request is made, so a live key aimed at a plain http host is refused either way.
A live key is only sent over https. Plain http is accepted for a loopback host with a test key, which covers local mocks and integration tests, and refused anywhere else. A CLI pointed at a host other than api.billkit.eu announces that on stderr before it sends.
Forward webhooks to localhost
This is the reason the CLI exists. No tunnel, no ngrok.
listen opens a live event stream and POSTs each event to your local URL, signed with a secret it generates and prints on start.
> Your webhook signing secret is bkwhsec_… Set that as your app’s webhook secret and verify with any BillKit SDK. Narrow the stream with --events customer.created,subscription.updated, or add --print-json to see each event in the terminal too. Under --print-json, stdout carries the JSON and nothing else, so you can pipe it.
The stream reconnects itself, backing off further after each failed attempt. Events created while it was down are read back out of the event log and forwarded before it resumes, in order and without duplicates, so a reconnect is not a hole in your testing. When a gap cannot be replayed the CLI says so and points at billkit events list rather than staying quiet. A connection that sends nothing at all for 45 seconds is treated as dead and re-dialled, since the server sends a keep-alive every 15.
A key the server rejects ends the command. A 401 or a 403 is not retried, the CLI names what to change, and the process exits non-zero, so a CI step that wraps listen fails instead of passing on a connection it never made.
Fire a test event
billkit trigger customer.created # makes the real test-mode API call
billkit trigger # list the supported events Three events can be triggered: customer.created, customer.updated and product.created.
trigger is not a fixture generator. It performs the real test-mode call, so the event you receive is one the API
actually emitted with a real object behind it. That is also why the list is short: anything that depends on a
settled payment, such as invoice.paid or the subscription.* events, needs a real charge and cannot be faked
from here. Every event listed here reaches a running listen, so a trigger that forwards nothing means something
is wrong with your setup, not with the list.
Refunds and one-off charges
billkit refunds create --payment pay_9XKp2vQ1 --amount 500 --reason "duplicate"
billkit refunds create --one-shot osp_9XKp2vQ1 # full refund, omit --amount
billkit refunds list --limit 20
billkit refunds retrieve re_9XKp2vQ1
billkit checkout one-shot \
--customer cus_9XKp2vQ1 --amount 1999 --method ideal \
--success-url https://example.com/thanks
billkit checkout retrieve osp_9XKp2vQ1 refunds create takes exactly one of --payment, --one-shot or --subscription, the last meaning that subscription’s most recent charge.
Every mutating call carries an Idempotency-Key, the same contract every BillKit SDK uses. Pass --idempotency-key to choose it, or let the CLI mint one. Either way it is printed on stderr before the request, because a key you never saw cannot make your retry safe.
> live mode (profile "live" at https://api.billkit.eu): refund 500 cents of pay_9XKp2vQ1
> Idempotency-Key: cli_8fQ2… (auto-generated; reuse it if you retry) Connection errors, timeouts, 429 and 5xx are retried twice with that same key, backing off with jitter and honouring Retry-After. A 4xx other than 429 is never retried. If the outcome is still unknown, the CLI says the refund may already exist, names the command that would show it, and prints the exact safe rerun.
Money-moving calls give the API 80 seconds rather than the 30 reads get, because the charge or refund makes a live Mollie round trip inside the request, and a client that gives up early is what creates the doubt in the first place.
Live mode needs confirmation. On a terminal the CLI asks. With no terminal, as in CI or behind a pipe, it refuses and
tells you to pass —yes, rather than blocking on a prompt nothing will answer. Test mode never asks.
Talk to the API
billkit events list --type customer.created
billkit events retrieve evt_9XKp2vQ1
# Escape hatch for any route:
billkit api GET /v1/customers
billkit api POST /v1/customers --data email=ada@example.com --data name=Ada billkit api keys anything that is not a GET and takes --idempotency-key too, so the escape hatch cannot become the one unsafe way to POST /v1/refunds.
Output and completion
JSON is pretty-printed and coloured when stdout is a terminal, and plain when you pipe it. Force it with --color always|never|auto; NO_COLOR is honoured. API errors render the envelope’s code, HTTP status, message, reason and param.
Only JSON goes to stdout. The mode banner, the idempotency key, the host notice and every warning go to stderr, so piped output stays machine-readable.
billkit completion zsh > "${fpath[1]}/_billkit" bash, zsh, fish and powershell are all supported.
BillKit