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

# Track referral status

> Every status a referral moves through, how status and notification changes reach you, and which ones to act on

A referral changes status many times between the moment you create it and the
moment a report is ready. Status changes are not a post-scan event: most of them
happen while we are still authorizing, contacting the patient, and scheduling.

This page lists every status you can receive, explains what each one means, and
says which ones are worth wiring logic to.

## Two different status fields

The API returns `status` in two places and they describe different things.

| Field                | Describes                                                           | Use it for                                           |
| -------------------- | ------------------------------------------------------------------- | ---------------------------------------------------- |
| `visits[].status`    | Where an individual visit is in scheduling, the exam, and reporting | Integration logic. This is what the webhook reports. |
| `status` (top level) | The referral's case and billing state                               | Reference only. Do not key scheduling logic off it.  |

A referral with two visits tracks each one separately, so a referral can have
one `booked` visit and one still waiting on authorization.

## The visit lifecycle

```mermaid theme={"dark"}
flowchart TD
  pending["pending"] --> pending_auth["pending_auth"]
  pending --> co_signed["co_signed"]
  pending --> ready_to_book["ready_to_book"]
  pending_auth --> ready_to_book
  co_signed --> ready_to_book
  ready_to_book --> intake_sent["intake_sent"]
  intake_sent --> ready_to_book
  ready_to_book --> contacted_for_booking["contacted_for_booking"]
  ready_to_book --> sent_to_rp["sent_to_rp"]
  contacted_for_booking --> booked["booked"]
  contacted_for_booking --> sent_to_rp
  booked --> complete["complete"]
  booked --> exam_not_completed["exam_not_completed"]
  booked --> canceled["canceled"]
  complete --> sent_to_rad["sent_to_rad"]
  sent_to_rad --> report_uploaded["report_uploaded"]
  report_uploaded --> qc_report_ready["qc_report_ready"]
  qc_report_ready --> invoice_ready["invoice_ready"]
```

Not every referral takes every hop. The gates before `ready_to_book` depend on
the line of business, and a visit can leave the happy path at any point.

### Before booking

| Status                  | What it means                                                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `pending`               | Referral received. We are checking the physician order and patient details before scheduling starts.                     |
| `pending_auth`          | Waiting on authorization or coverage approval from the payer. Common on workers' compensation and health plan referrals. |
| `co_signed`             | Waiting on a countersigned letter of protection from the referring law firm.                                             |
| `intake_sent`           | Safety and intake questions have gone to the patient and we are waiting on answers.                                      |
| `ready_to_book`         | Everything we need is on file and the visit is queued for scheduling. Your portal shows this as patient outreach.        |
| `contacted_for_booking` | We are in contact with the patient to agree on a time and a center.                                                      |

### Scheduled

| Status   | What it means                                                                             |
| -------- | ----------------------------------------------------------------------------------------- |
| `booked` | The appointment is confirmed. `visits[].location` and `visits[].start_time` are both set. |

### At and after the exam

| Status               | What it means                                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------------- |
| `complete`           | The patient attended and the exam was performed.                                                         |
| `exam_not_completed` | The patient did not attend or the exam could not be performed.                                           |
| `sent_to_rad`        | Images are with the radiologist for reading.                                                             |
| `report_uploaded`    | A radiology report is available. On a referral with several procedures, others may still be outstanding. |
| `qc_report_ready`    | The report has passed quality control.                                                                   |
| `invoice_ready`      | Reporting is finished for the visit. Your portal shows all reports as ready.                             |

### Other outcomes

| Status       | What it means                                                                                                                                                                               |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sent_to_rp` | Returned to the referring provider because we could not move it forward, for example we could not reach the patient or the order needs correcting. This one needs action from the provider. |
| `canceled`   | The visit was canceled and no exam will take place.                                                                                                                                         |

## Which statuses to act on

Three moments usually matter to an integration:

1. **`booked`** — tell the patient where and when. Read `visits[].location` and
   `visits[].start_time` from the same payload.
2. **`report_uploaded`** — a report exists. Pull the PDF with
   [GET /api/v2/order\_documents/report](/api-reference/order-documents/returns-the-radiology-report-pdf-for-an-order).
3. **`sent_to_rp`** — someone on your side needs to unblock the referral.

Visit `invoice_ready` means reporting is finished for that visit. It is **not**
the signal to pull payments. Payments are a referral-level concern: once the
top-level `status` is no longer `pending` or `canceled`, partners with payment
read enabled can keep listing them with
[`GET /api/v2/referrals/{referral_id}/payments`](/api-reference/referrals/lists-referral-payments).
New payments can arrive months later. The workers' compensation example is on
[Workers' compensation and payers](/guides/lines-of-business/workers-comp-and-payers#pull-payments).

Treat the rest as progress information you can surface but should not gate on.
Statuses never move backwards through the reporting phases, so you can store the
latest one you have seen. New statuses may be added over time, so ignore any
string you do not recognize rather than failing.

## Referral-level status

The top-level `status` tracks the case commercially rather than clinically.
These are the values with a meaning outside Scan:

| Status          | What it means                                                          |
| --------------- | ---------------------------------------------------------------------- |
| `pending`       | Open referral. This is the normal value from create through reporting. |
| `invoice_ready` | The case is ready to invoice.                                          |
| `invoice_sent`  | The invoice has been issued.                                           |
| `closed`        | The case is closed and no further activity is expected.                |
| `canceled`      | The referral was canceled.                                             |

Other values may appear on this field. They are internal accounts-receivable
states and carry no integration meaning, so do not branch on them.

`invoice_ready` appears at both levels but means different things. On
`visits[].status`, reporting is complete for that visit. On the top-level
`status`, the referral's case is ready for billing.

Payment pulls belong on the referral-level field. Skip `pending` and `canceled`.
`invoice_ready`, `invoice_sent`, `closed`, and the other accounts-receivable
values are when listing payments makes sense, and new rows can still appear
months after the first invoice-ready event.

## Receiving changes

You have two options, and they work well together: subscribe to the webhook for
push updates, and poll when you need a snapshot on demand.

### Webhook

We POST to the `webhook_url` on your `api_credentials` record every time a visit
status changes. The full payload is on
[Status changed](/webhooks/status-changed), and [Webhooks](/guides/webhooks)
covers the event catalog, retries, and delivery ordering across all three
events.

| Header             | Value                                    |
| ------------------ | ---------------------------------------- |
| `Content-Type`     | `application/json`                       |
| `User-Agent`       | `Scan.com-Webhook/1.0`                   |
| `X-Scan-Event`     | `visit.status_changed`                   |
| `X-Scan-Timestamp` | Unix timestamp used in the signature     |
| `X-Scan-Signature` | `v1=` followed by the HMAC-SHA256 digest |

#### Verify the signature

Read the request body as raw bytes before parsing JSON. Compute HMAC-SHA256 over
`"{timestamp}.{raw_body}"` with the signing secret shown on the API access tab,
then compare it to the digest after `v1=` using a constant-time comparison.
Reject timestamps older than five minutes to limit replay attacks.

```ruby theme={"dark"}
timestamp = request.headers.fetch("X-Scan-Timestamp")
provided = request.headers.fetch("X-Scan-Signature").delete_prefix("v1=")
payload = "#{timestamp}.#{request.raw_post}"
expected = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("SCAN_WEBHOOK_SECRET"), payload)

halt 401 unless OpenSSL.secure_compare(provided, expected)
halt 401 if (Time.now.to_i - Integer(timestamp)).abs > 300
```

The body carries a stable `event_id`, the new `status`, a `changes.status` array
of `[previous, current]`, the `visit_id`, an `occurred_at` timestamp, and a full
`referral` snapshot so you rarely need a follow-up call.

```json theme={"dark"}
{
  "event_id": "55555555-5555-5555-5555-555555555555",
  "event_name": "visit.status_changed",
  "status": "booked",
  "changes": { "status": ["contacted_for_booking", "booked"] },
  "visit_id": "11111111-1111-1111-1111-111111111111",
  "occurred_at": "2026-05-28T18:00:00Z",
  "referral": { "id": "22222222-2222-2222-2222-222222222222" }
}
```

Return HTTP 2xx as soon as you have stored the event, and do the rest of your
work afterward. We retry failed deliveries, so the same event can arrive more
than once — use `event_id` to discard duplicates.

### Polling

Ask for the current state whenever you need it.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.staging.scan.com/api/v2/referrals/$REFERRAL_ID" \
    -H "Authorization: Bearer $SCAN_API_TOKEN"
  ```

  ```ruby Ruby theme={"dark"}
  require "net/http"
  require "uri"

  uri = URI("https://api.staging.scan.com/api/v2/referrals/#{ENV.fetch("REFERRAL_ID")}")
  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{ENV.fetch("SCAN_API_TOKEN")}"

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  response = http.request(request)

  visit_statuses = JSON.parse(response.body)["visits"].map { |visit| visit["status"] }
  ```

  ```python Python theme={"dark"}
  import os
  import requests

  response = requests.get(
      f"https://api.staging.scan.com/api/v2/referrals/{os.environ['REFERRAL_ID']}",
      headers={"Authorization": f"Bearer {os.environ['SCAN_API_TOKEN']}"},
  )
  visit_statuses = [visit["status"] for visit in response.json()["visits"]]
  ```

  ```javascript JavaScript theme={"dark"}
  const response = await fetch(
    `https://api.staging.scan.com/api/v2/referrals/${process.env.REFERRAL_ID}`,
    { headers: { Authorization: `Bearer ${process.env.SCAN_API_TOKEN}` } },
  );

  const { visits } = await response.json();
  const visitStatuses = visits.map((visit) => visit.status);
  ```
</CodeGroup>

Full payload:
[Returns a referral by ID](/api-reference/referrals/returns-a-referral-by-id).

Do not poll for the internal steps between milestones. Wait for the next webhook
or take a snapshot when your own workflow needs one.

## Notification delivery

Separately from visit status, you can track the emails and texts Scan sends the
patient about a referral, such as intake questions, booking confirmations, and
appointment reminders. This is delivery metadata only. Recipients, subjects,
message bodies, and provider failure text are never returned on either route.

Both routes are off by default and are enabled independently of each other and
of visit-status delivery, so you can take one without the other. Ask Scan to
turn on what you need:

| Capability            | What it gives you                                                                                                            |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Notification read     | [`GET /api/v2/referrals/{referral_id}/notifications`](/api-reference/referrals/lists-referral-notification-delivery-history) |
| Notification webhooks | `notification.status_changed` deliveries to your webhook URL                                                                 |

### Delivery statuses

`pending` means we have queued the message and `dispatched` means we have handed
it to the email or SMS provider. Everything after that comes from the provider's
own callbacks, so the two channels do not share a vocabulary.

| Status              | Channel    | What it means                                         |
| ------------------- | ---------- | ----------------------------------------------------- |
| `pending`           | email, sms | Queued by Scan, not yet handed over.                  |
| `dispatched`        | email, sms | Accepted by the delivery provider.                    |
| `sent`              | email, sms | The provider reported it as sent.                     |
| `delivered`         | email, sms | The provider confirmed it reached the recipient.      |
| `opened`            | email      | The recipient opened the email.                       |
| `temporary_failure` | email      | Soft bounce. The address may accept mail later.       |
| `permanent_failure` | email      | Hard bounce. Stop expecting delivery to this address. |
| `failed`            | sms        | The message could not be delivered.                   |

How far a message gets depends on what the provider reports back, so not every
notification reaches every status. As with visit status, ignore any value you do
not recognize rather than failing.

### Webhook

Notification events go to the same `webhook_url` and are signed the same way, so
the [verification code above](#verify-the-signature) works unchanged. The event
header is what differs:

| Header         | Value                         |
| -------------- | ----------------------------- |
| `X-Scan-Event` | `notification.status_changed` |

You receive one event when the notification is created and one for every status
change after it. On that first event `changes.status` is `[null, "pending"]`.

```json theme={"dark"}
{
  "event_id": "66666666-6666-6666-6666-666666666666",
  "event_name": "notification.status_changed",
  "referral_id": "22222222-2222-2222-2222-222222222222",
  "status": "delivered",
  "changes": { "status": ["sent", "delivered"] },
  "notification": {
    "id": "44444444-4444-4444-4444-444444444444",
    "channel": "sms",
    "notification_type": "hard_confirmation_to_patient",
    "status": "delivered",
    "created_at": "2026-09-21T18:00:00Z",
    "updated_at": "2026-09-21T18:00:08Z",
    "visit_id": "11111111-1111-1111-1111-111111111111"
  },
  "occurred_at": "2026-09-21T18:00:08Z"
}
```

Delivery is at least once, so deduplicate on `event_id`.
Full payload:
[Notification status changed](/webhooks/notification-status-changed).

### Pull the history

Ask for every notification attached to a referral and its visits, newest first.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl "https://api.staging.scan.com/api/v2/referrals/$REFERRAL_ID/notifications" \
    -H "Authorization: Bearer $SCAN_API_TOKEN"
  ```

  ```ruby Ruby theme={"dark"}
  require "net/http"
  require "uri"

  uri = URI("https://api.staging.scan.com/api/v2/referrals/#{ENV.fetch("REFERRAL_ID")}/notifications")
  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{ENV.fetch("SCAN_API_TOKEN")}"

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  response = http.request(request)

  notifications = JSON.parse(response.body)["referral_notifications"]
  ```

  ```python Python theme={"dark"}
  import os
  import requests

  response = requests.get(
      f"https://api.staging.scan.com/api/v2/referrals/{os.environ['REFERRAL_ID']}/notifications",
      headers={"Authorization": f"Bearer {os.environ['SCAN_API_TOKEN']}"},
  )
  notifications = response.json()["referral_notifications"]
  ```

  ```javascript JavaScript theme={"dark"}
  const response = await fetch(
    `https://api.staging.scan.com/api/v2/referrals/${process.env.REFERRAL_ID}/notifications`,
    { headers: { Authorization: `Bearer ${process.env.SCAN_API_TOKEN}` } },
  );

  const { referral_notifications: notifications } = await response.json();
  ```
</CodeGroup>

```json theme={"dark"}
{
  "referral_notifications": [
    {
      "id": "44444444-4444-4444-4444-444444444444",
      "channel": "sms",
      "notification_type": "hard_confirmation_to_patient",
      "status": "delivered",
      "created_at": "2026-09-21T18:00:00Z",
      "updated_at": "2026-09-21T18:00:08Z",
      "visit_id": "11111111-1111-1111-1111-111111111111"
    }
  ],
  "meta": {
    "current_page": 1,
    "next_page": null,
    "prev_page": null,
    "total_pages": 1,
    "total_count": 1
  }
}
```

`visit_id` is set when the message is about a specific visit and `null` when it
is about the referral as a whole. Results are paginated with `page` and
`per_page`, which defaults to 20.
