Fetch Records Reporting
curl --request GET \
--url https://api.qobra.co/v2/reporting/{table_id}/records \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.qobra.co/v2/reporting/{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/reporting/{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/reporting/{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/reporting/{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/reporting/{table_id}/records")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v2/reporting/{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": "75b2c3d4e5f6g7h8i9j0k2l3",
"datatable.opportunity_name": "Acme Corp - Annual License",
"datatable.account_name": "Acme Corporation",
"datatable.amount": {
"value": 50000,
"currency": "USD"
},
"custom.total_paid": {
"value": 400,
"currency": "USD"
},
"datatable.close_date": "2024-01-15",
"datatable.stage": "Closed Won",
"standard.user": {
"id": "507f191e810c19729de860ea",
"email": "sarah.johnson@company.com"
},
"standard.commission_impact": {
"value": 2500,
"currency": "USD"
},
"standard.date": "2024-01-01"
},
{
"standard.id": "75b2c3d4e5f6g7h8i9j0k2l4",
"datatable.opportunity_name": "TechStart - Professional Plan",
"datatable.account_name": "TechStart Inc",
"datatable.amount": {
"value": 25000,
"currency": "USD"
},
"custom.total_paid": {
"value": 1200,
"currency": "USD"
},
"datatable.close_date": "2024-01-18",
"datatable.stage": "Closed Won",
"standard.user": {
"id": "507f191e810c19729de860eb",
"email": "sarah.johnson@company.com"
},
"standard.commission_impact": {
"value": 1250,
"currency": "USD"
},
"standard.date": "2024-01-18"
}
],
"meta": {
"next_start_id": "75b2c3d4e5f6g7h8i9j999l3",
"has_more": true,
"next_url": "https://api.qobra.co/v2/reporting/507f1f77bcf86cd799439012/records?start_id=75b2c3d4e5f6g7h8i9j999l3&limit=1000"
}
}Reporting
Fetch Records Reporting
Extracts records from record reporting structures. Records represent the underlying data used in commission calculations—CRM deals, activities, and custom metrics. Use this endpoint for sales analytics, pipeline reports, commission audit trails and CRM data extraction. To read a data table, use /v2/data-tables//records.
GET
/
v2
/
reporting
/
{table_id}
/
records
Fetch Records Reporting
curl --request GET \
--url https://api.qobra.co/v2/reporting/{table_id}/records \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.qobra.co/v2/reporting/{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/reporting/{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/reporting/{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/reporting/{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/reporting/{table_id}/records")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.qobra.co/v2/reporting/{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": "75b2c3d4e5f6g7h8i9j0k2l3",
"datatable.opportunity_name": "Acme Corp - Annual License",
"datatable.account_name": "Acme Corporation",
"datatable.amount": {
"value": 50000,
"currency": "USD"
},
"custom.total_paid": {
"value": 400,
"currency": "USD"
},
"datatable.close_date": "2024-01-15",
"datatable.stage": "Closed Won",
"standard.user": {
"id": "507f191e810c19729de860ea",
"email": "sarah.johnson@company.com"
},
"standard.commission_impact": {
"value": 2500,
"currency": "USD"
},
"standard.date": "2024-01-01"
},
{
"standard.id": "75b2c3d4e5f6g7h8i9j0k2l4",
"datatable.opportunity_name": "TechStart - Professional Plan",
"datatable.account_name": "TechStart Inc",
"datatable.amount": {
"value": 25000,
"currency": "USD"
},
"custom.total_paid": {
"value": 1200,
"currency": "USD"
},
"datatable.close_date": "2024-01-18",
"datatable.stage": "Closed Won",
"standard.user": {
"id": "507f191e810c19729de860eb",
"email": "sarah.johnson@company.com"
},
"standard.commission_impact": {
"value": 1250,
"currency": "USD"
},
"standard.date": "2024-01-18"
}
],
"meta": {
"next_start_id": "75b2c3d4e5f6g7h8i9j999l3",
"has_more": true,
"next_url": "https://api.qobra.co/v2/reporting/507f1f77bcf86cd799439012/records?start_id=75b2c3d4e5f6g7h8i9j999l3&limit=1000"
}
}Overview
This endpoint extracts records from record reporting structures. Records represent the underlying data used in commission calculations — CRM deals, activities, and custom metrics. Use this endpoint for:- Sales analytics and pipeline reports
- Commission audit trails (what deals contributed to commissions)
- CRM data extraction
Records = Details. This endpoint gives you the source data that flows
through Qobra’s calculations, not the final commission amounts.
To extract a data table as it was imported, before any calculation, use
fetch data table
records.
Only reporting tables from your company’s live environment are reachable.
A
table_id that belongs to a sandbox environment returns 404 Not Found,
even if the id was copied from the Qobra app while viewing a sandbox.Pagination
This endpoint uses ID-based pagination for consistent performance with large datasets.Basic pagination pattern
url = f"https://api.qobra.co/v2/reporting/{table_id}/records"
params = {"limit": 2000}
while True:
response = requests.get(url, headers=headers, params=params)
result = response.json()
# Process records
for record in result["data"]:
process(record)
# Check if more pages exist
if not result["meta"]["has_more"]:
break
# Use next_url for next page
url = result["meta"]["next_url"]
Use
next_url for simplicity: The API returns a pre-constructed
next_url in the response meta — just follow it instead of manually building
the next request.Filtering by modification date
Uselast_modified_after for incremental sync (only fetch new/updated records).
from datetime import datetime, timezone
def incremental_sync(table_id: str, api_key: str):
# Load last sync time from your database
last_sync = get_last_sync_time() # e.g., 2024-01-14T00:00:00Z
url = f"https://api.qobra.co/v2/reporting/{table_id}/records"
headers = {"X-API-Key": api_key}
params = {
"limit": 2000,
"last_modified_after": last_sync.isoformat()
}
new_records = []
while True:
response = requests.get(url, headers=headers, params=params)
result = response.json()
new_records.extend(result["data"])
if not result["meta"]["has_more"]:
break
url = result["meta"]["next_url"]
# Save current time as last sync
save_last_sync_time(datetime.now(timezone.utc))
return new_records
# Usage: Hourly incremental sync
new_records = incremental_sync(table_id, api_key)
print(f"Found {len(new_records)} new/updated records since last sync")
Selecting fields
The endpoint returns every field of the table by default. Passfields to receive only
the ones you need:
?limit=2000&fields=datatable.amount,datatable.close_date,datatable.stage
params = {
"limit": 2000,
"fields": "datatable.amount,datatable.close_date,datatable.stage",
}
- The values are
api_keys, as listed byGET /v2/data-structures/{table_id}/fields, separated by commas. The keys above are examples: read yours from that endpoint. standard.idandstandard.source_idare always returned, whether you list them or not.- A record keeps the table’s order of fields, not the order of your list. Spaces around a key and duplicate keys are ignored.
next_urlkeeps your selection. If you build the next URL yourself fromnext_start_id, passfieldsagain.- Without
fields, nothing changes: every field is returned.
Why select fields
A page holds one value per field and per record, so fewer fields make faster and lighter pages. On a table of 186 fields, keeping 50 divides the server time of a page by about three and its compressed size by about four. For a full extraction of a wide table, list only the fields your warehouse uses.Unknown keys
A key the fields endpoint does not list returns400, naming every unknown
key:
{
"count": 1,
"errors": [
{
"error": "ValidationError",
"resource": "fields",
"description": "Unknown fields: datatable.nope. A field is named by an api_key that /v2/data-structures/<id>/fields lists."
}
]
}
fields, read the fields endpoint again and update your
list. A list left empty (fields=) returns 400 too.
How many fields fit in the URL
The server accepts request lines of up to 4,094 bytes. The rest of the URL (the path,start_id, limit and the date filters) takes about 300 bytes.
That leaves about 3,790 bytes for the value of fields. Count start_id
from your first call: next_url adds it to every next page. Each key costs its
length plus 3 bytes, the size of a comma once encoded (%2C).
| Average key length | Fields that fit |
|---|---|
| 35 characters | About 100 |
| 45 characters | About 80 |
| 60 characters | About 60 |
A URL over the limit is refused before it reaches the API. You receive a
400 Bad Request in text/html, not in the JSON error format above, and its
body contains Request Line is too large. Remove fields from your list, or
drop fields to receive every field.Performance & best practices
Use ID-based pagination
Always use
next_url for large datasets.url = result["meta"]["next_url"]
Incremental sync
Use
last_modified_after for efficiency.params = {"last_modified_after": last_sync}
Use Limit 2000
Optimal balance between network overhead and response time.
params = {"limit": 2000}
Authorizations
Path Parameters
Query Parameters
Comma-separated api_keys of the fields to return, as listed by /v2/data-structures/{table_id}/fields (e.g. datatable.amount,datatable.close_date). standard.id and standard.source_id are always returned. When omitted, every field is returned. A key the fields endpoint does not list returns 400.
Was this page helpful?