Create a reconciliation report export task (asynchronous; produces xlsx)
x-idempotency-key
Why an asynchronous task instead of a synchronous download
A month's entries can easily reach a hundred thousand rows; synchronous generation would exceed CPU limits, appearing to users as a click with no response. This endpoint therefore only creates a task (201 + status: queued). Retrieve the file from GET /v1/reports/{id}/content.
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-idempotency-key |
string | Required | UUID v4. Within 24 hours, the same key and body replay the original 201
unchanged, with X-Idempotent-Replay: true, without creating another task.
⚠ After 24 hours, the same key really creates a new task, consuming an in-progress slot.
This business line has no second idempotency layer. Use a new key for each export. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | Required | daily / pnl / statements / line:<line>.
Case-sensitive; unrecognized values return invalid_request. |
days |
integer | Optional | Window length in days, 1..180, default 30.
Out-of-range or nonnumeric values silently fall back to 30. Use the returned days as authoritative. |
Response
status is always queued. days is the effective value.{
"id": "rep_4c8a10d2-91f7-4a3e-b0c6-52d8e7f19b34",
"kind": "statements",
"status": "queued",
"days": 30,
"created_at": "2026-08-13T05:00:00.000Z"
}invalid_request: body is not JSON or kind is unrecognized, including an unregistered
line:<line>.
limit_exceeded: 3 tasks are already in progress
(limit_type: report_exports_in_flight · limit_scope: merchant).insufficient_scope: missing merchant:read.idempotency_key_reused · idempotency_in_progressAdditional Details
Calls advance the queue; scheduled processing recovers stalled tasks
Task creation advances processing once in the background of the request. GET /v1/reports/{id} also advances it once. Small reports often finish before the first poll. A sweep runs every 5 minutes as well, so tasks are eventually completed even without polling, just more slowly.
This business line emits no webhooks; poll for readiness. Inventing a report.ready event would only document an event that nothing emits.
kind: unknown values are rejected immediately
kind | Meaning |
|---|---|
daily | Daily summaries by date × business line × asset |
pnl | Totals by business line over the entire window |
line:<line> | Same as above, restricted to one line |
statements | Business debits per order |
<line> is one of eight values: qrpay · remit · card · earn · exchange · withdraw · transfer · deposit. Unrecognized kind values immediately return 400 invalid_request. Accepting them would create a task only for the generator to fail after a 30-second wait; 400 immediately identifies the invalid name.
days: out-of-range values silently fall back to 30 without an error
Valid range: 1..180. An unbounded range would expose full-table scans. Nonnumeric input, 0, and 200 do not return an error; all use 30. ⚠ Read days in the 201 response for the effective window, not the number you supplied.
Maximum 3 in-progress tasks
When queued + building ≥ 3, returns 400 limit_exceeded with limit_type: report_exports_in_flight and limit_scope: merchant. Each export aggregates the whole window. Without this gate, an incorrect loop could let your report queue consume the Worker's entire CPU budget and impair your own order endpoints.
Why merchant:read rather than a new scope
Exported data is the same data already accessible through this scope, including statements and per-line aggregates, in a different format. A new scope would create two permissions for the same data without a clear way to determine which one is missing when downloads fail. It is a POST that does not move money, following the same criterion as POST /v1/merchant/deposit-addresses.
curl -X POST 'https://api.zinfra.vip/v1/reports' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"kind": "statements",
"days": 30
}'const res = await fetch("https://api.zinfra.vip/v1/reports", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"kind": "statements",
"days": 30
}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/reports",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"kind": "statements",
"days": 30
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/reports",
strings.NewReader(`{
"kind": "statements",
"days": 30
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// Decode amount fields as string, not float64.HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zinfra.vip/v1/reports"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"kind": "statements",
"days": 30
}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/reports');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"kind": "statements",
"days": 30
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"id": "rep_4c8a10d2-91f7-4a3e-b0c6-52d8e7f19b34",
"kind": "statements",
"status": "queued",
"days": 30,
"created_at": "2026-08-13T05:00:00.000Z"
}