Skip to content

Change Data Capture

CDC is QBO's incremental-sync endpoint: one request, multiple entities, and you get back everything that changed — including deletions — since a timestamp you supply. It's the efficient alternative to re-walking /query for every entity on every poll.

GET /v3/company/:realmId/cdc?entities=Invoice,Customer&changedSince=2026-07-01T00:00:00Z

Same Bearer auth as every other endpoint.

Example

bash
curl -s "https://api.tenkeybridge.com/v3/company/$REALM_ID/cdc" \
  --get --data-urlencode "entities=Invoice,Customer" \
  --data-urlencode "changedSince=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json"

The response wraps one slot per requested entity, in request order, inside a single CDCResponse envelope:

json
{
  "CDCResponse": [
    {
      "QueryResponse": [
        {
          "Invoice": [
            { "Id": "1F00-1", "SyncToken": "0", "DocNumber": "INV-1", "...": "full invoice payload" },
            {
              "Id": "1F00-DEL-1",
              "status": "Deleted",
              "domain": "QBO",
              "MetaData": { "LastUpdatedTime": "2026-07-20T09:15:00-07:00" }
            }
          ],
          "startPosition": 1,
          "maxResults": 2
        },
        {
          "Fault": {
            "Error": [
              {
                "Message": "More than 1000 Customer objects changed since 2026-07-01T00:00:00Z.",
                "Detail": "More than 1000 Customer objects changed since 2026-07-01T00:00:00Z. See https://docs.tenkeybridge.com/reference/error-codes.html#cdc_overflow",
                "code": "CDC_OVERFLOW"
              }
            ],
            "type": "ValidationFault"
          },
          "tkb": {
            "code": "CDC_OVERFLOW",
            "causes": ["More than 1,000 objects changed for this entity in the requested window. A CDC slot never silently truncates, so the entity returns this fault instead of a partial array."],
            "fixes": [
              "Shorten the changedSince window and poll more frequently.",
              "Or walk this entity with /query using a MetaData.LastUpdatedTime filter plus STARTPOSITION/MAXRESULTS pagination — that path has no object cap."
            ],
            "docsUrl": "https://docs.tenkeybridge.com/reference/error-codes.html#cdc_overflow"
          }
        }
      ]
    }
  ],
  "time": "2026-07-29T18:04:11.203Z"
}

Here Invoice changed: one updated record, then one deleted stub. Customer happened to cross the per-entity cap for this window, so its slot is a Fault instead of data — the Invoice slot is unaffected. That's the whole model: each entity gets its own outcome, and one bad entity never takes down the rest of the poll.

Deleted stubs are minimal on purpose — status, domain, Id, and MetaData.LastUpdatedTime (when the record was deleted) — matching QBO's shape. They're always sorted oldest-first and always come after the changed rows in the same array.

An entity with nothing to report still gets a slot — an empty array, not an omission:

json
{ "Item": [], "startPosition": 1, "maxResults": 0 }

Rules

RuleBehavior
Lookback windowchangedSince can be at most 30 days back — QBO's own limit. Older → whole-request 400 CDC_INVALID_CHANGED_SINCE.
changedSince formatFull ISO 8601 (2026-07-01T00:00:00Z) or a bare date (2026-07-01). Missing or unparseable → the same 400 above.
Per-entity cap1,000 changed objects. Crossing it never truncates silently — that entity's slot becomes a CDC_OVERFLOW fault instead. Shorten the window, or walk that entity with /query and a MetaData.LastUpdatedTime filter.
Entity listMissing or empty entities → whole-request 400 CDC_INVALID_ENTITIES.
DedupEntity names are deduped case-insensitively — entities=customer,Customer yields one Customer slot.
Slot orderOne slot per deduped entity, in the order you listed them (first occurrence wins on a dedup collision).
CoverageEvery requested entity gets a slot, even with zero changes — empty array, maxResults: 0.
Fault isolationA per-entity fault (unknown entity, gap/planned entity, overflow, or a Desktop execution error) replaces only that entity's slot. The poll keeps going for everything else.
Transport failure, before any dataThe agent is offline or times out before a single round trip has succeeded → the whole request fails: 503 AGENT_OFFLINE or 504 AGENT_TIMEOUT. No CDCResponse envelope at all.
Transport failure, mid-pollThe agent goes offline or times out after at least one round trip has already succeeded → the request still returns 200. Entities already fetched keep their data; the entity that was in flight and every entity still waiting get an AGENT_OFFLINE/AGENT_TIMEOUT fault slot, with no further sends attempted.

Unsupported or unrecognized entity names don't fail the whole request either — they come back as their own Fault slot (UNSUPPORTED_BY_DESKTOP for gap/planned entities, NOT_FOUND for names TenkeyBridge doesn't recognize at all), right alongside the entities that did return data.

Honest boundaries

CDC is a read over whatever Desktop can actually tell you changed — which is narrower than QBO's model in a few specific ways:

  • Deactivation is not deletion. Desktop only hard-deletes list records that were never used on a transaction. Setting Active: false on a Customer, Vendor, Item, etc. is a change, not a delete — it comes back as a normal changed record in the entity's array, never as a Deleted stub. If your sync logic treats deactivation as a delete signal, read Active on the changed record instead of waiting for a stub that will never arrive.
  • Three deletions Desktop can't report at all, each for a different reason:
    • TaxAgency — stored as vendors on Desktop, so a deleted tax agency is indistinguishable from a deleted vendor. Undetectable.
    • ExchangeRate — rides the Currency list rather than having its own delete surface.
    • Transfer'Transfer' isn't a valid TxnDelType in Desktop's deleted-transaction query, so transfer deletions can't be queried for.
  • Term deletion detection covers standard terms only. Desktop's deleted-list query reports standard payment terms; date-driven terms are outside that coverage.
  • A deleted sales-tax item can show up under two entities. If you request both Item and TaxRate in the same poll, a deleted sales-tax item's stub can appear in both slots — the same dual-surface behavior as the read path for that entity, not a bug in CDC.
  • CompanyInfo and Preferences always over-deliver rather than window-filter. Desktop doesn't version the company profile, so these two singleton entities return their current row whenever they're in entities, whether or not it changed since changedSince. Over-delivery beats a silent miss.

Latency

Each live entity in your entities list costs up to two sequential round trips to the agent — one for changed records, one for deleted stubs — and each round trip can take up to the agent's 60-second timeout. Worst case for N entities is N × 2 × 60s, all sequential; there's no per-endpoint deadline in v1. Poll with the entity list you actually need to sync, not every entity you might ever touch.

See also

  • Error codesCDC_INVALID_ENTITIES, CDC_INVALID_CHANGED_SINCE, CDC_OVERFLOW in full, with causes and fixes.
  • Entity matrix — CDC — field-by-field support and the same boundaries in the compatibility contract's format.

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.

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.