# Partner approval and authentication

> The FCRA API is not self-serve. This guide shows how to tell whether an account is enabled, what each caller can do, and how to build against the sandbox while approval is pending.

- **HTML:** https://offendersearch.app/docs/guides/fcra/partner-approval-and-auth
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## What you’ll build

A startup check for your integration: one call that tells you whether the account can place FCRA orders, which permissible purposes its agreement covers, whether a webhook endpoint is configured, and what role the caller holds — and the handling for the `403` you get on every other FCRA route when the answer is no. You will run it against the sandbox, where every test account is already an enabled sandbox partner, and against the live host, where it answers `enabled: false` until our team enables the account under a written FCRA agreement.

## Before you start

- A test key (`os_test_…`) against `https://sandbox.offendersearch.app`. On the sandbox server every `os_test_` account is an enabled sandbox partner for all four purposes, so every FCRA route works without approval.
- On the live host, access is enabled per account by our team after a written FCRA agreement is signed; the agreement names the permissible purposes the account may use. There is no dashboard switch. Request it at [/fcra](/fcra).
- Two credentials work on FCRA routes: `X-API-Key` (acts as an account **admin**) and a dashboard session as `Authorization: Bearer <token>` (acts as the seat’s role, `admin` or `user`). An API key sent as a Bearer token is not accepted.
- Trial accounts cannot place FCRA orders even when a partner row exists (`403 fcra_not_on_trial`).

> **Status is a GET, not an error code.** `GET /v1/fcra/partner` **never** returns `403`. An account without access gets a normal `200` with `enabled: false`. Use it for your health check and feature flag; use the `403 fcra_not_enabled` on the other routes only as a last line of defence.

## Step by step

1. **Read the partner status.** `GET /v1/fcra/partner`. On the sandbox expect `enabled: true`, `sandbox: true` and all four purposes.
2. **Gate your own features on it.** Show FCRA ordering only when `enabled` is true, offer only the purposes in `allowedPurposes`, and prompt for a webhook endpoint when `webhookConfigured` is false.
3. **Handle the refusal everywhere else.** Every other `/v1/fcra/*` partner route answers `403 {"detail": {"code": "fcra_not_enabled" | "fcra_not_on_trial", …}}` for an account without access. Surface `detail.message`; do not retry.
4. **Request live access.** When the sandbox build is done, request FCRA Partner Access at [/fcra](/fcra). Ask for the sandbox flag on your live partner account too — with it, live orders follow the same last-name rules as the sandbox server.

**cURL**
```bash
curl https://sandbox.offendersearch.app/v1/fcra/partner \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"
```

**Python**
```python
import os, requests

res = requests.get("https://sandbox.offendersearch.app/v1/fcra/partner",
                   headers={"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]}, timeout=30)
res.raise_for_status()                    # never 403 here
status = res.json()

if not status["enabled"]:
    print("FCRA orders are off for this account", "(trial)" if status["trial"] else "")
else:
    print("purposes:", status["allowedPurposes"])
    if not status["webhookConfigured"]:
        print("set a webhook endpoint before placing orders with callbackUrl")
```

**JavaScript**
```javascript
const res = await fetch("https://sandbox.offendersearch.app/v1/fcra/partner", {
  headers: { "X-API-Key": process.env.OFFENDERSEARCH_TEST_KEY },
});
const status = await res.json();          // never 403 here

if (!status.enabled) {
  console.log("FCRA orders are off for this account", status.trial ? "(trial)" : "");
} else {
  console.log("purposes:", status.allowedPurposes);
  if (!status.webhookConfigured) console.log("set a webhook endpoint before placing orders with callbackUrl");
}
```

*200 — an enabled sandbox partner*
```json
{
  "enabled": true,
  "sandbox": true,
  "allowedPurposes": ["employment", "tenant_screening", "consumer_written_instructions", "legitimate_business_need"],
  "agreementRef": "sandbox",
  "enabledAt": "2026-10-25T13:58:02Z",
  "webhookConfigured": false,
  "trial": false,
  "role": "admin"
}
```

*200 — a live account that is not a partner*
```json
{
  "enabled": false,
  "sandbox": false,
  "allowedPurposes": [],
  "agreementRef": null,
  "enabledAt": null,
  "webhookConfigured": false,
  "trial": false,
  "role": "admin"
}
```

### The refusal on every other route

```bash
curl https://api.offendersearch.app/v1/fcra/end-users \
  -H "X-API-Key: $OFFENDERSEARCH_KEY"
```

*403*
```json
{
  "detail": {
    "code": "fcra_not_enabled",
    "message": "This account is not enabled for FCRA-regulated orders. Regulated access requires a signed partner agreement; contact us to discuss it."
  }
}
```

## Reading the result

| Field | Values | What it decides |
| --- | --- | --- |
| `enabled` | boolean | An in-force partner agreement **and** not a trial account. The single switch for your FCRA features. |
| `sandbox` | boolean | Orders on this account follow the deterministic last-name rules and are never metered. Always `true` on the sandbox server. |
| `allowedPurposes` | `employment`, `tenant_screening`, `consumer_written_instructions`, `legitimate_business_need` | The only purposes an end user may hold and an order may state. Anything else is `403 fcra_purpose_not_allowed`. |
| `agreementRef`, `enabledAt` | string / date-time or `null` | Which agreement enabled the account and when; `null` for non-partners. |
| `webhookConfigured` | boolean | Whether `PUT /v1/fcra/webhook-endpoint` has been done. Required before any order can carry a `callbackUrl`. |
| `trial` | boolean | The account is on a free trial; FCRA orders are refused with `fcra_not_on_trial`. |
| `role` | `admin`, `user` | What this caller may do. Only an `admin` may set or delete the webhook endpoint. An API key is always `admin`. |

## Edge cases and pitfalls

- **The sandbox says yes, production says no.** Expected: the sandbox server enables every test account. Keep the partner check in your startup path so the switch to a live key does not surprise you.
- **`403` with a plain string** — `"Only an admin on this account can do that…"` — is not the partner gate; it is a dashboard team member without the admin seat changing the webhook endpoint.
- **`401`** is a string too: `"Provide a valid X-API-Key or session token."` Check the key before you suspect the gate.
- **A disabled partner mid-order** — a search running when the agreement ends finishes as `error`; your `GET /v1/fcra/partner` flips to `enabled: false`.
- **Session callers resolve to the organization root account.** A team member’s session places orders on the organization’s account, with the seat’s `role`.
- **Purpose approval is per agreement.** `allowedPurposes` may be a subset of the four; design the end-user registration UI around that list, not around all four.

## Production checklist

- Call `GET /v1/fcra/partner` at startup and on a schedule; cache `enabled`, `allowedPurposes`, `webhookConfigured`.
- Use `detail.code` on `403` to show the right message; never retry `fcra_not_enabled` or `fcra_not_on_trial`.
- Use an API key for server-to-server work; reserve dashboard sessions for your own UI.
- Whether a decision falls under the FCRA, and which purpose applies, is for you and your counsel to determine under your agreement; the API records what you state.

## Related

- [Access & end users (reference)](https://offendersearch.app/docs/fcra/access)
- [FCRA sandbox](https://offendersearch.app/docs/fcra/sandbox)
- [Error codes](https://offendersearch.app/docs/fcra/errors)
- [Authentication (platform)](https://offendersearch.app/docs/platform/authentication)
