Skip to content

Run a saved report

A saved report is a ReportDefinition your tenant operators built in the in-app Report Builder. As an integrator you can run it (paginate the rows) or export it (Excel, PDF, or CSV) without re-creating the query yourself. This recipe runs a report twice: once to consume the rows, once to download the same data as Excel.

  • reports:view
  • reports:export (for the export step)
Terminal window
curl -sS \
-H "X-Api-Key: $AURA_API_KEY" \
"https://api.auradonors.com/api/tenants/$TENANT_ID/reports?page=1&pageSize=50" | \
jq '.items[] | {id, name, dataSourceKey, isShared}'

Pick the id of the report you want. Cache it on your side — report ids don’t change unless an operator deletes and re-creates the report.

If the report’s isShared = false and was authored by another user, your key won’t see it (the listing filters to keys’ authorial scope). Have an operator share the report or duplicate it under an account whose scope your key has.

Terminal window
REPORT_ID="00000000-0000-0000-0000-00000000abcd"
curl -sS -X POST \
-H "X-Api-Key: $AURA_API_KEY" \
"https://api.auradonors.com/api/tenants/$TENANT_ID/reports/$REPORT_ID/run?page=1&pageSize=100"

The endpoint is a POST (because running has measurable side effects — it writes a ReportRun log row) but takes page and pageSize as query parameters. Default pageSize is 50.

The response is a ReportResultDto:

{
"columns": [
{ "key": "ContactId", "label": "Contact ID", "type": "Integer" },
{ "key": "ContactName", "label": "Donor", "type": "String" },
{ "key": "OrderDate", "label": "Date", "type": "Date" },
{ "key": "Amount", "label": "Amount", "type": "Decimal" }
],
"rows": [
{ "ContactId": 1834, "ContactName": "Jane Donor", "OrderDate": "2026-04-12", "Amount": 100.00 },
{ "ContactId": 4012, "ContactName": "Sam Smith", "OrderDate": "2026-04-12", "Amount": 250.00 }
],
"totalCount": 1432,
"page": 1,
"pageSize": 100
}

The columns array describes the shape of each row. Don’t hard-code column keys against a saved report — operators can edit a report’s column list and your code should adapt. Switch on the column type to parse the values:

Column typeJSON shape
Stringstring
Integerinteger
Decimalnumber (parse to decimal / fixed-point arithmetic — JS numbers lose precision past 2^53)
Booleanboolean
DateISO-8601 date string (yyyy-MM-dd)
DateTimeISO-8601 timestamp (yyyy-MM-ddTHH:mm:ssZ)

Same pagination contract as everywhere else (see Conventions → Pagination):

public async IAsyncEnumerable<JsonElement> WalkReportRowsAsync(
HttpClient http, Guid tenantId, Guid reportId)
{
var page = 1;
while (true)
{
var url = $"/api/tenants/{tenantId}/reports/{reportId}/run?page={page}&pageSize=200";
using var response = await http.PostAsync(url, content: null);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
foreach (var row in result.GetProperty("rows").EnumerateArray())
yield return row;
var total = result.GetProperty("totalCount").GetInt32();
var size = result.GetProperty("pageSize").GetInt32();
var pages = (int)Math.Ceiling((double)total / size);
if (page >= pages) yield break;
page++;
}
}

Watch the size of totalCount. Reports can return tens of thousands of rows; if you don’t actually need to consume every row, prefer a tighter filter on the saved report (an operator change) over walking pages.

When you don’t want to materialize the rows in memory, ask the platform to build a file. The export endpoint streams the file back as the HTTP response body:

Terminal window
curl -sS -X POST \
-H "X-Api-Key: $AURA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"format":"Excel"}' \
"https://api.auradonors.com/api/tenants/$TENANT_ID/reports/$REPORT_ID/export" \
-o "donor-report.xlsx"

Body fields:

FieldValuesNotes
formatExcel / Pdf / CsvRequired. Case-sensitive — capitalize the first letter.

The response is the file bytes with Content-Type set to the matching MIME type (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/pdf, or text/csv). The full report is rendered — there is no per-export page parameter; an export pulls every row up to the 50,000-row platform cap.

Same shape, different URLs. Use these for analytical reports the platform ships out of the box (donor retention, RFM scoring, fund performance, etc.):

Terminal window
curl -sS \
-H "X-Api-Key: $AURA_API_KEY" \
"https://api.auradonors.com/api/tenants/$TENANT_ID/reports/types"
curl -sS -X POST \
-H "X-Api-Key: $AURA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"parameters":{"asOf":"2026-01-01"}}' \
"https://api.auradonors.com/api/tenants/$TENANT_ID/reports/types/RfmScoring/run"

The parameters object’s keys come from the report type’s Parameters list returned by GET /reports/types.

StatusBodyFix
400{"": ["Report exceeds 50,000 row export cap."]}Add a tighter date filter; export is hard-capped.
400{"": ["Cohort is unavailable."]}The saved report references a cohort that’s been deleted. Have an operator update the report.
403{"title": "Missing required permission: reports:export"}Add the scope to your key.
404(empty)Report id doesn’t exist or doesn’t belong to this tenant.
  • For periodic delivery, set up a schedule from inside the app rather than polling. Schedules email rendered files on a fixed cadence.
  • Subscribe to report.created / report.updated if you want to react to schema changes (those are part of the future webhook catalog — current catalog at Webhook events).