Skip to content

Safe retries (requestid) ​

A write that times out leaves you guessing: did QuickBooks apply it or not? Retry blindly and you may post the invoice twice. TenkeyBridge supports QuickBooks Online's answer to this, the requestid query parameter: send the same id again and the gateway returns the original result instead of applying the write a second time.

http
POST /v3/company/{realmId}/invoice?requestid=6b1c2f0e-5d7a-4c19-9f64-0a3c1d2e8b77

Use it on every write you might retry, and always after a 504 AGENT_TIMEOUT or a dropped connection.

The rules ​

  • Where: any POST /{entity} write — create, update (including sparse), ?operation=delete, ?operation=void — and each write item of a /batch. GET and queries ignore it.
  • Format: 1 to 50 printable ASCII characters, no spaces (a UUID is 36). On /batch the limit is 36, and each bId may be at most 10 characters. Otherwise 400 REQUESTID_INVALID.
  • One id, one request. Make a fresh id for every new write and keep it for every retry of that write. Reusing an id with a different body, entity or operation is refused: 422 REQUESTID_BODY_MISMATCH, nothing runs.
  • Scope: an id is remembered per company (realm), for 7 days. After that it is forgotten and the same id would run as a new write. (Intuit does not publish its window.)
  • Nothing is ever re-sent. A replay never sends the write to QuickBooks again.
ts
import { randomUUID } from "node:crypto";

const requestId = randomUUID();
for (let attempt = 0; ; attempt++) {
  try {
    return await client.invoice.create(invoice, { requestId });
  } catch (err) {
    if (!(err instanceof TenkeyBridgeApiError) || attempt >= 4) throw err;
    // 409 in flight, 503 offline, 504 timeout, 502 outcome unknown: same id, wait, retry.
    await sleep(err.retryAfterMs ?? 2000 * (attempt + 1));
  }
}

What a retry returns ​

The original requestRetrying with the same requestid
Succeeded (applied)200 with the record, read fresh from QuickBooks (same shape as the original success, current values). Header X-TKB-Idempotent-Replay: true. A replayed delete returns { "status": "Deleted" }
Failed with a definite fault (validation, duplicate name, stale SyncToken…)The same status and fault code. Nothing runs. Fix the request and send it with a new id
Still running409 REQUEST_IN_FLIGHT with Retry-After. Wait, then resend with the same id
Was never sent or was refused by a record lock (503 AGENT_OFFLINE, 503 QB_NOT_OPEN, QuickBooks 3175)Runs normally. Nothing was recorded, so the id is free
Lost after it was sent (504 AGENT_TIMEOUT, dropped connection, gateway restart)The gateway looks, read-only, for proof it applied (see below). Proven: 200 with the record. Not proven: 502 REQUEST_OUTCOME_UNKNOWN

Outcome unknown ​

When a write is lost after it reached QuickBooks, it may have applied. On a retry, the gateway tries to settle it with reads only:

WriteHow it is settled
Create with a DocNumber (Invoice, Bill, Estimate, …)Query by DocNumber. Exactly one match: applied
Create of a Customer, Vendor, Account, Item or ClassQuery by DisplayName / Name (QuickBooks enforces these as unique). Exactly one match: applied
DeleteRead the record. Gone: applied
VoidRead the record. SyncToken has moved and it carries QuickBooks' VOID: memo: applied
Update, or a create with no natural keyCannot be proven: always REQUEST_OUTCOME_UNKNOWN.

A match is evidence, not proof of authorship: a record found by DocNumber or name could have been made by someone else, and a void or delete could have been done by someone else first. In both cases the record is already in the state you asked for, so it is reported as applied.

Why never just re-send? Over the Web Connector a timed-out request may still be queued and run later, so "not found yet" does not mean "not applied". The gateway stays safe and says so: 502 REQUEST_OUTCOME_UNKNOWN. Retrying with the same id re-checks every time, so a late-running write is reported as soon as it lands. To settle it yourself, look the record up (query by DocNumber or Id; for an update compare the record's SyncToken).

If an update or create still cannot be confirmed and you must resolve it, check QuickBooks and then send any new write with a new requestid.

Batches ​

Put the requestid on the batch URL. Each write item is remembered as requestid:bId, so resending the whole envelope after a failure replays the items that settled and runs only the ones that did not: no duplicates and no bookkeeping. Changing an item's body under the same requestid and bId faults only that item (REQUESTID_BODY_MISMATCH). Queries in a batch are not recorded.

Sandbox ​

The developer sandbox behaves identically. A sandbox reset also forgets its remembered ids, since the records they point to are gone.

Privacy ​

TenkeyBridge remembers the requestid, the operation and entity, the resulting record Id, the status and fault code, timestamps, and a SHA-256 digest of the request. Request and response bodies are never stored.

Differences from QuickBooks Online ​

Intuit documents only that a repeated id returns the original response. Where it is silent, we chose behavior that cannot double-apply. See requestid in COMPATIBILITY.

See also ​

TenkeyBridge is an independent product, not affiliated with, endorsed by, or sponsored by Intuit Inc. QuickBooks, QuickBooks Online, and QuickBooks Desktop are trademarks of Intuit Inc., used only to describe compatibility.