Fetch Data Table Records
curl --request GET \
--url https://api.qobra.co/v2/data-tables/{data_table_id}/records \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.qobra.co/v2/data-tables/{data_table_id}/records"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.qobra.co/v2/data-tables/{data_table_id}/records', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.qobra.co/v2/data-tables/{data_table_id}/records",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.qobra.co/v2/data-tables/{data_table_id}/records"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.qobra.co/v2/data-tables/{data_table_id}/records")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v2/data-tables/{data_table_id}/records")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"standard.id": "6a1b2c3d4e5f60718293a4b1",
"standard.source_id": "0065g00000AbCdEAAV",
"datatable.name": "Acme Corp - Annual License",
"datatable.amount": {
"value": 50000,
"currency": "USD"
},
"datatable.close_date": "2026-01-15",
"datatable.stage": "Closed Won",
"datatable.owner": {
"id": "507f191e810c19729de860ea",
"email": "sarah.johnson@company.com"
},
"datatable.account": {
"id": "0015g00000XyZaBAAV",
"name": "Acme Corporation"
},
"datatable.is_renewal": false
},
{
"standard.id": "6a1b2c3d4e5f60718293a4b2",
"standard.source_id": "6a1b2c3d4e5f60718293a4b2",
"datatable.name": "TechStart - Professional Plan",
"datatable.amount": {
"value": 25000,
"currency": "EUR"
},
"datatable.close_date": "2026-01-18",
"datatable.stage": "Negotiation",
"datatable.owner": {
"id": "507f191e810c19729de860eb",
"email": "michael.chen@company.com"
},
"datatable.account": {
"id": null,
"name": null
},
"datatable.is_renewal": true
}
],
"meta": {
"next_start_id": "6a1b2c3d4e5f60718293a4b2",
"has_more": true,
"next_url": "https://api.qobra.co/v2/data-tables/507f1f77bcf86cd799439010/records?start_id=6a1b2c3d4e5f60718293a4b2&limit=100"
}
}{
"count": 1,
"errors": [
{
"error": "ValidationError",
"resource": "start_id",
"description": "Can't parse value for param 'start_id' : Value error, Invalid ObjectId"
}
]
}{
"message": "You're trying to access resource you're not authorized to.",
"type": "UnauthorizedError"
}{
"message": "You don't have permission to access this resource.",
"type": "ForbiddenError",
"error": "ForbiddenError"
}{
"message": "We couldn't find the requested resource",
"resource": "ObjectModel",
"type": "NotFoundError"
}Data tables
Fetch Data Table Records
Extracts the records of a data table: the rows synchronized from an integration, or created in Qobra, before any commission calculation. Use this endpoint to replicate a data table into a warehouse or a BI tool.
GET
/
v2
/
data-tables
/
{data_table_id}
/
records
Fetch Data Table Records
curl --request GET \
--url https://api.qobra.co/v2/data-tables/{data_table_id}/records \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.qobra.co/v2/data-tables/{data_table_id}/records"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.qobra.co/v2/data-tables/{data_table_id}/records', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.qobra.co/v2/data-tables/{data_table_id}/records",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.qobra.co/v2/data-tables/{data_table_id}/records"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.qobra.co/v2/data-tables/{data_table_id}/records")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v2/data-tables/{data_table_id}/records")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"standard.id": "6a1b2c3d4e5f60718293a4b1",
"standard.source_id": "0065g00000AbCdEAAV",
"datatable.name": "Acme Corp - Annual License",
"datatable.amount": {
"value": 50000,
"currency": "USD"
},
"datatable.close_date": "2026-01-15",
"datatable.stage": "Closed Won",
"datatable.owner": {
"id": "507f191e810c19729de860ea",
"email": "sarah.johnson@company.com"
},
"datatable.account": {
"id": "0015g00000XyZaBAAV",
"name": "Acme Corporation"
},
"datatable.is_renewal": false
},
{
"standard.id": "6a1b2c3d4e5f60718293a4b2",
"standard.source_id": "6a1b2c3d4e5f60718293a4b2",
"datatable.name": "TechStart - Professional Plan",
"datatable.amount": {
"value": 25000,
"currency": "EUR"
},
"datatable.close_date": "2026-01-18",
"datatable.stage": "Negotiation",
"datatable.owner": {
"id": "507f191e810c19729de860eb",
"email": "michael.chen@company.com"
},
"datatable.account": {
"id": null,
"name": null
},
"datatable.is_renewal": true
}
],
"meta": {
"next_start_id": "6a1b2c3d4e5f60718293a4b2",
"has_more": true,
"next_url": "https://api.qobra.co/v2/data-tables/507f1f77bcf86cd799439010/records?start_id=6a1b2c3d4e5f60718293a4b2&limit=100"
}
}{
"count": 1,
"errors": [
{
"error": "ValidationError",
"resource": "start_id",
"description": "Can't parse value for param 'start_id' : Value error, Invalid ObjectId"
}
]
}{
"message": "You're trying to access resource you're not authorized to.",
"type": "UnauthorizedError"
}{
"message": "You don't have permission to access this resource.",
"type": "ForbiddenError",
"error": "ForbiddenError"
}{
"message": "We couldn't find the requested resource",
"resource": "ObjectModel",
"type": "NotFoundError"
}Overview
This endpoint returns the records of a data table — the rows Qobra synchronizes from your CRM, database or files, and the ones created in Qobra — as they stand before any commission calculation. Use this endpoint for:- Replicating a data table into a data warehouse
- BI dashboards on the data your plans read
- Checking what Qobra imported from a source
Data tables = inputs. This endpoint gives you the rows your plans read.
What each row contributed to commissions comes from
record reporting.
Discover the ids with
GET /v2/data-structures,
where every data table is listed with "type": "data_table". A data table
belongs to no environment: unlike a reporting table or a quota, its id works
whichever environment you copied it from in the Qobra app.Your first call
curl --request GET \
--url 'https://api.qobra.co/v2/data-tables/DATA_TABLE_ID/records?limit=100' \
--header 'X-API-Key: YOUR_API_KEY'
import requests
url = "https://api.qobra.co/v2/data-tables/DATA_TABLE_ID/records"
headers = {"X-API-Key": "YOUR_API_KEY"}
response = requests.get(url, headers=headers, params={"limit": 100})
records = response.json()["data"]
const url = "https://api.qobra.co/v2/data-tables/DATA_TABLE_ID/records?limit=100";
const response = await fetch(url, {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { data } = await response.json();
Example response
{
"data": [
{
"standard.id": "6a1b2c3d4e5f60718293a4b1",
"standard.source_id": "0065g00000AbCdEAAV",
"datatable.name": "Acme Corp - Annual License",
"datatable.amount": { "value": 50000.0, "currency": "USD" },
"datatable.close_date": "2026-01-15",
"datatable.stage": "Closed Won",
"datatable.owner": {
"id": "507f191e810c19729de860ea",
"email": "sarah.johnson@company.com"
},
"datatable.account": {
"id": "0015g00000XyZaBAAV",
"name": "Acme Corporation"
},
"datatable.is_renewal": false
},
{
"standard.id": "6a1b2c3d4e5f60718293a4b2",
"standard.source_id": "6a1b2c3d4e5f60718293a4b2",
"datatable.name": "TechStart - Professional Plan",
"datatable.amount": { "value": 25000.0, "currency": "EUR" },
"datatable.close_date": "2026-01-18",
"datatable.stage": "Negotiation",
"datatable.owner": {
"id": "507f191e810c19729de860eb",
"email": "michael.chen@company.com"
},
"datatable.account": { "id": null, "name": null },
"datatable.is_renewal": true
}
],
"meta": {
"next_start_id": "6a1b2c3d4e5f60718293a4b2",
"has_more": true,
"next_url": "https://api.qobra.co/v2/data-tables/507f1f77bcf86cd799439010/records?start_id=6a1b2c3d4e5f60718293a4b2&limit=100"
}
}
What a record holds
Every record starts with two identifiers, then carries one key per field of the table:standard.id— the id of the record in Qobra.standard.source_id— its id in the source system, such as a Salesforce opportunity id. A record created in Qobra carries its ownstandard.idthere, as the second record above does.datatable.<key>— one per field. The keys are specific to your table: read them fromGET /v2/data-structures/{table_id}/fieldsrather than guessing them from the field names.
| Field type | Value in the response |
|---|---|
| Text | A string |
| Number | A number |
| Percentage | A number, as stored: 0.85 means 85% |
| Amount | { "value": …, "currency": … } |
| Date | YYYY-MM-DD |
| Yes/No | true or false, and false when left empty |
| User | { "id": …, "email": … }, both null when not found |
| Picklist | The name of the option |
| Multi-picklist | A list of option names |
| Link to a record | { "id": …, "name": … }, see below |
id is the source id of
that record, not its Qobra id, so it matches the standard.source_id you read
from the linked table. An empty link reads { "id": null, "name": null }, and a
link whose target Qobra has not resolved yet carries its id with a null name.
An amount carries the currency of its own value, so a page can mix currencies
within the same field.
Records created in Qobra are included
A table synchronized from an integration can also hold records created by hand in Qobra. This endpoint returns both: it reads the whole table, whatever the origin of each record. Tell them apart withstandard.source_id, which equals
standard.id on a record created in Qobra.
Pagination
Pages are linked: every response carries anext_url that already includes
your limit and any date filters you sent. Follow it until has_more is
false.
url = f"https://api.qobra.co/v2/data-tables/{data_table_id}/records"
params = {"limit": 2000}
while True:
result = requests.get(url, headers=headers, params=params).json()
for record in result["data"]:
process(record)
if not result["meta"]["has_more"]:
break
url = result["meta"]["next_url"]
params = None
limitdefaults to its maximum, 2000. Omitting it gives you the largest page, not a small one. It is validated rather than capped, so?limit=5000returns400instead of 2000 rows.- Records come in creation order.
Filtering by modification date
last_modified_after and last_modified_before restrict the response to
records modified inside a window. Both bounds are inclusive, and they cannot be
equal — passing the same datetime twice returns 400.
params = {
"limit": 2000,
"last_modified_after": "2026-03-01T09:00:00+00:00",
}
Deleted records are never returned, so an incremental sync does not see a
record disappear. Re-walk the whole table periodically to remove from your
copy the records deleted in Qobra or in the source.
Errors
| Status | Cause |
|---|---|
400 | Invalid pagination parameter |
401 | Missing or unknown API key |
403 | The public API is not enabled for your company |
404 | Unknown id, or an id that is not a data table, see below |
404 also answers the id of a reporting table or of a quota, which have
endpoints of their own, and the id of a table Qobra generates from your plans’
results. Those tables are not listed by GET /v2/data-structures either: their
results come from
record reporting and
statements.
This endpoint is the v2 counterpart of the v1
fetch data table records
endpoint, which stays maintained. Moving to v2 is optional: see the
migration guide.
Authorizations
Your Qobra API key. Generate it from Settings > API Keys in Qobra.
Path Parameters
ID of a data table (from /v2/data-structures, where it is listed with type data_table)
Query Parameters
Start after this record ID (for ID-based pagination, recommended)
Number of records per page (1-2000)
Required range:
1 <= x <= 2000ISO 8601 datetime - Only return records modified after this timestamp (for incremental sync)
ISO 8601 datetime - Only return records modified before this timestamp
Was this page helpful?