Skip to content

Bulk reports export

This page describes the bulk reports export feature (type=reports) from the Analyses section. The canonical endpoint is POST /exports with body {type: "reports"}. The legacy /reports/export/* routes still work with deprecation headers until sunset (see migration guide).

User flow

  1. Navigate to /analyses from the side menu.
  2. Click Export queries (visible only with ExportReports permission).
  3. The Export queries modal opens.
  4. Fill the form and review the estimate.
  5. Click Export to queue the job.
  6. The page navigates to /analyses/exports where the job appears with live status via SSE.
  7. When the job is done, download the ZIP from the list.

Date range

  • From (after): inclusive start date. Cannot be in the future.
  • To (before): inclusive end date. Must be >= after.
  • Maximum: 365 days between after and before. Exceeding returns DATE_RANGE_TOO_LARGE.

Sources (sourceKeys)

List of bureau identifiers to include. Examples: datacredito-acierta, datacredito-cifin, transunion-cdv, midas.

  • Default: all sources checked (when opening the modal without a previous selection).
  • Persistence: the last selection is saved in sessionStorage (reports-export-last-sources).
  • UX for long lists (>10 sources):
    • Select all / Deselect all buttons in the header.
    • Search bar (ion-searchbar) that filters by name/key without losing selection.
  • Leaving all unchecked disables the Export button with the message "Select at least one source".
  • If sourceKeys is empty in the API request, it is interpreted as "all sources" (no filter).

Statuses (statuses)

Report statuses to include (whitelist):

  • done
  • failed
  • running
  • retry
  • initial
  • unverifiable

Empty means "all statuses".

Format (format)

ValueOutput
consolidatedA single reports.csv with all selected columns in each row
separatedA manifest.csv with metadata + payloads-crudos/ and/or payloads-parseados/ folders with individual payloads as files

Columns (columns)

Closed whitelist of columns. At least one must be selected (EMPTY_COLUMNS otherwise):

KeyDescriptionSource
dateReport creation datereport.created_at
nameSource keyjob parameter
idTypeSubject's document typeparsed from report.input
idNumberDocument numberparsed from report.input
firstNameFirst namesparsed from report.input
lastNameLast namesparsed from report.input
statusReport statusenum
errorError message if failedreport.error
retriesRetry countreport.retries
durationMsDuration in msreport.finished_in_ms
idReport IDreport.id
rawPayloadRaw bureau payload (XML/JSON)report.Response
parsedPayloadParsed payload (JSON)report.Output

rawPayload and parsedPayload are exported as escaped cells when format=consolidated, or as individual files in payloads-* folders when format=separated.

Pre-flight estimate (dry-run)

Before queuing the job, the modal makes an estimate call to show:

  • itemCount: estimated count of reports matching the filters.
  • overflowed: true if the count exceeds the tenant hard-cap — in that case the Export button is disabled.
  • softWarn: true if the count exceeds the soft-warn (typically 100k). The modal shows a warning but allows continuation.

Endpoint: POST /exports?dryRun=true body {type: "reports", ...}. Creates no state.

Required permissions

  • ExportReports for all operations (open modal, create, estimate, list sources, download).
  • The Export queries button is hidden if the user lacks the permission.
  • Downloads validate cross-tenant ownership: a tenant T1 file is not accessible by T2 (404 Not Found).

ZIP download format

format=consolidated

reports-export-2026-05-12.zip
+- reports.csv

reports.csv contains one row per report with the selected columns. UTF-8 encoding without BOM.

format=separated

reports-export-2026-05-12.zip
+- manifest.csv
+- payloads-crudos/
|  +- datacredito-acierta/
|  |  +- 01H8XYZ.xml
|  |  +- 01H8ABC.xml
|  +- transunion-cdv/
|     +- 01H8DEF.json
+- payloads-parseados/
   +- datacredito-acierta/
   |  +- 01H8XYZ.json
   +- transunion-cdv/
      +- 01H8DEF.json

manifest.csv acts as an index and includes the selected metadata columns (without rawPayload/parsedPayload). The payloads-crudos/ and payloads-parseados/ folders appear only if the corresponding columns are checked.

Payload file names

  • Raw: <source>/<reportID>.<ext> where <ext> is xml or json depending on the bureau's native format.
  • Parsed: <source>/<reportID>.json.

Compliance and PII

  • Payloads contain PII: names, document numbers, scores, debts.
  • Access restricted by the ExportReports permission (not ReadAnalysis).
  • Retention: 30 days from the job's finishedAt.
  • After expiration, GET /exports/{id}/file (and the legacy alias /reports/export/download/:fileid) return 410 Gone with code: "EXPORT_FILE_EXPIRED".
  • Each download is logged (entity_id, user_id, order_id, file_id, ip, user_agent).

Error codes

StatusCodeCause
400EMPTY_COLUMNSThe columns array is empty
400INVALID_COLUMNA column is not in the whitelist
400INVALID_STATUSA status is not in the whitelist
400INVALID_DATE_RANGEafter or before empty, or before < after
400DATE_RANGE_TOO_LARGEbefore - after > 365 days
400INVALID_FORMATformat is neither consolidated nor separated
403User without ExportReports permission
404Order does not exist or belongs to another tenant
410EXPORT_FILE_EXPIREDFile expired (>30 days)
429Tenant rate limit exceeded

curl examples

bash
# Pre-flight estimate
curl -X POST "https://api.kuenta.co/v1/exports?dryRun=true" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Organization-ID: $ORG_ID" \
     -H "Content-Type: application/json" \
     -d '{
       "type": "reports",
       "after": "2026-01-01T00:00:00Z",
       "before": "2026-04-30T23:59:59Z",
       "format": "consolidated",
       "columns": ["date", "name", "idNumber", "status"]
     }'

# Create export
curl -X POST "https://api.kuenta.co/v1/exports" \
     -H "Authorization: Bearer $TOKEN" \
     -H "Organization-ID: $ORG_ID" \
     -H "Content-Type: application/json" \
     -d '{
       "type": "reports",
       "after": "2026-01-01T00:00:00Z",
       "before": "2026-04-30T23:59:59Z",
       "sourceKeys": ["datacredito-acierta"],
       "statuses": ["done"],
       "format": "separated",
       "columns": ["date", "name", "idNumber", "status", "rawPayload", "parsedPayload"]
     }'

# Available sources
curl -H "Authorization: Bearer $TOKEN" \
     -H "Organization-ID: $ORG_ID" \
     "https://api.kuenta.co/v1/exports/sources?type=reports"

# Download
curl -OJ \
     -H "Authorization: Bearer $TOKEN" \
     -H "Organization-ID: $ORG_ID" \
     "https://api.kuenta.co/v1/exports/01H8XYZ/file"

Operational notes

  • The job runs in an async worker; typical times: 30s-2min for <50k reports.
  • Status updates via SSE on GET /exports/sse; the frontend listens and updates the list without polling.
  • If the job fails, status="failed" and error describes the cause. You can retry by creating a new export.

Next steps