Appearance
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:00ZSame 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
| Rule | Behavior |
|---|---|
| Lookback window | changedSince can be at most 30 days back — QBO's own limit. Older → whole-request 400 CDC_INVALID_CHANGED_SINCE. |
changedSince format | Full ISO 8601 (2026-07-01T00:00:00Z) or a bare date (2026-07-01). Missing or unparseable → the same 400 above. |
| Per-entity cap | 1,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 list | Missing or empty entities → whole-request 400 CDC_INVALID_ENTITIES. |
| Dedup | Entity names are deduped case-insensitively — entities=customer,Customer yields one Customer slot. |
| Slot order | One slot per deduped entity, in the order you listed them (first occurrence wins on a dedup collision). |
| Coverage | Every requested entity gets a slot, even with zero changes — empty array, maxResults: 0. |
| Fault isolation | A 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 data | The 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-poll | The 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: falseon 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 aDeletedstub. If your sync logic treats deactivation as a delete signal, readActiveon 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 validTxnDelTypein 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
ItemandTaxRatein 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 sincechangedSince. 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 codes —
CDC_INVALID_ENTITIES,CDC_INVALID_CHANGED_SINCE,CDC_OVERFLOWin full, with causes and fixes. - Entity matrix — CDC — field-by-field support and the same boundaries in the compatibility contract's format.