# Registering end users

> Every order names an end user. Register each one once, with its certification and the purposes it holds, and the orders follow.

- **HTML:** https://offendersearch.app/docs/guides/fcra/end-users
- **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

An **end user** is the employer, landlord or other business a report is for. You register it with the permissible purposes it holds and your certification text, receive an `eu_` id, and name that id on every order for that business. You will create one, list and read it, change its purposes, suspend and reinstate it, and see the two refusals that protect the purpose chain: `403 fcra_purpose_not_allowed` and `422 end_user_suspended`.

Vetting end users and holding their certifications stays with you; the API records what you certify, per end user and per order, in the evidence record.

## Before you start

- A test key against `https://sandbox.offendersearch.app`; every test account may use all four purposes there.
- Required fields: `name` (1–200), `permissiblePurposes[]` (at least one of `employment`, `tenant_screening`, `consumer_written_instructions`, `legitimate_business_need`), `certification` with `text` (10–8,000 characters), `version` (1–50) and `certifiedAt` (a date-time not in the future). Optional: `externalId` (≤ 200), `address` (`line1`, `city`, `state` as two letters, `postalCode`), `certification.certifiedBy`.
- Request bodies are strict: an unknown field is a `422` with `detail` as an array.

> **Purposes are checked twice.** An end user may hold only purposes your agreement allows (`403 fcra_purpose_not_allowed` on create and on PATCH). An order may then state only a purpose its end user holds **and** your agreement still allows. Keep `allowedPurposes` from `GET /v1/fcra/partner` as the list you offer.

## Step by step

1. **Register the end user.** `POST /v1/fcra/end-users` → `201` with status `active`. Store `id` against your customer record; `externalId` carries your own key back.
2. **List and read.** `GET /v1/fcra/end-users` returns every end user on the account (no pagination); `GET /v1/fcra/end-users/{end_user_id}` returns one or `404 not_found`.
3. **Change purposes.** `PATCH /v1/fcra/end-users/{end_user_id}` with `permissiblePurposes` replaces the list. A purpose outside your agreement is `403 fcra_purpose_not_allowed`.
4. **Suspend and reinstate.** `PATCH` with `status: "suspended"`; new orders naming it are `422 end_user_suspended`. `status: "active"` reinstates it.

**cURL**
```bash
curl -X POST https://sandbox.offendersearch.app/v1/fcra/end-users \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Northwind Staffing LLC",
    "externalId": "EU-4821",
    "address": { "line1": "1 Example Way", "city": "Springfield", "state": "IL", "postalCode": "62701" },
    "permissiblePurposes": ["employment"],
    "certification": {
      "text": "Northwind Staffing LLC certifies that it will request and use consumer reports only for employment purposes, has made the required disclosures and obtained written authorization from each consumer, and will comply with its obligations under the FCRA and applicable state law.",
      "version": "v3",
      "certifiedAt": "2026-10-02T14:00:00Z",
      "certifiedBy": "compliance@your-cra.example"
    }
  }'
```

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

BASE = "https://sandbox.offendersearch.app"
H = {"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]}

res = requests.post(BASE + "/v1/fcra/end-users", headers=H, timeout=30, json={
    "name": "Northwind Staffing LLC",
    "externalId": "EU-4821",
    "address": {"line1": "1 Example Way", "city": "Springfield", "state": "IL", "postalCode": "62701"},
    "permissiblePurposes": ["employment"],
    "certification": {
        "text": "Northwind Staffing LLC certifies that it will request and use consumer reports only for "
                "employment purposes, has made the required disclosures and obtained written authorization "
                "from each consumer, and will comply with its obligations under the FCRA and applicable state law.",
        "version": "v3",
        "certifiedAt": "2026-10-02T14:00:00Z",
        "certifiedBy": "compliance@your-cra.example",
    },
})
if res.status_code == 403:
    print("refused:", res.json()["detail"]["code"], res.json()["detail"]["message"])
res.raise_for_status()
end_user = res.json()
print(end_user["id"], end_user["status"])

# later: add tenant screening, or suspend
requests.patch(BASE + "/v1/fcra/end-users/" + end_user["id"], headers=H, timeout=30,
               json={"permissiblePurposes": ["employment", "tenant_screening"]})
requests.patch(BASE + "/v1/fcra/end-users/" + end_user["id"], headers=H, timeout=30,
               json={"status": "suspended"})
```

**JavaScript**
```javascript
const BASE = "https://sandbox.offendersearch.app";
const headers = { "X-API-Key": process.env.OFFENDERSEARCH_TEST_KEY, "Content-Type": "application/json" };

const res = await fetch(BASE + "/v1/fcra/end-users", {
  method: "POST", headers,
  body: JSON.stringify({
    name: "Northwind Staffing LLC",
    externalId: "EU-4821",
    address: { line1: "1 Example Way", city: "Springfield", state: "IL", postalCode: "62701" },
    permissiblePurposes: ["employment"],
    certification: {
      text: "Northwind Staffing LLC certifies that it will request and use consumer reports only for employment purposes, has made the required disclosures and obtained written authorization from each consumer, and will comply with its obligations under the FCRA and applicable state law.",
      version: "v3",
      certifiedAt: "2026-10-02T14:00:00Z",
      certifiedBy: "compliance@your-cra.example",
    },
  }),
});
const body = await res.json();
if (res.status === 403) throw new Error(body.detail.code + ": " + body.detail.message);
console.log(body.id, body.status);

// later: add tenant screening, or suspend
await fetch(BASE + "/v1/fcra/end-users/" + body.id, { method: "PATCH", headers,
  body: JSON.stringify({ permissiblePurposes: ["employment", "tenant_screening"] }) });
await fetch(BASE + "/v1/fcra/end-users/" + body.id, { method: "PATCH", headers,
  body: JSON.stringify({ status: "suspended" }) });
```

*201 Created*
```json
{
  "id": "eu_0b44acbeefeaa7d7",
  "name": "Northwind Staffing LLC",
  "externalId": "EU-4821",
  "address": { "line1": "1 Example Way", "city": "Springfield", "state": "IL", "postalCode": "62701" },
  "permissiblePurposes": ["employment"],
  "certification": {
    "text": "Northwind Staffing LLC certifies that it will request and use consumer reports only for employment purposes, …",
    "version": "v3",
    "certifiedAt": "2026-10-02T14:00:00+00:00",
    "certifiedBy": "compliance@your-cra.example"
  },
  "status": "active",
  "createdAt": "2026-10-25T14:12:40.207113+00:00",
  "updatedAt": "2026-10-25T14:12:40.207113+00:00"
}
```

*403 — a purpose outside your agreement*
```json
{
  "detail": {
    "code": "fcra_purpose_not_allowed",
    "message": "Your partner agreement does not cover ['tenant_screening']"
  }
}
```

*422 — an order naming a suspended end user*
```json
{
  "detail": { "code": "end_user_suspended", "message": "This end user is suspended." }
}
```

## Reading the result

| Field | Values | What it decides |
| --- | --- | --- |
| `id` | `eu_` + 16 hex | The `endUserId` on every order for this business. |
| `status` | `active`, `suspended` | Whether new orders may name it. Existing orders are unaffected by a suspension. |
| `permissiblePurposes` | subset of the four | The purposes an order for this end user may state. The order’s `permissiblePurpose` must be in this list. |
| `certification` | `text`, `version`, `certifiedAt`, `certifiedBy` | What you certified and when. Bump `version` when your certification text changes. |
| `externalId` | your string or `null` | Round-trips your own customer key. |
| `updatedAt` | date-time | Moves on every PATCH, even an empty one. |

## Edge cases and pitfalls

- **`certifiedAt` in the future** (more than five minutes ahead) is a `422` array entry. Send the time the certification was actually signed.
- **`state` must be two letters** and is upper-cased; it is not checked against a list of states.
- **A `403 fcra_purpose_not_allowed` on an end user is not written to the evidence record**; the same refusal on an order is. Fix the purpose list, do not retry blindly.
- **PATCH replaces `permissiblePurposes`**, it does not merge. Send the full new list.
- **One end user per business, not per order.** Re-registering the same business creates a second `eu_` id; use `externalId` to find the existing one in `GET /v1/fcra/end-users`.
- **Suspension is reversible; deletion does not exist.** Keep suspended end users for the orders that reference them.

## Production checklist

- Register each end user once; store `eu_` id ↔ your customer id.
- Offer only `allowedPurposes` from `GET /v1/fcra/partner` in your UI.
- Version your certification text; re-certify (new `version`, new `certifiedAt`) when it changes.
- Suspend end users whose certification lapses; handle `422 end_user_suspended` at order time.
- Which purposes an end user may lawfully hold is for you and your counsel to determine; the API enforces only your agreement’s list.

## Related

- [Access & end users (reference)](https://offendersearch.app/docs/fcra/access)
- [Orders](https://offendersearch.app/docs/fcra/orders)
- [Error codes](https://offendersearch.app/docs/fcra/errors)
- [FCRA API reference](https://offendersearch.app/docs/fcra/api-reference)
