> ## 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.

# Authentication

> Authenticate with an API token on every request

Partner API v2 uses a Bearer token. Scan issues the token from **Get API keys**
(sandbox) or from the **API access** tab (production). Send it on
every `/api/v2` request:

```
Authorization: Bearer <api_token>
```

There is no sign-in step. The token is the credential.

## Environments

|           | Sandbox                                                           | Production                    |
| --------- | ----------------------------------------------------------------- | ----------------------------- |
| Host      | `https://api.staging.scan.com/api/v2`                             | `https://api.scan.com/api/v2` |
| Keys      | [Get API keys](https://admin.staging.scan.com/partner/api_access) | **API access** tab            |
| Data      | Synthetic only                                                    | Real PHI under your BAA       |
| Allowance | 2,000 **successful** requests or 7 days                           | Issued per contract           |

The docs navbar **Get API keys** button always opens the **sandbox** form.
Production tokens are never issued from that page.

Use the matching host for the token. A sandbox token against production (or the
reverse) returns `401`.

## Sandbox tokens

Use **Get API keys** with a company email to receive a sandbox `api_token`.
Consumer and disposable email domains are not eligible, and each company
domain can receive one sandbox token. Store the token in a secret manager:

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

The initial sandbox allows **2,000 successful authenticated requests** and expires after
**7 days**, whichever happens first. Requests that return `4xx` or `5xx` do not
consume the allowance. Use synthetic data only.
The sandbox is updated frequently — retry transient errors before escalating.

An optional access code entered on the initial **Get API keys** form skips the
trial and issues an unlimited 90-day sandbox token. Without a code, Scan reviews
extension requests and can extend the same token to an unlimited 90-day term.
Production credentials are issued separately.

Some endpoints are disabled until Scan enables them for your account, including
payment read, notification read, auto-book, and FHIR. Calls to those endpoints
return `403` with the error envelope (`code` `endpoint_not_enabled`).

The token is shown once and cannot be recovered. If it is lost or exposed,
contact [api@scan.com](mailto:api@scan.com) to revoke it and issue a replacement.
Never put it in browser code, URLs, logs, or chat.

### Sandbox limit responses

Once the sandbox window elapses or the request cap is consumed, `/api/v2`
endpoints stop returning data and respond with one of the following. Treat
either response as the signal to contact Scan.com for an extension.

| Status                  | Body                                   | Meaning                                            |
| ----------------------- | -------------------------------------- | -------------------------------------------------- |
| `401 Unauthorized`      | `code` `api_token_expired`             | The 7-day sandbox window has elapsed.              |
| `429 Too Many Requests` | `code` `sandbox_request_limit_reached` | The 2,000-successful-request allowance is used up. |

Both use the envelope in [Errors and limits](/guides/errors): `error`, `message`,
`type`, `code`, `request_id`, and `documentation_url`.

## Call an authenticated endpoint

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl https://api.staging.scan.com/api/v2/referrals \
    -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")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{ENV.fetch("SCAN_API_TOKEN")}"

  response = http.request(request)
  puts response.body
  ```

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

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

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

  const referrals = await response.json();
  ```

  ```php PHP theme={"dark"}
  $ch = curl_init("https://api.staging.scan.com/api/v2/referrals");
  curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SCAN_API_TOKEN")],
    CURLOPT_RETURNTRANSFER => true,
  ]);

  $response = curl_exec($ch);
  ```

  ```go Go theme={"dark"}
  package main

  import (
    "io"
    "net/http"
    "os"
  )

  func main() {
    req, _ := http.NewRequest(
      http.MethodGet,
      "https://api.staging.scan.com/api/v2/referrals",
      nil,
    )
    req.Header.Set("Authorization", "Bearer "+os.Getenv("SCAN_API_TOKEN"))

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    _ = body
  }
  ```
</CodeGroup>

## Courier SSO

Courier SSO (`POST /api/v2/auth/sso`) uses the same Bearer token. Send
`Authorization: Bearer <api_token>` and Scan returns a one-time portal URL.
