> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsolum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Request verifications of benefits for a patient

> Send a patient, the coverage you know, and their insurance cards in one call; Solum opens a verification of benefits for every active policy.

<Note>
  This endpoint is available after the verification of benefits requests backend release is enabled in your environment. Check your environment's API reference before integrating.
</Note>

Use `POST /v1/verification-of-benefits-requests` to set up a patient's verification in one asynchronous call. Upload the insurance cards using the Files API first, then send their IDs with the patient and any coverage you already know. Solum:

1. matches the patient to an existing one, or creates them, as `POST /v1/patients/upsert` does;
2. stores the coverage you sent;
3. reads the insurance cards, completes and corrects the patient's coverage from them, and adds a coverage for a card that matches none;
4. opens a verification of benefits (VOB) for every active policy of the patient — including coverage the patient already had — and, when it opened one, moves the patient to your Needs VOB stage unless the patient is already on a stage that verifies insurance.

## Submit a request

```bash theme={null}
curl -X POST "$SOLUM_API_URL/v1/verification-of-benefits-requests" \
  -H "X-API-Key: $SOLUM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: intake-2026-001" \
  -d '{
    "patient": {
      "first_name": "Jane",
      "last_name": "Doe",
      "date_of_birth": "2015-02-03",
      "phone_number": "+15555550100",
      "state": "GA"
    },
    "policies": [
      {"insurance_name": "Aetna", "coverage_tier": "primary"}
    ],
    "file_ids": [
      "5a86bb0a-3387-4963-b3f1-91cf8b3184b1",
      "0c2f4b9e-6d7a-4c1e-9b8a-2f3e4d5c6b7a"
    ]
  }'
```

The server returns `202 Accepted` and a `Location` header pointing to the request. Acceptance means the request was saved; processing happens afterwards.

```json theme={null}
{
  "id": "3c6231dd-766e-4890-9001-ad9f2a31c3bf",
  "status": "queued",
  "patient": null,
  "policies": [],
  "verifications": [],
  "error": null,
  "created_at": "2026-09-29T12:00:00Z",
  "updated_at": "2026-09-29T12:00:00Z",
  "completed_at": null
}
```

## Request body

| Field | Required | Description |
| - | - | - |
| `patient` | One of `patient` or `patient_id` | The patient, with the fields `POST /v1/patients/upsert` takes except coverage and workflow stage: identity, contact, address, `external_id`, `custom_fields`, `tags`, `location_id`, `organization_id`, `comments`. Send a phone number, or first name, last name and date of birth, or an `external_id`. |
| `patient_id` | One of `patient` or `patient_id` | An existing active patient of your company. |
| `policies` | At least one of `policies` or `file_ids` | The coverage you know, one per `coverage_tier`: `insurance_id` or `insurance_name`, `coverage_tier`, and optionally `member_number`, `group_number`, `plan_name`, `subscriber` and `external_id`. |
| `file_ids` | At least one of `policies` or `file_ids` | 1–15 insurance-card files uploaded through the Files API, belonging to your company. Supported documents are PDF, PNG, and JPEG, with a combined size limit of 50 MB; comment attachments, message attachments, and form-submission attachments are not accepted. Front and back of a card may be separate files. |
| `specialties` | No | The specialties to verify on every VOB, as on `POST /v1/verification-of-benefits`. When omitted, each policy gets the specialties your company's configuration derives for it. |
| `priority` | No | `true` flags every VOB this request opens as a priority. Defaults to `false`. |

What you send wins over what a card says. A card fills only the fields you left out, and a card that disagrees with a `member_number` or `insurance_id` you sent is skipped (`conflicts_with_request`) rather than applied. An `insurance_id` you send is never changed. An insurance you send as `insurance_name` is different: a card with the same member ID can correct it to the insurance the card names (source `both`). The workflow stage is not sent: the patient moves to your Needs VOB stage when a VOB is opened, unless the patient is already on a stage that verifies insurance.

A coverage you send without `member_number` (or with a blank one) is completed as follows. When the patient already has exactly one policy on that insurance, and no other coverage in your request names that insurance, the coverage is that policy (unless you send an `external_id` that policy does not hold), and a card with another member ID may then correct it. Otherwise the member number is taken from the card of that insurance. With no such card the coverage is skipped as `no_member_number`; with cards carrying several member IDs for that insurance and not exactly one printed with the coverage's tier, as `ambiguous`.

## Poll for the result

```bash theme={null}
curl "$SOLUM_API_URL/v1/verification-of-benefits-requests/3c6231dd-766e-4890-9001-ad9f2a31c3bf" \
  -H "X-API-Key: $SOLUM_API_KEY"
```

Poll every few seconds, increasing the interval for requests that take longer. Reading the cards may take several minutes. Stop polling at either terminal state.

| Status | Meaning |
| - | - |
| `queued` | Accepted and waiting for processing. |
| `processing` | The cards are being read or the results written, or an internal retry is pending. |
| `completed` | The request was processed. `patient`, `policies` and `verifications` say what happened. |
| `failed` | Nothing was written. `error` says why and whether a new submission could help. |

A completed request lists IDs and codes only, never patient data:

```json theme={null}
{
  "id": "3c6231dd-766e-4890-9001-ad9f2a31c3bf",
  "status": "completed",
  "patient": {"id": "b1d2c3e4-0000-4000-8000-000000000001", "action": "created"},
  "policies": [
    {"id": "b1d2c3e4-0000-4000-8000-000000000002", "action": "created", "reason": null, "source": "both"},
    {"id": null, "action": "skipped", "reason": "unresolved_payer", "source": "card"}
  ],
  "verifications": [
    {
      "id": "b1d2c3e4-0000-4000-8000-000000000003",
      "patient_policy_id": "b1d2c3e4-0000-4000-8000-000000000002",
      "status": "opened",
      "reason": null,
      "left_out": []
    }
  ],
  "error": null,
  "created_at": "2026-09-29T12:00:00Z",
  "updated_at": "2026-09-29T12:02:00Z",
  "completed_at": "2026-09-29T12:02:00Z"
}
```

`patient.action` is `created`, `updated` or `matched` (found, nothing changed).

Each entry of `policies` is one of the patient's active policies, or one input that was not stored:

| `action` | Meaning |
| - | - |
| `created` | A new policy. |
| `corrected` | A policy the patient already had whose member ID changed (`new_member_number`) or whose insurance changed (`corrected_insurance`, or `specific_insurance` when a card sharpens a generic insurance to the specific one it prints — always a card's correction). `source` says who made the change: your policy, a card, or both. A card with another member ID corrects the patient's policy on the card's insurance when the patient has exactly one there and the card prints no tier or that policy's tier, unless you sent that policy's `member_number`. A card with the same member ID corrects an insurance you sent by name. A policy this request created reads `created` even when a card corrected it. |
| `filled` | Your data or a card updated fields other than the insurance and member ID. |
| `matched` | Your coverage or a card named it; nothing changed. |
| `unchanged` | Nothing in the request named it (`reason` is `tier_moved` when another coverage took its tier). |
| `skipped` | A coverage or card that was not stored; `id` is null and `reason` says why. |

`source` is `request`, `card` or `both`, and null for a policy nothing in the request named. `both` means your data and a card both contributed, whatever the action: a `created` policy with source `both` may be one where a card corrected the insurance you named. A `skipped` entry's `reason` is one of `unresolved_payer` (no insurance of yours, ready for verification, matched the name or the card), `ambiguous`, `no_member_number`, `tiers_full` (the patient has four coverages already), `conflicts_with_request` (a card disagreed with a `member_number` or `insurance_id` you sent), `duplicate_policy`, `external_id_taken` or `not_saved`. Identical cards count once.

`ambiguous` covers three situations: several of your insurances matched a name you sent; a coverage you sent without `member_number` whose insurance has several member IDs on the cards and no single one printed with its tier; or a card Solum cannot place on one policy — the patient has two or more policies on the card's insurance and the card doesn't say which (neither its member ID nor a printed tier picks one), the card's member ID is on more than one of the patient's policies, or several cards compete for one policy. Solum leaves the policies as they are and reports the coverage or the card as skipped.

Each entry of `verifications` is one active policy: `opened` (a VOB was opened), `already_open` (the policy has an open VOB; `id` names it) or `not_requested` (none could be: `reason` is `no_service_type` when no specialty is configured, `specialties_unresolved` when none resolved). `left_out` lists specialties that could not be requested — the VOB is opened with the rest, its entry's `reason` is `specialties_unresolved`, and the VOB carries a comment from Solum saying what is missing.

A failed request:

```json theme={null}
{
  "id": "3c6231dd-766e-4890-9001-ad9f2a31c3bf",
  "status": "failed",
  "patient": null,
  "policies": [],
  "verifications": [],
  "error": {
    "code": "files_unreadable",
    "message": "The insurance cards could not be read.",
    "retryable": true,
    "fields": []
  },
  "created_at": "2026-09-29T12:00:00Z",
  "updated_at": "2026-09-29T12:09:00Z",
  "completed_at": "2026-09-29T12:09:00Z"
}
```

| `error.code` | Meaning |
| - | - |
| `files_unreadable` | The insurance cards could not be read, including when a card file was removed before Solum read it. A new submission may succeed (`retryable: true`). |
| `patient_conflict` | The patient conflicts with another patient record; the cards name another person than the patient, whether you sent `patient_id` or `patient` (a date of birth that disagrees on both sides, or a card name sharing no word or initial with the patient's); or filling in the patient's blank name and date of birth from a card would match another patient of yours. |
| `validation_failed` | The patient could not be created from what was sent, the selected `patient_id` is no longer active, a card file belongs to another patient (with `patient`; with `patient_id` it is refused at submission), a card file was removed after Solum read the cards, or the submitting account lost access. |
| `invalid_operation_payload` | The stored request could not be read for processing. Submit a new request. |
| `operation_failed` / `operation_timeout` | Processing failed after its retries. A new submission may succeed. |

A failed request is terminal. To correct it, submit a new request with a new `Idempotency-Key`.

## Errors at submission

Invalid input is rejected synchronously and nothing is stored:

| Status | When |
| - | - |
| `401` / `403` | Authentication failed. |
| `404` | A file, patient, insurance, location, organization, external ID type or credential is not an active one of your company. |
| `409` | `idempotency_key_reused`: the key was used with a different body. |
| `422` | An unknown field anywhere in the body (unlike `POST /v1/patients/upsert`, which ignores unknown fields), including the upsert's own `workflow_stage_id`, `policies`, `patient_id` and `source_extraction_id` inside `patient`; neither `policies` nor `file_ids`; both or neither of `patient` and `patient_id`; a `patient` with no `external_id`, no phone number and no first name, last name and date of birth; `policies: null`; an empty `specialties` list; more than 15 files, duplicate `file_ids`, or an unsupported file type; a file that isn't an attachable document (comment, message and form-submission attachments aren't accepted); files over the combined size limit; an unknown service type or a service type sent twice in `specialties`; two coverages on one tier; the same `external_id` on two policies; the same `insurance_id` and `member_number` on two policies; with `patient_id`, a card file that belongs to another patient; an empty `Idempotency-Key` header. Two policies whose `insurance_name` resolves to the same insurance with the same member number are not rejected here: the request completes with the policies you sent skipped as `duplicate_policy`. |

## Retry submissions safely

`Idempotency-Key` is optional and scoped to your company. Repeating the same key and body returns the original request and its current status; the same key with a different body returns `409`. Use the same key when retrying after a network timeout. Without a key, every submission is a new request.

Completion is available through polling; there is no completion webhook. The usual webhooks still fire: `patient_created` or `patient_updated`, `vob_created` for each VOB opened, and `patient_insurance_verification` when the request moves the patient to your Needs VOB stage and that stage verifies insurance. A patient kept on your Missing Information stage by another VOB is not moved. The comments Solum adds on a VOB — the specialties it left out, or that the request changed a policy whose VOB waits on missing information — do not trigger a `vob_comment_created` webhook or its emails.
