> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qubesync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The qube CLI

> Log in once from a terminal, then get your app's keys, create connections, watch requests, drive the simulator and push workflows from a shell, a script, or an AI agent.

`qube` puts your QuBe Sync account in your terminal. It's the quickest way to set up a development
environment, and the safest way to let an AI coding agent work against your account: the agent can do
everything below without ever seeing a secret.

> [!NOTE]
>
> Run `qube version` to see what you have, and `brew upgrade qube` to update. Some of this page needs a
> recent CLI:
>
> * v0.3.0: `qube connections create ... --use`, `qube use --connection`, `qube qb RESOURCE create --example`
>   and `qube workflows install --replace`.
> * v0.5.0: `qube connections create --webhook-url`, `qube workflows decide` and `search`, and `qube check`.
> * v0.6.0: `qube skills install`, and connection events in `qube listen`.
> * v0.7.0: `qube simulator faults --stall`.

## Install

```bash Homebrew theme={null}
brew install qubeintegrations/tap/qube
```

```bash Go theme={null}
go install github.com/qubeintegrations/qube-cli/cmd/qube@latest   # Go 1.16 or newer
```

Or download the archive for macOS, Linux or Windows (amd64 and arm64) from the
[releases page](https://github.com/qubeintegrations/qube-cli/releases), unpack it, and put `qube` on your
`PATH`. `qube version` prints what you have; `eval "$(qube completion zsh)"` (or `bash`, `fish`) adds tab
completion.

## Log in

```bash theme={null}
qube login
```

`qube login` shows a short code and opens the dashboard. You confirm the code there, signed in as yourself,
and choose what the session may reach (**sandbox apps only** by default) and whether it may change anything
(**read and write**, or **read-only**). The terminal then receives a session token, stored in your config
directory with owner-only permissions. Nothing is typed into the terminal and nothing is pasted into a chat.

`qube login --read-only` asks for a read-only session: the approval page preselects it. See
[Sessions, scope and access](#sessions-scope-and-access).

`qube status` shows which host, user and app the CLI is pointed at, and when the session expires, without a
network call. `qube whoami` asks the server to confirm it.

## Set up a project in three commands

```bash theme={null}
qube use "My App Dev"                                     # the app the commands below act as
qube env --write .env                                     # QUBE_URL, QUBE_API_KEY, QUBE_WEBHOOK_SECRET
qube connections create --simulated --name "Local dev" --use   # a fake QuickBooks, connected at once
```

`qube env --write` names the variables it wrote and prints nothing of their values. `qube env --print`
prints them in full as `export` lines, if you want to see them.

QuBe Sync keeps only a hash of each API key, so `qube env` makes a new key for your session, listed on
the app's page as `qube CLI: <session name>`. Running it again in the same session replaces that key, and
the one written before stops working. Only an organization's owners can run it: a member's session gets
`403 owner_only`.

If the file already exists, `--write` changes only its `QUBE_*` lines: those already there are updated in
place, keeping any `export` in front, the missing ones are added at the end, and every other line stays
as it was. If `.env` is a symbolic link, the file it points to is the one updated.

`--use` makes the new connection the default, so commands that act on a connection don't need its id:

```bash theme={null}
qube qb customers list --max-returned 5 --wait    # on "Local dev"
qube requests list --state error
qube use --connection Riverbend                   # switch, by id or name; `none` forgets it
qube requests list --connection <id>              # another connection, this once
```

`qube connections list` marks the default with `*`. Deleting a connection always needs it named.

## Commands

> [accordion] Apps and sessions
>
> ```bash theme={null}
> qube apps                       # the apps this session may act on
> qube use "My App Dev"           # the default app
> qube sessions                   # your CLI sessions on this host
> qube sessions revoke <id>       # end one
> qube logout                     # end this one
> ```

> [accordion] Connections and requests
>
> ```bash theme={null}
> qube connections list
> qube connections create --simulated --name "Local dev"
> qube connections create --name "Riverbend" --webhook-url https://your-app.example/webhooks/qube/connection/42
> qube connections show <connection>
> qube connections update <connection> --name "Riverbend Electric"   # --webhook-url "" stops its events
> qube connections onboarding-url <connection>              # a fresh link; the old one stops working
> qube connections qwc <connection> --output riverbend.qwc  # the file the Web Connector adds
> qube connections password <connection>                    # a new Web Connector password; --stdin reads it from standard input
> qube connections delete <connection>                      # asks first; --yes in a script
> qube connections name-index <connection> --webhook-url https://your-app.example/webhooks/qube   # build it before the first match
> qube match <connection> customer "ACME, Inc." --email ap@acme.example   # the records a name could mean
> qube match <connection> item Consulting --exact-only      # exact or none, never a fuzzy match
> qube requests list <connection> --state error --page-size 50   # 20 per page by default
> qube requests list <connection> --webhook-state failed --search Riverbend --sort updated_at --sort-direction asc
> qube requests show <connection> <id>                      # one request, as JSON
> qube requests pages <connection> <id>                     # every page of an iterated query
> qube requests tail <connection>                           # watch requests as they're answered
> qube requests discard <connection> <id>                   # withdraw one the Web Connector hasn't taken
> ```
>
> `--webhook-url` on `connections create` or `update` is sent the connection's own events:
> `connection.connected`, `setup_cancelled`, `stalled` and `resumed` ([Connection events](https://docs.qubesync.com/webhooks.md#connection-events)).
>
> `qube match` asks a realtime or simulated connection's name index which records a name could mean
> ([Match names](https://docs.qubesync.com/matching.md)). A customer, vendor, employee or other name can be matched on more than
> its name: `--account-number`, `--email`, `--phone` and `--postal-code` can make a strong match, and
> `--city` and `--company-name` rank the candidates. `--parent` matches under a parent (a job under a
> customer), `--min-score` drops weaker candidates, and `qube match -h` lists the kinds and flags. The first match on a connection starts
> building its index and is refused until it's in (exit `1`); `qube connections name-index` starts the
> build ahead of time, and `--webhook-url` is told when it's ready.

> [accordion] QuickBooks operations
>
> ```bash theme={null}
> qube qb                                                   # every resource and its verbs
> qube qb customers                                         # one resource's operations
> qube qb customers list --help                             # its flags, their rules, an example
> qube qb customers list <connection> --name-starts-with North --max-returned 5 --wait
> qube qb customers create <connection> --name "Northwind Builders" --bill-address '{"city": "Austin"}'
> qube qb invoices create --example > invoice.json          # the API's example body, to edit
> qube qb invoices create <connection> --data @invoice.json
> qube qb txn-void execute <connection> --txn-id 1A2B-3C --txn-void-type Invoice
> qube qb items list <connection> --wait    # every page, 100 items at a time
> ```
>
> `qube qb` runs every QuickBooks operation in the [API reference](https://docs.qubesync.com/api-reference/introduction.md). It
> reads them from the API's own OpenAPI document, so the CLI offers the flags and body fields your
> host has. Query parameters and the body's top-level fields are flags (`max_returned` is
> `--max-returned`), and `--data` takes a whole body as JSON, `@file` or `-` for standard input.
> Each operation queues a request. `--wait` waits for QuickBooks' answer and prints it, every page
> of an iterated query, and exits `1` if it failed. It waits up to 10 minutes; use `--wait=30m` for
> longer. Otherwise pass `--webhook-url` to be sent the answer, or look it up later with
> `qube requests show`. `--help --json` prints an operation's full parameters and body schema.
>
> `--help` comes from the same document. Flags that exclude each other are listed together in an
> `Exactly one of:` block (or `At most one of:` when none is required), with `or:` between the
> alternatives and the flags that go together grouped under each. Help also shows which lists
> combine (an invoice's item and group lines, a journal entry's debit and credit lines), and which
> keys of an object are required. Help ends
> with an example built from the operation's request example in the
> [API reference](https://docs.qubesync.com/api-reference/introduction.md), and `--example` prints that body as JSON for you
> to edit and send with `--data @file`. Tab completion offers each operation's flags, and the
> values of a flag that has a fixed list.

> [accordion] The simulator
>
> ```bash theme={null}
> qube simulator show <connection>                  # the fake company file and its faults
> qube simulator faults <connection> --next 3100    # one-shot: xml | 3100 | 3120 | 3140 | 3180 | 3200 | ok
> qube simulator faults <connection> --next lost    # one-shot: the next add, change or delete is applied, then its answer is lost; --next ok clears it
> qube simulator faults <connection> --qb closed    # sticky: closed | modal | mismatch | unexpected | ok
> qube simulator faults <connection> --latency 5000 # milliseconds QuickBooks "thinks" per answer
> qube simulator faults <connection> --stall not_checking_in  # sticky: not_checking_in | quickbooks_unavailable | off
> qube simulator sync <connection>                  # run the fake Web Connector now
> qube simulator reset <connection>                 # back to the seeded company, faults cleared
> ```
>
> `--stall` stalls the connection, so you get `connection.stalled` once a request waits, and
> `--stall off` sends `connection.resumed` ([Test your stall handling](https://docs.qubesync.com/simulator.md#test-your-stall-handling)).
> See [Test without QuickBooks](https://docs.qubesync.com/simulator.md).

> [accordion] Workflows
>
> ```bash theme={null}
> qube workflows templates                        # the shipped templates
> qube workflows templates add_transaction --usage   # how to call one before it's installed, as Markdown (--json: the contract)
> qube workflows install add_transaction --publish   # --as NEW_KEY installs a second copy
> qube workflows list                             # TEMPLATE: the template each came from, and whether it changed since
> qube workflows show add_transaction
> qube workflows validate ./chart.json            # check a chart without saving it; whether what it calls exists is checked on push
> qube workflows push ./chart.json --publish --notes "Handle vendors too"   # saved under the chart's top-level "key", which it must have
> qube workflows publish add_transaction --notes "Handle vendors too"
> qube workflows versions add_transaction
> qube workflows usage add_transaction                 # how to call it, as Markdown
> qube workflows usage --all                           # every workflow's, with --- between them (--json: an array of {key, usage})
> qube workflows schema                           # the chart JSON Schema
> qube workflows run add_transaction --connection <id> --input @order.json \
>   --wait                                        # until it ends or needs a decision; --version published|working|N on sandbox; --webhook-url U
> qube workflows runs <connection>                # runs on a connection; --state, --outcome and --limit N filter them
> qube workflows runs <connection> <run> --events # one run's event log; --after SEQ for the next page
> qube workflows decide <connection> <run> --choice use:1   # answer a waiting run's question by a choice's id
> qube workflows search <connection> <run> --field list_id "Lone Star"   # the records a record field can take
> qube workflows decide <connection> <run> --choice use --data '{"list_id": "80000004-1234567890"}'   # one of them
> qube workflows cancel <connection> <run> --reason "Wrong customer"
> qube workflows delete-run <connection> <run>    # an ended run and its history
> qube workflows unpublish add_transaction
> qube workflows delete add_transaction
> ```
>
> If the app already has a workflow at that key, the install is refused with `409` (`key_taken`), and so
> is a second `POST /workflow-templates/{key}/install`. `--replace` (`"replace": true` in the API)
> installs again over it: the workflow, and each one it calls that the app already has, gets the
> template's current chart, earlier versions kept, so a setup script can run the same install every
> time. `--as` (`as` in the API) installs a second copy under another key instead, and
> `qube workflows push` replaces a workflow with a chart file of your own.
>
> A template can change after you install it. `qube workflows list` then says `update available` in
> its TEMPLATE column (`update available (edited)` when you've changed the workflow too), and prints
> the `install --replace` command that takes the current version. For an edited workflow that command
> replaces your edits.
>
> Runs start only on a realtime or simulated connection: on a `standard` one, `qube workflows run` is
> refused with `422` `realtime_required` ([Workflows run on realtime connections](https://docs.qubesync.com/workflows.md#workflows-run-on-realtime-connections)).
>
> See [Workflows](https://docs.qubesync.com/workflows.md).

> [accordion] Agent skills
>
> ```bash theme={null}
> qube skills install                     # the integration audit, into ./.claude/skills/qube-sync-audit/SKILL.md
> qube skills install --dir ../my-app     # into another repo
> ```
>
> `qube skills install [NAME]` saves a skill QuBe Sync publishes where Claude Code finds it. `NAME`
> defaults to `qube-sync-audit`, the [integration audit](https://docs.qubesync.com/integration-checklist.md). It needs no login, and
> running it again updates the file.

> [accordion] Anything else
>
> ```bash theme={null}
> qube api GET "/connections/<id>/customers?max_returned=5"
> qube api POST /connections/<id>/customers --data '{"name": "Northwind Builders"}'
> qube api PUT /connections/<id>/customers --data @update.json
> ```
>
> `qube api` calls any v2 path with the current app's key. A path that doesn't start with `/api/` is
> a v2 path. `--data` takes JSON, `@file` or `-` for standard input.

`qube help` lists every command and flag. Every command also takes:

| Flag | |
| - | - |
| `--json` | Machine-readable output. |
| `--app` | Act as a specific app, this once. |
| `--connection` | The connection to act on, this once, when a command doesn't name one. |
| `--timeout` | Seconds (or `2m`). Default 60. |
| `--yes` | Answer yes to a confirmation, for scripts. See [Production apps](#production-apps). |

Exit codes: `0` ok, `1` the command failed, `2` wrong usage, `130` interrupted.

> [!NOTE]
>
> `--wait` and `qube requests tail` poll, so you can see answers from a terminal or a script. They're a
> developer convenience. Your integration should pass a `webhook_url` with every request and be told
> when it's answered ([Webhooks](https://docs.qubesync.com/webhooks.md)).

## Receive webhooks locally

`qube listen` gives you a `webhook_url` that reaches the app on your laptop, with no tunnel. It works for
sandbox apps:

```bash theme={null}
qube listen --forward-to http://localhost:3000/webhooks/qube --write-env .env
```

```
Webhook URL for My App Dev (the same every time on this machine):
  https://qubesync.com/relay/rl_4f7k2m9q8t3w6x1z5c0v7b2n4h
Forwarding to http://localhost:3000/webhooks/qube. Ctrl-C to stop.
14:02:11  request 1f0c2a3b…  response_received  → POST /webhooks/qube  200  41 ms
14:02:19  workflow_run.notification  customers_page #3  → POST /webhooks/qube  200  58 ms
14:02:40  connection.stalled  connection 3f2a9c1e…  not_checking_in  → POST /webhooks/qube/connection/42  200  33 ms
14:03:02  request 1f0c2a3b…  response_received  → POST /webhooks/qube  500  12 ms  (QuBe will retry)
```

Pass the URL it prints as the `webhook_url` of your requests and workflow runs, and every delivery to it
is sent on to the `--forward-to` URL: the body and `X-QUBE-Signature` byte for byte, so your app
[verifies the signature](https://docs.qubesync.com/webhooks.md#securing-your-endpoints) as it will in production. A path or query you
add to the URL is added to the `--forward-to` URL too. Your app's answer is the delivery's outcome, so a
non-`2xx` is retried and the dashboard shows each attempt, as for any `webhook_url`.

| Flag | |
| - | - |
| `--forward-to URL` | Required. Where deliveries go; `localhost:3000` means `http://localhost:3000`. |
| `--write-env FILE` | Writes the URL into `FILE` as `QUBE_WEBHOOK_URL`, changing no other line. Without it, `qube listen` prints the line to add when `./.env` lacks it. |
| `--forget` | Asks, then deletes this machine's URL for the app, so the next `qube listen` gets a new one. With `--forward-to`, it then listens on the new URL. |
| `--json` | One JSON line with the app and URL, then one per delivery: `at`, `ms`, `path`, `summary`, and `status` or `error`. |

* **One URL per machine.** The URL stays the same for an app on this machine, so it goes into `.env` once.
  The machine is identified by a random id in the CLI's config folder, shared by every config on it. You can
  have URLs on up to 10 machines per app; an 11th takes over the one unused longest, which then stops
  working, and `qube listen` says so.
* **One listener per URL.** Starting `qube listen` again, in another terminal, takes over: the older one stops
  and says why.
* **When it isn't running,** deliveries fail and are retried for about 8 hours like any other, and **Resend
  webhook** in the dashboard sends one again. It reconnects by itself when the network drops.
* Ctrl-C stops it (exit `130`).

See [Receive webhooks in development](https://docs.qubesync.com/ngrok.md) for a tunnel instead.

## Check your webhook endpoint

`qube check webhooks` sends signed test deliveries to your own webhook endpoint and grades the answers, so you
find out before QuBe Sync does whether it [verifies signatures](https://docs.qubesync.com/webhooks.md#securing-your-endpoints), refuses
what it should and answers in time. Run it with your app running on your machine:

```bash theme={null}
qube check webhooks http://localhost:3000/webhooks/qube
```

> [!NOTE]
>
> `qube check` needs v0.5.0 or later of the CLI. On an older one, `qube check` answers `unknown command`:
> run `qube version`, and `brew upgrade qube` to update.

The URL is the base your app builds each webhook URL from, `QUBE_WEBHOOK_URL`. The CLI signs with the app's
webhook secret, fetched the way `qube env` fetches it and kept in memory. It never prints, logs or writes the
secret, or anything your app answers. Each check is one request, and each sends only what your app must refuse
or can't route, so none reaches a real run or connection:

| Check | It sends | It passes when |
| - | - | - |
| `bad-signature-401` | A run event with a wrong signature, to `URL/runs/qube-check-1`. | Your app answers `401`. |
| `stale-timestamp-401` | A correct signature over a timestamp 10 minutes old, to `URL/runs/qube-check-2`. | `401`. |
| `unknown-operation-404` | A correct signature, to `URL/qube-check-unknown/1`. | `404`. |

```
CHECK                  RULE                    SENT                  EXPECTED  GOT         RESULT
bad-signature-401      verify-signature        wrong v1              401       401 in 3ms  pass
stale-timestamp-401    refuse-stale-timestamp  t 10 min old          401       200 in 3ms  FAIL
unknown-operation-404  fixed-operation-map     signed, unknown slug  404       404 in 3ms  pass

stale-timestamp-401: answered 200 to a correctly signed delivery whose timestamp is 10 minutes old. Refuse a `t` more than 5 minutes from now, or a captured delivery can be replayed.

2 passed, 1 failed, 0 skipped.
```

The deliveries are fake: their ids and references say `qube-check`, and no record of your app's matches them.
Every request carries an `X-QUBE-Check` header naming its check, for your logs. What a check can't send safely,
such as whether your app verifies the raw body or answers within 5 seconds, the
[audit skill](https://docs.qubesync.com/integration-checklist.md) checks in your code.

| Flag | |
| - | - |
| `--operation SLUG` | The segment your run webhook URLs start with after `URL`, when it isn't `runs` (the libraries' `URL/runs/<run id>`). |
| `--production` | Required when the current app is a production app. |
| `--yes` | Required when the URL isn't `localhost`, `127.0.0.1` or `[::1]`. |
| `--json` | The same report as data: each check's `result`, `expected`, `got`, `status` and `detail`, a `summary` and `ok`. |

* **Exit codes.** `1` when a check fails, `0` otherwise. A skipped check isn't a failure.
* **It never follows a redirect.** A redirect is reported as one, since QuBe Sync doesn't follow them either,
  and a request that gets no answer in 10 seconds fails.
* **It never sends to QuBe Sync.** A URL on `qubesync.com` is refused, and so is a `qube listen` URL: check the
  local URL you gave `--forward-to`.
* **It needs a read and write session.** A [read-only session](#sessions-scope-and-access) can't read the
  webhook secret, and neither can an act-as grant.

## Production apps

In a production app, every request that changes something asks before it's sent, and names the app.
That covers every `qube qb` operation except `list`, since each one writes to the customer's real
company file. It also covers creating or changing a connection (its password, onboarding link and
name index too), discarding a request, driving the simulator, every workflow change, run or answer, and
`qube api` with any method but `GET`. Reads never ask. Removing something (a connection, a workflow,
a run, a `qube listen` URL) asks in any app.

A script answers with `--yes`. Without a terminal, or under `--json`, a request that would ask is refused
until `--yes` is given, so nothing reaches a production company file by accident.

## Let an AI agent use it

Log in yourself, choosing the default **sandbox** scope, then let your coding agent run `qube`. It can
write your `.env`, create simulated connections, inject faults, push workflows and call the API, but it
never sees the session token or your production apps, and you can revoke its access at any moment. For
an agent that should only look (reading your data, running QuickBooks queries, checking charts), approve
a **read-only** session instead: the server refuses anything else.
Combined with the [MCP server](https://docs.qubesync.com/mcp-server.md), an agent can explore QuickBooks operations, build requests
and test them end to end on its own.

## Sessions, scope and access

A session belongs to you, in one organization.

| Approved with | It can | It can't |
| - | - | - |
| **Sandbox** scope (the default) | List the organization's sandbox apps, get their API keys and webhook secrets (owners only), and act on their connections. | See production apps, or change billing, members or your account. |
| **Every app** scope | The same, in production apps too. | Change billing, members or your account. |
| **Read-only** access | List and show what its scope reaches, run QuickBooks queries, [match names](https://docs.qubesync.com/matching.md), validate charts, download a connection's QWC file and [receive webhooks](#receive-webhooks-locally) with `qube listen`. | Create, change, delete, discard, run or answer anything, or read an app's API key or webhook secret, so `qube env` isn't available to it. |

The server enforces read-only access. `qube status` and `qube sessions` show each session's access.

Sessions expire after 30 days. `qube logout`, `qube sessions revoke <id>`, or **CLI sessions** in the
dashboard end one sooner, and the CLI stops working at once: it calls the API with the session's own
token, never the app's API key. The token is only ever sent to `qubesync.com`, the host that issued it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.