# Download a bulk consistency job's results

`GET /api/v1/consistency/bulk/{jobId}/results`

- Authentication: Secret API key as a bearer token
- Billing: Free.
- Group: Consistency

The result file as CSV: one row per input row, in input order. A run created from an uploaded file carries that file's own columns first, then the four signals as submitted and the country they were measured against, then the comparison.

The comparison columns are `signals_requested` and `signals_answered` (`;`-joined, in the order the legs ran), `country_agreement`, `flags` (`;`-joined), one cell per comparison — `phone_address_country`, `ip_address_country`, `ip_phone_country`, `email_ip_disposable_datacenter`, `email_ip_disposable_anonymous`, `email_address_free_provider_mail_drop`, `email_phone_disposable` — and `distance_km`. Last come the few per-product fields those comparisons read, so a difference can be checked against the values that decided it, and `credits_used`.

**A comparison cell reads `agree`, `differ`, `not_compared`, `missing_value`, `low_confidence`, or nothing at all.** An empty cell means the comparison was never attempted, because the row did not carry both of its signals. The two withheld reasons are written in place of a bare `not_compared` because they mean different things and a spreadsheet has nowhere else to put them: `missing_value` is a field the product did not evaluate — which means "not evaluated" and never "no" — and `low_confidence` is an IP address whose country belongs to the network operator rather than to the person, a VPN exit, a relay or a datacenter egress. `distance_km` is evidence only and never raises a flag: there is no honest distance at which a person stops being themselves.

`signals_requested` against `signals_answered` is also where a row cut short says so: a run that stopped on an empty balance part-way through a row keeps the legs it had already paid for, and that row answers fewer signals than it asked for.

Add `?variant=flagged|unflagged|uncompared` to download only the rows a flag was raised on, the rows that were compared and raised none, or the rows too thin to compare; the default `full` returns everything. Available once the job is `completed`, or `cancelled` or `failed` with at least one processed row — those rows were billed, so they stay downloadable. A job still running answers `409 RESULTS_NOT_READY`, a cancelled job with nothing processed `409 JOB_CANCELLED`, and a failed job with nothing processed `409 JOB_FAILED`.

`format=json` answers the same rows, filtered the same way, as objects keyed by the result file's own header instead of a CSV attachment. It is streamed row by row exactly as the CSV is. A CSV cell carries no type, so only the two the writer encodes come back typed: an empty cell is `null` and `true`/`false` are booleans. Everything else is the string as written, and the columns of an uploaded file come back exactly as they were uploaded.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `jobId` | path | integer | yes |  |
| `variant` | query | string | no | Restrict the file to the rows a flag was raised on, the rows compared with none, or the rows fewer than two signals answered for. Defaults to full. One of: full, flagged, unflagged, uncompared. |
| `format` | query | string | no | `csv` streams the file as an attachment; `json` streams the same rows as objects. One of: csv, json. Default: csv. |

## Example request

```bash
curl https://spaw.co/api/v1/consistency/bulk/{jobId}/results \
  -H "Authorization: Bearer sk_live_…"
```

## Responses

### 200 — The result rows, as a CSV attachment or as streamed JSON.

```csv
email,phone,ip,address,country,signals_requested,signals_answered,country_agreement,flags,phone_address_country,ip_address_country,ip_phone_country,email_ip_disposable_datacenter,email_ip_disposable_anonymous,email_address_free_provider_mail_drop,email_phone_disposable,distance_km,email_deliverable,email_disposable,email_free_provider,phone_valid,phone_country,phone_is_disposable,ip_country,ip_risk_level,ip_is_datacenter,ip_is_anonymous,address_valid,address_country,address_is_mail_drop,credits_used
ada@acme.com,+442079460018,81.2.69.142,"10 Downing Street, London, SW1A 2AA",GB,ip;phone;address;email,ip;phone;address;email,agree,,agree,agree,agree,not_compared,not_compared,not_compared,not_compared,12.4,true,false,false,true,GB,false,GB,low,false,false,true,GB,false,3
```

### 401 — The key is missing, malformed, or revoked.

```json
{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 404 — No such record on this account.

```json
{
    "success": false,
    "error": {
        "code": "NOT_FOUND",
        "message": "No record with that id on this account.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 409 — The job is still running (`RESULTS_NOT_READY`), or was cancelled or failed before any row was processed (`JOB_CANCELLED`, `JOB_FAILED`).

```json
{
    "success": false,
    "error": {
        "code": "RESULTS_NOT_READY",
        "message": "The results are not ready yet.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

### 429 — Over 5 requests per second for the key. Retry after the limit resets.

```json
{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}
```

## Error codes

- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `NOT_FOUND` — https://spaw.co/docs/errors/NOT_FOUND
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RESULTS_NOT_READY` — https://spaw.co/docs/errors/RESULTS_NOT_READY
- `JOB_CANCELLED` — https://spaw.co/docs/errors/JOB_CANCELLED
- `JOB_FAILED` — https://spaw.co/docs/errors/JOB_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED

---

Canonical page: https://spaw.co/docs/api/download-consistency-bulk-results · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
