Skip to main content

The Data 360 Query API: running SQL over the model from code

The programmatic side of Data Cloud SQL: run a live query over the unified model from code and get rows back. The two surfaces — the synchronous Query API v2 with its nextBatchId cursor, and the newer asynchronous Query Connect API with a queryId and offset paging — the auth flow at a high level, the time constraints Salesforce enforces, and when a live query beats a Calculated Insight.

Reference·Last updated 2026-06-01·Drafted by Lira · Edited by German Medina

The Query API is how you run Data Cloud SQL from code instead of from the Query Editor. Same dialect, same DMOs over the unified profile — the difference is that a program submits the query and reads the rows back, so it can export, integrate, or feed another system. This is the "query live every time" half of the subcategory's anchor; the other half is the Calculated Insight, which computes a metric once and serves it everywhere. This page is the reference for the live side: the surfaces, the failure mode, and the decision of when to reach for it.

There are two surfaces, and which one you target changes how you read results. Both speak the Data Cloud SQL dialect; neither changes the model underneath. What changes is whether the call is synchronous or asynchronous — and that single fact is where the rows go missing.

The two surfaces

Query API v2POST /api/v2/query. The classic surface, synchronous: you post a body with a sql string and the first response already carries rows. Results come back in cursor batches. The response includes the data array, a rowCount, a metadata block describing the columns, a nextBatchId, and a done boolean. While done is false, there is another batch to fetch via nextBatchId; when done is true, you have the whole result set. The cursor is forward-only — you walk it to the end or you stop short.

// Query API v2 — shape of a paginated response (conceptual, fields trimmed)
{
  "data": [ /* one object per row */ ],
  "rowCount": 5000,
  "done": false,          // there is more — keep going
  "nextBatchId": "abc123", // fetch the next batch with this
  "metadata": { /* column types and order */ }
}

Query Connect API/ssot/query-sql, introduced August 2025. The newer surface, built for larger and longer-running work, and asynchronous in shape: you submit the query and get back a queryId, then you poll status and retrieve rows separately. Pagination here is by rows, not by cursor — you ask for a window with an offset and a rowLimit (for example, 2,000 rows at a time out of a 100,000-row result), and you page by advancing the offset. It runs on Salesforce's Hyper engine, keeps results available for up to 24 hours so you can page back over them without re-running, and Salesforce positions it as the surface new integrations should target.

// Query Connect API — submit returns a handle; you poll and page by rows
{
  "queryId": "f1e2d3...",  // track status and pull rows against this
  "done": false            // poll until the run completes, then page offset/rowLimit
}

The practical read: v2 is the shorter path for a query whose result fits a handful of synchronous batches; Query Connect is the one you want when the result is large, the run is long, or you need to page back over it. Either way the obligation is the same — read past the first response.

Pagination and async are the failure surface

This is Query & Insights gotcha 7, and it is worth stating in full here because the Query API is exactly where it bites. Code that reads the first response and stops is not querying a small result — it is silently truncating a large one. v2 hands you the first batch with done: false and trusts you to follow nextBatchId; Query Connect hands you a queryId and trusts you to poll and page. In both cases the first payload looks like a valid, complete answer, because structurally it is a valid answer — just a partial one.

The discipline is small and non-negotiable: whatever calls the Query API handles the full retrieval loop, not the first page. If a result is small enough to fit one synchronous v2 batch today, write the loop anyway — the day the data grows past one page, the loop is the difference between a correct export and a quietly broken one.

What you actually send

The request is a SQL string in the Data 360 (formerly Data Cloud) dialect — unquoted, namespaced identifiers, DMOs over the unified profile. The same single-object discipline the rest of this subcategory uses applies: query one object, count the right thing, and traverse to source records through IndividualIdentityLink__dlm rather than joining UnifiedIndividual__dlm straight to a source object.

-- The kind of SQL the Query API runs: single object, resolved profile.
-- Counting unified individuals, NOT raw imported rows. The caller still
-- pages this to the end even though it looks small — results grow.
SELECT
  i.ssot__Id__c          AS individual_id,
  i.ssot__FirstName__c   AS first_name
FROM UnifiedIndividual__dlm i
WHERE i.ssot__FirstName__c IS NOT NULL;

Note what is true on both surfaces: LIMIT in the SQL does not save you from pagination. LIMIT bounds the result the engine produces; pagination governs how that result is handed back to you. A LIMIT 10000 result still arrives in batches, and you still walk them.

Auth, at the level you need it

You authenticate against the Query API in two moves, and the shape matters more than the syntax. First, a connected app gets you a standard Salesforce OAuth access token — the cdp_query_api scope is the one that authorizes running ANSI SQL against Data 360. Second, you exchange that Salesforce token for a Data 360 token, and that exchange also tells you the org's own Data 360 endpoint — a unique, system-generated host (the cdpInstanceUrl) that no other org shares. You call the Query API against that host, with the Data 360 token, and you re-exchange before the token expires rather than per call.

That is the whole conceptual model: OAuth in, token exchange to a Data-Cloud-scoped token plus the org's endpoint, then query. The exact request bodies, header names, and refresh handling belong in code you write against the reference below, against your org's connected app — not copy-pasted from a docs page. The shape is the durable part; the keystrokes are yours.

When the Query API, and when a Calculated Insight

This is the anchor decision of the whole subcategory, seen from the live side. The two surfaces answer two different questions, and the cost of confusing them compounds.

Reach for the Query API when the work is genuinely live or one-off: an ad-hoc pull a human asked for once, a programmatic export to a warehouse, an integration that needs current rows on demand, a query whose shape you don't know in advance. Nothing is persisted; you get exactly the rows the model holds right now, and you pay for the scan each time you run it.

Reach for a Calculated Insight when the same metric is retrieved by many consumers — lifetime value, an engagement score, an order count per person. Computing it once and letting segments, activations, and agents retrieve it is cheaper, consistent, and stable; running that same aggregation live through the Query API on every consumer is slower, costs more on every call, and drifts as two callers compute it slightly differently. The honest test: if more than one consumer needs this number, or the same consumer needs it repeatedly, it is a Calculated Insight, not a query you re-run. (The Style Guide frames this trade-off as a convention; data-cloud-principles — principle 7 — frames it as the rule.)

Put plainly: the Query API is for questions asked once, by code; a Calculated Insight is for an answer retrieved many times, by everyone. Use the live surface for live work, and stop re-deriving on the fly what you could compute once.

Related

Reference: