Submit an asynchronous search job to query negotiated rates across payer machine-readable files. Returns immediately with a job_id you can pass to Get Search Status to poll for completion.
You can make a request for a single Payer and Plan combination (canonically called a fee schedule), or multiple fee schedules at once.
Example request for multiple fee schedules:POST/v1/searches
{
"npis": [1144218512, 1234567890],
"billing_codes": ["99213", "99214"],
"fee_schedules": [
{ "payer": "Cigna", "plan": "PPO" },
{ "payer": "UnitedHealthcare", "plan": "Other" }
],
"testing": false
}Headers
| Header | Required | Value |
|---|---|---|
Authorization | Yes | Bearer kh_live_<your_key> — see Authentication |
Content-Type | Yes | application/json |
Idempotency-Key | Strongly recommended | A client-chosen string, up to 255 printable ASCII characters. Repeat POSTs with the same key within 24 hours return the original job_id instead of creating a duplicate billable job. See Idempotency. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
npis | integer[] | Yes | One or more 10-digit National Provider Identifier numbers. Up to 1000 per request. |
billing_codes | string[] | Yes | CPT or HCPCS billing codes to retrieve rates for. Up to 100 per request. |
fee_schedules | object[] | Yes | Filter results by payer and plan. Up to 20 per request. See fields below. |
testing | boolean | No | If true, uses development datasets instead of production data. Default false. |
fee_schedules object
| Field | Type | Description |
|---|---|---|
payer | string | Payer name (e.g. "Cigna", "Aetna", "UnitedHealthcare"). |
plan | string | Plan type (e.g. "PPO", "HMO", "Choice Plus"). |
Requests that exceed the hard per-request limits (1000 NPIs, 100 billing codes, 20 fee schedules) are rejected with 400 validation_error before any work is enqueued. See Rate Limits for the full limit table and Errors for the validation-error envelope shape.
Response
202 Accepted — the job has been queued for processing. The response includes a Location header with the status polling URL.
{
"job_id": "6f4245de-4b44-4bb6-aaae-f11c0d4f45c0",
"status": "queued",
"created_at": "2026-04-11T09:39:35.373732+00:00",
"status_url": "https://api.keeperhealth.com/v1/searches/6f4245de-4b44-4bb6-aaae-f11c0d4f45c0"
}| Field | Type | Description |
|---|---|---|
job_id | string (UUID) | Unique identifier for this search job. Pass it to Get Search Status to poll and retrieve results. |
status | string | Current job state. One of: queued, processing, completed, failed. New jobs always start in queued. See the full state machine. |
created_at | string (ISO 8601) | Timestamp when the job was created. |
status_url | string (URL) | Convenience URL for polling the job status. |
Idempotency
Strongly recommended. We strongly recommend sending an
Idempotency-Keyon everyPOST /v1/searchesrequest. Searches are billable, long-running jobs — an accidental retry without a key creates a duplicate job, duplicate work, and duplicate cost. Treat the key as mandatory in production code paths.
POST /v1/searches accepts an Idempotency-Key header. When present, repeat requests from the same organization with the same key within a 24-hour window return the original job_id (still 202, with an Idempotent-Replay: true response header) instead of creating a second job.
Use this on any retry path that might fire twice — cron jobs over flaky networks, job runners with at-least-once semantics, UI flows where a user could double-click a submit button.
curl -X POST https://api.keeperhealth.com/v1/searches \
-H "Authorization: Bearer $KEEPER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-nightly-2026-04-13" \
-d '{
"npis": [1144218512],
"billing_codes": ["99213"],
"fee_schedules": [{"payer": "Cigna", "plan": "PPO"}]
}'Requirements:
- Up to 255 printable ASCII characters
- Unique per logical request — reuse the same key for safe retries of the same intended operation, generate a new one for a new intended operation
- Scoped per organization — two different orgs using the same key are independent
Without an Idempotency-Key, every POST creates a new job, including retries. If your code might retry network failures, store the job_id after the first 202 and re-poll instead of re-POSTing.
Error responses
All error responses use the standard error envelope.
error.type | HTTP | Meaning |
|---|---|---|
validation_error | 400 | A field failed validation (e.g. empty npis, over a per-request limit). details lists each failing field. |
invalid_request | 400 | The request body is missing, not valid JSON, or the Idempotency-Key header is malformed. |
unauthorized | 401 | Missing, malformed, or invalid API key. See Authentication. |
forbidden | 403 | API key revoked or organization inactive. See Authentication. |
enqueue_failed | 503 | Transient — safe to retry. Retry-After header is set. See Rate Limits. |
internal_error | 500 | Contact support. |
See also
- Get Search Status — poll the job you just created
- Code Examples — full end-to-end client in four languages
- Rate Limits — hard per-request limits and quotas
- Errors — full error envelope taxonomy