Retrieve the current status of a search job created via Create Search. Safe to call repeatedly. Once status is "completed", the response includes a download_urls array of pre-signed URLs — one per Parquet shard. See the Reimbursement Data Schema for what's inside.
Example request:GET/v1/searches/{job_id}
GET requests have no body — pass the job_id in the URL path and the API key in the Authorization header.
curl https://api.keeperhealth.com/v1/searches/6f4245de-4b44-4bb6-aaae-f11c0d4f45c0 \
-H "Authorization: Bearer $KEEPER_API_KEY"The three possible successful response shapes — queued/processing, completed, and failed — are shown below.
HTTP status is always
200. A successful request to this endpoint always returns HTTP200, regardless of whether the job itself isqueued,processing,completed, orfailed. The HTTP code reflects "the status check succeeded" — the job's actual state lives in the response body'sstatusfield. Read it, not the HTTP code, to detect job failure.
Headers
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer kh_live_<your_key> — see Authentication |
Path Parameters
| Parameter | Type | Description |
|---|---|---|
job_id | string (UUIDv4) | The job identifier returned from Create Search. |
Status values
status | Meaning |
|---|---|
queued | Accepted and waiting for a worker. Usually flips to processing within seconds. |
processing | Worker is running the search against BigQuery. |
completed | Terminal. download_urls and expires_at are set. |
failed | Terminal. The error field contains a human-readable failure reason. |
completed and failed are terminal — once a job reaches either, it will never change state again. Stop polling as soon as you see one. Poll every 15–30 seconds; see Rate Limits for the recommended cadence, and Code Examples for complete polling loops in four languages.
Response — queued / processing
200 OK
{
"job_id": "6f4245de-4b44-4bb6-aaae-f11c0d4f45c0",
"status": "processing",
"created_at": "2026-04-11T09:39:35.736000+00:00",
"status_url": "https://api.keeperhealth.com/v1/searches/6f4245de-..."
}Response — completed
200 OK
{
"job_id": "6f4245de-4b44-4bb6-aaae-f11c0d4f45c0",
"status": "completed",
"created_at": "2026-04-11T09:39:35.736000+00:00",
"completed_at": "2026-04-11T09:40:18.847000+00:00",
"download_urls": [
"https://storage.googleapis.com/...&X-Goog-Signature=...",
"https://storage.googleapis.com/...&X-Goog-Signature=..."
],
"expires_at": "2026-04-11T10:40:39.343468+00:00",
"status_url": "https://api.keeperhealth.com/v1/searches/6f4245de-..."
}| Field | Type | Description |
|---|---|---|
download_urls | string[] (URLs) | List of pre-signed URLs, one per Parquet shard. Always an array — length is 1 or more, even for single-file results. No authentication header is required on the downloads themselves. Only present when status is "completed". |
expires_at | string (ISO 8601) | Shared expiration time for every URL in download_urls (~1 hour after they were minted). Re-poll the status endpoint to obtain a fresh set after expiry. |
completed_at | string (ISO 8601) | Timestamp when job processing finished. Only present when status is "completed". |
Why multiple URLs? BigQuery shards large result sets into multiple Parquet files on export (each shard is capped at 1 GB). Every file has the same schema; together they are the complete result. Always iterate
download_urls— do not assume a length. Readers like DuckDB, pandas (pd.read_parquet([...])), and pyarrow accept a list of Parquet files as a single dataset.
Download URLs expire. The pre-signed URLs are valid for approximately 1 hour and all share the same
expires_at. If the links have expired, simply poll the status endpoint again to receive a fresh set. Do not cache or persist signed URLs — always re-fetch them via this endpoint.
Response — failed
200 OK
{
"job_id": "6f4245de-4b44-4bb6-aaae-f11c0d4f45c0",
"status": "failed",
"created_at": "2026-04-11T09:39:35.736000+00:00",
"status_url": "https://api.keeperhealth.com/v1/searches/6f4245de-...",
"error": "Payer 'FooBar' not supported"
}| Field | Type | Description |
|---|---|---|
error | string | Human-readable reason the job failed. Only present when status is "failed". |
Failed jobs are terminal. Read the error field, correct the underlying issue, and submit a corrected request as a new job via Create Search.
Error responses
All error responses use the standard error envelope.
error.type | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, malformed, or invalid API key. See Authentication. |
forbidden | 403 | API key revoked or organization inactive. See Authentication. |
not_found | 404 | The job_id does not exist, or belongs to a different organization. These cases are intentionally indistinguishable to avoid confirming the existence of other customers' jobs. |
internal_error | 500 | Contact support. |
See also
- Create Search — submit the job you're polling here
- Reimbursement Data Schema — columns you'll find in each downloaded shard
- Code Examples — complete polling loops with error handling
- Rate Limits — polling cadence and
Retry-Afterhandling - Errors — full error envelope taxonomy