> ## 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.

# Integration checklist

> Every rule a QuBe Sync integration has to get right, and an audit skill that has your coding agent check your code against all of them.

A QuickBooks Desktop integration has more to get right than most: questions only your user can answer, ids that are unique only within one company file, and answers that can be lost on the way back from your customer's computer.

## Audit your code with your coding agent

**1. Download the audit skill**

It's one Markdown file: every rule below, with what to look for in your code and what getting it wrong
and right looks like, and the steps to check them. It holds no key and calls nothing. From your app's
repo, save it where your agent looks for skills:

> [tab] Codex, Cursor
>
> Both read skills from `.agents/skills/`:
>
> ```bash theme={null}
> curl -o .agents/skills/qube-sync-audit/SKILL.md --create-dirs https://qubesync.com/skills/qube-sync-audit/SKILL.md
> ```

> [tab] Claude Code
>
> ```bash theme={null}
> curl -o .claude/skills/qube-sync-audit/SKILL.md --create-dirs https://qubesync.com/skills/qube-sync-audit/SKILL.md
> ```

> [tab] Other agents
>
> Gemini CLI, GitHub Copilot and the rest can follow the file as a prompt, wherever you keep it:
>
> ```bash theme={null}
> curl -o qube-sync-audit/SKILL.md --create-dirs https://qubesync.com/skills/qube-sync-audit/SKILL.md
> ```

**2. Run it**

Ask your agent to audit your QuBe Sync integration: an agent that reads skills finds this one by its
description, and in Claude Code `/qube-sync-audit` runs it by name. For an agent without skills, say
"Follow qube-sync-audit/SKILL.md and audit this repo."

The agent only reads your code. It never opens your `.env` or prints a secret, and it changes nothing
until you ask. Its report lists each finding with its rule id, severity and `file:line`, critical first.

**3. Check your webhook endpoint live**

With your app running on your machine, the [CLI](https://docs.qubesync.com/cli.md) sends your webhook endpoint signed test deliveries
and grades its answers: a bad signature, an old timestamp, a path your app doesn't route. Each is a delivery
your app must refuse or can't route, so none reaches a real run or connection. The audit skill runs it too,
and folds what fails into its report:

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

Give it your app's local webhook URL, the one `qube listen --forward-to` forwards to, not the relay's.
When your run webhook URLs don't start with `.../webhooks/qube/runs/`, pass `--operation` with the segment
yours start with. The command needs the CLI release that has `qube check webhooks`.

Your agent can also read these rules, or one area's, from the [MCP server](https://docs.qubesync.com/mcp-server.md) with its
`get_integration_checklist` tool.

A Critical rule guards against data loss, a
security hole or another customer's data, and a Major one against
wrong behaviour your users see.

## Onboarding

Connecting a customer's QuickBooks: the connection, its webhook and its events.

### `connection-webhook-url`

Major Applies when the app connects its customers' QuickBooks (it
creates connections).

**Create each customer's connection with a `webhook_url`, so QuBe Sync can tell your app when
they're connected.**

The setup ends on the QuickBooks computer and never comes back to your app: the connection's
`webhook_url` hearing `connection.connected` is how the app knows. Without one, the app shows the
customer's QuickBooks as waiting forever, and nothing tells it when requests stall. See
[Add the "Connect to QuickBooks" button](https://docs.qubesync.com/onboarding.md#add-the-connect-to-quickbooks-button) and
[Connection events](https://docs.qubesync.com/webhooks.md#connection-events).

### `save-connection-id`

Major Applies when the app connects its customers' QuickBooks (it
creates connections).

**Save the connection's `id` on the record that keeps it (the customer's account or shop, say)
before you send them to its `links.onboarding`, and reuse it when they click Connect again.**

Every request and run goes out on that id, and the connection's events are matched to it. A
simulated connection is connected as it's made, so its `connection.connected` can arrive before the
create call returns. A customer who comes back (a link past its 48 hours, QuickBooks on a new
computer) needs the same connection's link again, not a second connection. See
[Add the "Connect to QuickBooks" button](https://docs.qubesync.com/onboarding.md#add-the-connect-to-quickbooks-button) and
[Setting a connection up again](https://docs.qubesync.com/onboarding.md#setting-a-connection-up-again).

### `workflow-connections-realtime`

Major Applies when the app creates connections and starts workflow
runs on them.

**Create the connections your app runs workflows on as `realtime` (`"type": "realtime"` on
`POST /api/v2/connections`), and handle a start answered `422` with `errors.code`
`realtime_required`.**

Workflows run on realtime and simulated connections only: a start on a standard connection is
refused with nothing stored, so its customer never gets the run. A connection's type can't be
changed through the API, only upgraded from its page in the dashboard, and once a downgrade takes
effect new runs are refused (runs already going finish). The same call with the same
`Idempotency-Key` starts the run once the connection is upgraded. See
[Workflows run on realtime connections](https://docs.qubesync.com/workflows.md#workflows-run-on-realtime-connections) and
[When a workflow run is refused](https://docs.qubesync.com/error_handling.md#when-a-workflow-run-is-refused).

### `connected-by-webhook`

Major Applies when the app connects its customers' QuickBooks (it
creates connections).

**Show a customer's QuickBooks as connected only when a signed `connection.connected` arrives for
their connection, never when a browser comes back.**

The onboarding page doesn't send your customer back to your app: the setup ends on the QuickBooks
computer, which usually isn't signed in to your app, and the v2 API refuses a `redirect_url`.
Nothing has to wait for it either: runs started before the Web Connector connects wait in the queue.
See [Add the "Connect to QuickBooks" button](https://docs.qubesync.com/onboarding.md#add-the-connect-to-quickbooks-button) and
[Connection events](https://docs.qubesync.com/webhooks.md#connection-events).

### `connection-event-for-saved-connection`

Critical Applies when the app takes connection events.

**Apply a connection event only to the record whose saved connection id is the event's
`connection.id`, and answer it `2xx` either way.**

The signature covers the body, not the URL. A URL that names the record keeping the connection (the
`qube_rails` engine's `/webhooks/qube/connections/<id>`) is only a pointer, so a signed event posted
to another record's URL would change that record. See
[Connection events](https://docs.qubesync.com/webhooks.md#connection-events) and
[Add the "Connect to QuickBooks" button](https://docs.qubesync.com/onboarding.md#add-the-connect-to-quickbooks-button).

### `connection-event-newer-revision`

Major Applies when the app takes connection events.

**Apply a connection event only when its `revision` is newer than the last you applied for that
connection, and save the revision with the change.**

Deliveries can come out of order, a retried `connection.stalled` after its `connection.resumed` say.
The `revision` counts up across all four events, per connection, and a retry of one event has the
same `delivery_id` and `revision`. See [Connection events](https://docs.qubesync.com/webhooks.md#connection-events).

### `show-stall-reason`

Minor Applies when the app takes connection events.

**Show your customer what `connection.stall_reason` says to do, and clear it on
`connection.resumed`.**

`not_checking_in` means the Web Connector hasn't checked in when it was due: turn on the QuickBooks
computer and check the Web Connector. `quickbooks_unavailable` means it is checking in but can't
reach QuickBooks: open QuickBooks with the company file. A stall can change its reason and send
`connection.stalled` again, so show the newest. See [Connection events](https://docs.qubesync.com/webhooks.md#connection-events)
and [Keep an eye on the connection](https://docs.qubesync.com/onboarding.md#keep-an-eye-on-the-connection).

### `setup-cancelled-offers-connect`

Minor Applies when the app takes connection events.

**On `connection.setup_cancelled`, offer the Connect button again.**

Your customer chose Cancel setup on the onboarding page. The connection is unchanged and its
onboarding link still works, so Connect can take them back to the same one. See
[Connection events](https://docs.qubesync.com/webhooks.md#connection-events).

## Calling the API and starting runs

The API key, the calls that queue requests or start workflow runs, and what each one carries.

### `api-key-on-the-server`

Critical Applies to every integration.

**Call the API only from your server, with HTTP basic auth: the API key as the username and an empty
password, read from the environment.**

An app's key reaches every connection of that app. In a browser, a mobile app or the repo, anyone
who sees it can read and write your customers' company files. See
[Do it from your code](https://docs.qubesync.com/quickstart.md#do-it-from-your-code).

### `sandbox-production-apart`

Major Applies to every integration.

**Keep sandbox and production apart: each environment uses its own app's API key and webhook secret,
and only that app's connection ids.**

Your code doesn't change between the two; the API key, the webhook secret and the connection ids do.
A sandbox key can't reach production connections, and the other way round, so a connection id kept
under one app answers `404` to the other's key. Every webhook says which app sent it, in `app_id`
and `sandbox`. See [Launch checklist](https://docs.qubesync.com/going-live.md#launch-checklist).

### `reference-saved-before-start`

Major Applies when the app starts workflow runs.

**Start every run with a new `reference`, saved on your record before you send the start, and find
the record by it rather than by `run_id`.**

Any run webhook can arrive before the `201` that starts the run. Revisions count per run, so a
reference used for a second run lets the first run's webhooks be taken for the second's, and the
second's skipped against the first's. See [Workflow runs](https://docs.qubesync.com/webhooks.md#workflow-runs) and
[Good to know](https://docs.qubesync.com/workflows.md#good-to-know).

### `one-run-per-record-at-a-time`

Major Applies when the app starts workflow runs.

**While a record's last run of an operation hasn't ended, give back that run (resending its start
with the same `reference` if QuBe Sync never confirmed it) rather than starting another.**

A second click, a retried job or a page loaded twice would otherwise start a second run with a new
reference, which QuBe Sync can't tell from a new one: the invoice is added twice. See
[Good to know](https://docs.qubesync.com/workflows.md#good-to-know).

### `missing-records-in-adds`

Major Applies when the app starts `add_transaction` (or another
workflow that takes `adds`).

**Send each customer, vendor, employee or item your transaction names that QuickBooks may not have
yet as an entry of `adds`, built from your own record, until it's linked.**

`add_transaction` creates a missing record only from an add your app sent for it. A reference with
no entry is matched on its name alone, and when nothing matches, the run can only ask your user to
pick an existing record: there's no choice to create it. See
[Pick a template](https://docs.qubesync.com/workflows.md#pick-a-template).

### `webhook-url-per-run`

Minor Applies when the app starts workflow runs.

**Give each run a webhook URL that names it, such as `/webhooks/qube/runs/<id>` (a row written for
the run before it starts, which keeps its operation and your record), or route one shared endpoint
on `event` and `reference`.**

Each run takes its own `webhook_url`, so the URL can carry what the job needs: which class handles
the run and which record it's about. One shared URL works too, when its job routes on `event` first
and finds the record by `reference`. See [Handle it in a job](https://docs.qubesync.com/webhooks.md#handle-it-in-a-job) and
[Workflow runs](https://docs.qubesync.com/webhooks.md#workflow-runs).

### `idempotency-key`

Major Applies when the app queues requests or starts workflow runs.

**Make one `Idempotency-Key` for each call you mean to make once, from something new to that call (a
UUID, or the run's new `reference`), store it with the record before you send it, and resend it only
to retry that call.**

Every call queues a new request: two calls to create an invoice make two invoices, and starting a
run twice runs it twice. With the same key, a call sent again after a timeout gets back what the
first one queued (`Idempotent-Replayed: true`), and nothing new is queued. A key lasts until what it
created is purged (60 days after a request is answered or a run ends), and then frees up. See
[Retrying a call safely](https://docs.qubesync.com/making-requests.md#retrying-a-call-safely) and
[Good to know](https://docs.qubesync.com/workflows.md#good-to-know).

## Webhooks

The endpoint that takes QuBe Sync's deliveries, and the job that does the work.

### `verify-signature`

Critical Applies to every integration.

**Verify every delivery's `X-Qube-Signature` before you act on it, and answer `401` when no `v1` in
it matches.**

Anyone can POST to a public URL. The header carries a timestamp `t` and one `v1` for each active
webhook secret on your app, each the hex HMAC-SHA256 of `<t>.<raw body>` keyed with that secret. See
[Verify the signature](https://docs.qubesync.com/webhooks.md#verify-the-signature).

### `refuse-stale-timestamp`

Major Applies to every integration.

**Refuse a delivery whose `t` is more than five minutes from now, with `401`.**

It stops someone replaying a captured delivery. Every delivery, retries included, is signed afresh,
so a healthy receiver never sees an old one. See
[Verify the signature](https://docs.qubesync.com/webhooks.md#verify-the-signature).

### `verify-raw-body`

Major Applies to every integration.

**Verify the signature over the raw bytes you received, before anything parses the body.**

The signature is over the exact bytes QuBe Sync sent. Parsed and serialized again, JSON has other
spacing, key order and escapes, so real deliveries fail, and a parser that runs before the check
runs on bodies nobody has verified. See [Verify the signature](https://docs.qubesync.com/webhooks.md#verify-the-signature).

### `answer-fast-work-in-job`

Major Applies to every integration.

**Answer `2xx` within 5 seconds, and do the work in a background job.**

Anything but a `2xx` within 5 seconds is a failed delivery: a timeout, a redirect, a `4xx` or a
`5xx`. It's retried with backoff for about 8 hours, and then marked failed (a run's latest event can
then be sent again with `POST .../workflow-runs/{run_id}/redeliver`), so work done before the answer
can run past it and run again on each retry. See
[Four jobs for your endpoints](https://docs.qubesync.com/webhooks.md#four-jobs-for-your-endpoints) and
[When a delivery fails](https://docs.qubesync.com/webhooks.md#when-a-delivery-fails).

### `durable-job-queue`

Major Applies to every integration.

**Enqueue each delivery on a durable queue that retries a failed job, and answer `5xx` when the
enqueue itself fails.**

Once QuBe Sync has its `2xx`, it won't send that event again: a job that raises, or one an
in-process queue loses on a restart, is gone unless your queue keeps and retries it. See
[Handle it in a job](https://docs.qubesync.com/webhooks.md#handle-it-in-a-job) and
[When a delivery fails](https://docs.qubesync.com/webhooks.md#when-a-delivery-fails).

### `fixed-operation-map`

Critical Applies when the webhook URL names its handler (an operation
slug in the path).

**Turn the operation slug in a webhook URL into its class through a fixed map, and answer `404` for
any other slug; never build a class name from the path.**

The signature covers the body, not the URL, so a signed body can be posted to any path. A class
looked up from the path lets the path choose which code runs. See
[Handle it in a job](https://docs.qubesync.com/webhooks.md#handle-it-in-a-job).

### `route-on-event`

Minor Applies when one endpoint takes more than one kind of webhook
(request answers, run events, connection events, the name index's).

**Route a webhook on its `event` first: run, connection and name index webhooks carry one, and a
request's answer doesn't.**

One endpoint can take a request's answers, a run's events, a connection's events and
`name_index.ready`. A handler that treats every body as a request's answer finds no request for the
others, and drops them. See [Handle it in a job](https://docs.qubesync.com/webhooks.md#handle-it-in-a-job) and
[Name index events](https://docs.qubesync.com/webhooks.md#name-index-events).

### `skip-other-reference`

Critical Applies when the app starts workflow runs.

**Skip, and log, a run event whose `reference` isn't the one saved on the record it's for.**

The signature covers the body, not the URL, so a record id in the URL is only a pointer, and a
record's earlier runs keep sending until they end. Applying another run's event writes its results
to the wrong record, perhaps another customer's. See [Workflow runs](https://docs.qubesync.com/webhooks.md#workflow-runs) and
[What your app receives](https://docs.qubesync.com/workflows.md#what-your-app-receives).

### `run-event-newer-revision`

Major Applies when the app starts workflow runs.

**Apply a run's webhook only when its `revision` is higher than the last you applied for that run,
and save the revision with the change; notifications are the exception.**

Deliveries can repeat, cross each other and arrive before the response to the call that caused them.
Every body of a run (a webhook, the `201`, a decision's response, a `GET`) carries its `revision`,
which goes up with every change. A notification is data, not the run's state, so it's deduplicated
on `delivery_id` and never skipped by revision. See [Workflow runs](https://docs.qubesync.com/webhooks.md#workflow-runs) and
[What your app receives](https://docs.qubesync.com/workflows.md#what-your-app-receives).

### `notification-once`

Major Applies when a workflow the app starts sends it data
(`pull_changes`, `stream_query`, or any chart with a `notify` state).

**Take each `workflow_run.notification` once, by its `delivery_id`, in a store every worker
shares.**

Delivery is at least once: a failed delivery is retried up to 10 times over about an hour, with the
same `delivery_id`. A record of handled ids in one process's memory misses a retry that reaches
another worker. See [Sending your app data](https://docs.qubesync.com/workflows.md#sending-your-app-data) and
[What your app receives](https://docs.qubesync.com/workflows.md#what-your-app-receives).

### `dedupe-request-webhooks`

Major Applies when the app queues QuickBooks requests itself, not only
through workflows.

**Deduplicate a request's webhooks on `page_request_id` plus `state`: a `retryable` webhook isn't
final.**

A `retryable` webhook and the answer that follows it share `id` and `page_request_id`, and only
`state` differs. Keyed on an id alone, the answer looks like a repeat and is lost. See
[What to do with each `state`](https://docs.qubesync.com/webhooks#what-to-do-with-each-state) and
[Handling far edge cases](https://docs.qubesync.com/pagination.md#handling-far-edge-cases).

### `pages-complete-1-to-n`

Major Applies when the app queues paged list queries itself.

**Count a paged query as done once you've handled every page from 1 to the `done` page's `page`, not
when the `done` page arrives.**

Pages can arrive out of order (page 4 before page 3), and a retried page twice. The `done` page
(`max_sandbox_iterations_reached` in a sandbox) is the last sent, and its `page` says how many there
are. If the session ends partway, the next page ends `error`, and the query has to be queued again
from page 1. See [Handling far edge cases](https://docs.qubesync.com/pagination.md#handling-far-edge-cases) and
[If the session ends partway](https://docs.qubesync.com/pagination.md#if-the-session-ends-partway).

### `fetch-omitted-response-xml`

Minor Applies when the app reads `response_xml` from request webhooks.

**When a request's webhook has `response_xml_omitted: true`, GET its `links.self` with your API key
for `response_xml`.**

A webhook that would be over 1 MB comes without `response_xml`: it's null, and `links.self` is the
request's URL in the API, `response_xml` included. `response_json` is always there, so an app that
reads only `response_json` needs nothing here. This one GET, in the webhook's job, isn't polling:
the answer is already in. See [Payload fields](https://docs.qubesync.com/webhooks.md#payload-fields).

## Questions and endings

What a workflow run asks your app, and every way it ends.

### `never-pick-an-answer`

Major Applies when a workflow the app starts can ask a question
(`workflow_run.awaiting_input`).

**Show each question a run asks to your user, from its `message`, `details` and `choices`, and
answer with the choice they pick; never pick one in code.**

A run asks only when it can't decide: which of several records a name means, or whether a lost add
went through. A guessed answer can put an invoice on the wrong customer or add it twice, and the run
waits for an answer for as long as it takes, so there's nothing to gain by guessing. A rule your
user set up ahead of time is still their choice. See
[Answering a question](https://docs.qubesync.com/workflows.md#answering-a-question),
[Two kinds of question](https://docs.qubesync.com/workflows.md#two-kinds-of-question) and
[What your app receives](https://docs.qubesync.com/workflows.md#what-your-app-receives).

### `answer-at-links-decisions`

Major Applies when a workflow the app starts can ask a question
(`workflow_run.awaiting_input`).

**Answer at the saved `links.decisions` as given, with the picked `choice` and its fields' `data`,
and take a `409` or a `400 not_awaiting_input` on a retry as already answered.**

`links.decisions` carries the question's `prompt_token`, so an answer sent again can't answer the
run's next question. A `choice` names one the server offered, so what it fills in (a candidate's
ListID, say) comes from the server, not your app. Without a token, an answer is taken for whatever
the run is asking when it arrives. See [Answering a question](https://docs.qubesync.com/workflows.md#answering-a-question),
[Good to know](https://docs.qubesync.com/workflows.md#good-to-know) and [The API at a glance](https://docs.qubesync.com/workflows.md#the-api-at-a-glance).

### `questions-passed-through`

Major Applies when a workflow the app starts can ask a question
(`workflow_run.awaiting_input`).

**Show any question the same way, from its `message`, `details` and `choices`, and post the chosen
`choice`; never branch on a question's id, and never send a ListID or TxnID your user typed or your
app picked.**

Each prompt carries what a screen needs: what it's about in `details`, and every choice with the
fields it takes, so one screen answers every question, including ones a workflow adds later. Code
per question id falls behind the chart, and it's where guesses creep in. A candidate's choice
carries its id from the server, and a `record` field's value comes from `links.search`, so every id
the run uses is one QuickBooks has. See [Answering a question](https://docs.qubesync.com/workflows.md#answering-a-question) and
[What your app receives](https://docs.qubesync.com/workflows.md#what-your-app-receives).

### `question-route-authorized`

Critical Applies when the app answers or searches a run's questions from
routes of its own.

**Check that the signed-in user may act on the record before an answer or search route reads its
question; never trust a record id from a form or URL alone.**

The route answers with your API key, which reaches every connection of your app, and the record id
in its URL is the user's to change. Without the check, any signed-in user can answer another
customer's question, or search their company file, by changing one number. See
[Answering a question](https://docs.qubesync.com/workflows.md#answering-a-question) and
[Do it from your code](https://docs.qubesync.com/quickstart.md#do-it-from-your-code).

### `handle-every-ending`

Major Applies when the app starts workflow runs.

**Handle every way a run can end: each outcome of `workflow_run.completed`, `workflow_run.failed`
and `workflow_run.cancelled`, with a branch for an outcome you don't know.**

A run ends once: `completed` with an `outcome` (a chart's own `cancelled` outcome is a completed
run), `failed` with an `outcome` and an `error` in words, or `cancelled`, with an optional
`cancel_reason`. What it changed in QuickBooks stays changed either way, and a record left sending
after a failure tells your user nothing. See [How a run ends](https://docs.qubesync.com/workflows.md#how-a-run-ends).

## Stored QuickBooks data

The QuickBooks ids and records your app keeps, and how it finds them again.

### `ids-keyed-by-connection`

Critical Applies when the app stores QuickBooks ids (`list_id`, `txn_id`)
or QuickBooks records.

**Key every QuickBooks id you store by the QuBe Sync connection it came from (run events carry it as
`connection_id`) and its list or transaction type: never by the id alone, and never by your record
that keeps the connection (the customer's account or shop).**

QuickBooks makes a `list_id` unique only within one list family of one company file (customers,
vendors, employees and other names share one; every item type shares another), and a `txn_id` within
one transaction type: two of your customers' files can hold the same one, and a copy of a file (a
restored backup, a test copy) holds every one the same. A customer who connects another company file
gets a new connection, so nothing from the old file may match it. See
[Create](https://docs.qubesync.com/making-requests.md#create) and [Sending your app data](https://docs.qubesync.com/workflows.md#sending-your-app-data).

### `stored-id-on-its-own-connection`

Critical Applies when the app sends stored QuickBooks ids back (a
`list_id` in a reference such as `customer_ref`, or a `txn_id`).

**Send a stored QuickBooks id only on the connection it came from; on any other, send the name, or
find the record again.**

On another connection the same id is another company file's record, or none: an invoice can land on
the wrong customer, or be refused with 3140. After a customer connects another company file, their
old rows' ids mean nothing there. See [Create](https://docs.qubesync.com/making-requests.md#create).

### `upsert-keeps-later-copy`

Major Applies when the app imports or syncs QuickBooks records.

**Upsert each record you import, keeping the copy with the later `time_modified`, and don't let a
page saved later bring back a record that was deleted.**

Pages can be saved in any order: by a queue with several workers, after a retried delivery, or when
a query queued again after a session ended partway starts from page 1. `pull_changes` sends its
deletions once the last page is answered, which can be before every page is saved. See
[If the session ends partway](https://docs.qubesync.com/pagination.md#if-the-session-ends-partway) and
[Sending your app data](https://docs.qubesync.com/workflows.md#sending-your-app-data).

### `refer-by-id-not-name`

Major Applies when the app keeps references to QuickBooks records
(customers, items, QuickBooks accounts).

**Once you know a record's `list_id` or `txn_id`, store it on your own record and refer to the
QuickBooks record by it, not by its name.**

A `list_id` is stable for the life of the company file; a `full_name` changes when someone renames
the record in QuickBooks. Match a name again only when the id stops working: the record was deleted,
or merged into another, and a request that references it is refused with 3120. See
[Create](https://docs.qubesync.com/making-requests.md#create).

### `store-what-the-ending-carries`

Major Applies when the app keeps what its workflow runs make or find.

**Store what a run's final webhook carries in `output`: the records and ids it made or found, such
as `add_transaction`'s `output.record` and each entry of its `output.references`.**

A run's `output` is built for your app. `add_transaction`'s holds the whole transaction with its
lines, and each reference keyed by where it sits in your `params` (`customer_ref`,
`invoice_line_add.0.item_ref`), with the record it was found, chosen or created as. `summary` is the
run's own log of its steps. See [How a run ends](https://docs.qubesync.com/workflows.md#how-a-run-ends).

### `prune-ended-runs-only`

Major Applies when the app deletes its run records.

**Prune only runs that have ended, at least a day after they ended, never the QuickBooks ids you
keep, and never the run your next sync's `since` comes from unless you keep that time elsewhere.**

A run deleted before it ends loses its reference, so the next start makes a new one and can add the
record twice. A webhook for a deleted run is answered `404`, which QuBe Sync retries for hours and
then reports. The ids are what say a record is in QuickBooks, and a lost `since` makes the next sync
a full one. See [When a delivery fails](https://docs.qubesync.com/webhooks.md#when-a-delivery-fails).

## QuickBooks behaviour

What QuickBooks Desktop and the Web Connector do that other APIs don't, for an app that sends
requests itself.

### `timed-out-look-first`

Major Applies when the app queues QuickBooks writes itself (a workflow
settles its own).

**Treat a `timed_out` request, or an `error` with `error_type` `outcome_unknown` or
`response_too_large`, as perhaps applied: queue a query again (a too-large one paged or narrowed),
but look before you send a create or an update again.**

`timed_out` means QuickBooks received the request but its answer never came back: the customer's
computer lost its connection, or QuickBooks or the Web Connector was closed, while the request was
being processed. An `Idempotency-Key` doesn't help here, since the request was queued and sent:
sending it again with its key only returns it. `outcome_unknown` is the same for a write the Web
Connector reported an error on mid-request: QuickBooks may have applied it. `response_too_large`
means QuickBooks ran the request and answered with more than one answer may hold, so the answer
wasn't kept: a write happened, and a query needs smaller pages or a narrower filter, not the same
request again. See [When the answer is lost](https://docs.qubesync.com/error_handling.md#when-the-answer-is-lost),
[When the outcome is unknown](https://docs.qubesync.com/error_handling.md#when-the-outcome-is-unknown),
[When the answer is too large](https://docs.qubesync.com/error_handling.md#when-the-answer-is-too-large) and
[Lost answers](https://docs.qubesync.com/workflows.md#lost-answers).

### `retryable-not-resent`

Major Applies when the app queues QuickBooks requests itself.

**Don't resend a `retryable` request: show your customer `error.user_message`, and wait for the
answer.**

The Web Connector couldn't get it into QuickBooks (QuickBooks wasn't running, a dialog was open, or
the wrong company file was), so nothing was applied, and QuBe Sync keeps trying on the Web
Connector's next runs, for as long as it takes. A copy sent beside it runs too, once QuickBooks is
back. See [When QuickBooks is out of reach](https://docs.qubesync.com/error_handling.md#when-quickbooks-is-out-of-reach) and
[What to do with each `state`](https://docs.qubesync.com/webhooks#what-to-do-with-each-state).

### `stale-edit-sequence-reapply`

Major Applies when the app sends updates itself.

**On a `stale_edit_sequence` (3200), re-apply your change on the record in
`response_json.error.current` and resend with its `edit_sequence`, deciding first which value wins
for a field someone else changed too.**

QuickBooks refuses an update whose `edit_sequence` is out of date and changes nothing, but answers
with the record as it stands. Resending your fields with the new `edit_sequence` without a look
quietly overwrites what the other person changed. See
[When the record changed after you read it](https://docs.qubesync.com/error_handling.md#when-the-record-changed-after-you-read-it)
and [Conflicts](https://docs.qubesync.com/workflows.md#conflicts).

### `names-unique-across-lists`

Major Applies when the app creates customers, vendors, employees,
other names or items itself.

**Plan for a name another list already holds: names are unique across QuickBooks' customer, vendor,
employee and other-name lists together.**

Creating a customer whose name a vendor already has fails with error 3100 (`name_in_use`), and items
share one name space across every type. A sync that looks only among customers before adding one
meets this on real company files. See [Create](https://docs.qubesync.com/making-requests.md#create) and
[QuickBooks said no](https://docs.qubesync.com/error_handling.md#quickbooks-said-no).

### `ref-number-not-unique`

Major Applies when the app stores or matches QuickBooks transactions.

**Key transactions by `txn_id`, not by their number: QuickBooks doesn't keep `ref_number` unique.**

QuickBooks accepts a new invoice with a number another invoice already has, and a voided invoice
keeps its number, with its amounts at zero. An import keyed on the number merges different invoices.
See [Tips](https://docs.qubesync.com/pagination.md#tips).

### `modified-date-filters`

Minor Applies when the app syncs changes with modified-date queries
itself.

**Filter lists with `from_modified_date` and transactions with
`modified_date_range_filter[from_modified_date]`, from the newest `time_modified` you received, sent
back exactly as it came.**

A list query takes the filter directly, and a transaction query takes it nested. QuickBooks labels
local times with the standard offset even in daylight saving time and reads the filter the same way,
so its own value finds what it should, while a UTC time from your own clock reaches up to an hour
further back. A modified-date filter never finds deletions. See [Tips](https://docs.qubesync.com/pagination.md#tips) and
[Query](https://docs.qubesync.com/making-requests.md#query).

### `max-returned-is-not-the-whole-list`

Major Applies when the app queries QuickBooks lists itself.

**Don't take the answer to a query sent with `max_returned` alone as the whole list: leave the
paging parameters out, or send `iterator=true` with it, when you need every record.**

A query sent with `max_returned` alone gets at most that many records, in one answer, and QuickBooks
doesn't say whether more matched. What the app concludes about the records it didn't get, such as
that they're gone, is then wrong on a large company file. See [Auto-Pagination](https://docs.qubesync.com/pagination.md).

### `warning-is-success`

Major Applies when the app queues QuickBooks writes itself.

**Treat a `response_received` that carries a `warning` as saved: store the record's id and don't
resend it.**

A warning is still a success, and `data` is what QuickBooks saved; sent again, it's a second record.
`530` means a field you sent isn't supported by this QuickBooks, so the record was saved without it:
`message` names the field. See
[QuickBooks said yes, with a warning](https://docs.qubesync.com/error_handling.md#quickbooks-said-yes-with-a-warning).

### `branch-on-codes`

Minor Applies when the app queues QuickBooks requests itself.

**Branch on `error.error_type` and then the status code (`error.error_code`, or
`response_json.error.code`), never on the message text, and show your customer
`error.user_message`.**

The codes are stable things to branch on. `message` is QuickBooks' or the Web Connector's raw text,
and `user_message` is plain language for your end user: what went wrong and what to do. See
[QuickBooks said no](https://docs.qubesync.com/error_handling.md#quickbooks-said-no) and
[Where each key lives](https://docs.qubesync.com/error_handling.md#where-each-key-lives).

## Going live

Production: answers by webhook, secrets kept secret, and the production app ready.

### `webhooks-not-polling`

Major Applies to every integration.

**Take answers from webhooks, not by polling: `links.self`, and a `GET` of a request or a run, are
for scripts and for looking into a problem.**

A request is answered only when the customer's Web Connector checks in, every 5 minutes on a
standard connection, so a poll mostly finds nothing. The webhook arrives the moment QuickBooks
answers, and a run's notifications go only to its `webhook_url`. See
[Handling the response](https://docs.qubesync.com/making-requests.md#handling-the-response) and
[Where to next](https://docs.qubesync.com/quickstart.md#where-to-next).

### `no-secrets-in-logs-or-browser`

Critical Applies to every integration.

**Keep the API key and the webhook secret out of logs, error reports, the browser and the repo.**

Either is enough to act as your app: the key reads and writes every connected company file, and the
secret signs webhooks your endpoint will trust. Logged request headers and committed environment
files are where they leak. See [Do it from your code](https://docs.qubesync.com/quickstart.md#do-it-from-your-code) and
[Operating it](https://docs.qubesync.com/going-live.md#operating-it).

### `production-credentials`

Major Applies to every integration.

**Give production the production app's own API key and webhook secret, and rotate either by adding
the new one before expiring the old.**

Deliveries are signed with every active secret on the app, one `v1` each, so a rotation drops
nothing when your endpoint accepts any `v1`. Expiring an API key takes effect immediately, so deploy
its successor first. See [Launch checklist](https://docs.qubesync.com/going-live.md#launch-checklist) and
[Rotate your webhook secret](https://docs.qubesync.com/webhooks.md#rotate-your-webhook-secret).

### `production-runs-published`

Major Applies when the app starts workflow runs.

**Publish every workflow your app starts in the production app before launch: production connections
run only published versions.**

Sandbox and simulated connections run your latest edits, but a production connection always uses the
workflow's published version. Promote each workflow from your sandbox app, which publishes it there
too, and fix any company-file values the promotion warns about. See
[Launch checklist](https://docs.qubesync.com/going-live.md#launch-checklist) and
[From sandbox to production](https://docs.qubesync.com/workflow-authoring.md#from-sandbox-to-production).

### `public-webhook-url`

Major Applies to every integration.

**In production, make every `webhook_url` your app's public `https` address: never localhost, a
private address, a tunnel or a `qube listen` URL, and never one that redirects.**

QuBe Sync doesn't follow redirects, and a delivery to an address it can't reach, such as localhost
or a private IP address, isn't sent or retried: it fails at once. A tunnel or relay URL from
development stops working when the laptop does. See
[When a delivery fails](https://docs.qubesync.com/webhooks.md#when-a-delivery-fails) and
[Receiving webhooks in development](https://docs.qubesync.com/webhooks.md#receiving-webhooks-in-development).


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