# Rivon Health — Public REST API

**Base URL:** `https://app.rivon.health/api/v1`
**Version:** `2026-04-22`

This is the integration API for partner engineers building on top of Rivon. It's a read-focused, scope-gated REST API with HMAC-signed webhooks for real-time event sync.

---

## Table of Contents

1. [Authentication](#1-authentication)
2. [Scopes](#2-scopes)
3. [Conventions](#3-conventions)
4. [Rate limiting](#4-rate-limiting)
5. [Errors](#5-errors)
6. [Endpoints](#6-endpoints)
   - [Providers](#providers)
   - [Credentialing](#credentialing)
   - [Licensing](#licensing)
   - [Tasks](#tasks)
   - [Monitoring — Expirations](#monitoring--expirations)
   - [Webhooks — Management](#webhooks--management)
7. [Webhooks — Consuming events](#7-webhooks--consuming-events)
8. [Versioning](#8-versioning)
9. [Support](#9-support)

---

## 1. Authentication

Every request requires a Bearer token in the `Authorization` header:

```http
Authorization: Bearer rvn_live_1a2b3c...
```

Keys are issued by **Rivon staff**, not self-serve. Email `api@rivon.health` (or open a support request inside Rivon) with:

- The scopes you need (see §2 below)
- The org name the key should be tied to
- A short description of your integration

We'll review, provision a scoped key, and email it to the requester. One key = one organization's data. Keys are hashed server-side — if you lose one, request a new one and we'll revoke the old. This provisioning model lets us audit partner access and keeps PHI-scope keys behind a human review.

```bash
curl -H "Authorization: Bearer $RIVON_API_KEY" \
     https://app.rivon.health/api/v1/credentialing/summary
```

---

## 2. Scopes

Each key is granted a set of scopes at creation. Request only what you need.

| Scope                  | Grants                                                       |
| ---------------------- | ------------------------------------------------------------ |
| `providers:read`       | List / read providers                                        |
| `providers:write`      | Create / update providers                                    |
| `credentialing:read`   | Enrollment summaries, lists, and details                     |
| `credentialing:write`  | Create enrollments, advance stages                           |
| `licensing:read`       | Licensing pipeline data                                      |
| `licensing:write`      | Create and advance licensing pipelines                       |
| `tasks:read`           | List and read tasks                                          |
| `tasks:write`          | Create and complete tasks                                    |
| `documents:read`       | List documents + generate signed download URLs               |
| `monitoring:read`      | Expiration reports                                           |
| `webhooks:admin`       | Create / edit / delete webhook endpoints                     |
| `phi:read`             | Include sensitive fields (DOB, SSN/EIN, birthplace, full address). **Requires a signed BAA between your org and Rivon.** |

A request missing a required scope returns `403 insufficient_scope`.

---

## 3. Conventions

- **Encoding**: All bodies are JSON. Responses use UTF-8.
- **Timestamps**: ISO-8601 UTC (`2026-04-21T12:34:56.789Z`).
- **IDs**: 25-character CUIDs (`cmnpkacda0001123f1wjtif6j`).
- **Pagination**: Cursor-based. List endpoints accept `?limit=<1-200>` (default 50) and `?cursor=<id>`. Responses include `{ data, pagination: { limit, nextCursor } }`. Pass `nextCursor` back as `?cursor=` for the next page; when `nextCursor` is `null` you've reached the end.
- **Versioning header (optional)**: Pin to a specific date with `X-Rivon-Version: 2026-04-20`. Omit to always get the latest.

---

## 4. Rate limiting

**100 requests per minute per API key.** Exceeding returns `429 rate_limited`. Design retries with exponential backoff and respect the `429` status. Need a higher limit for bulk backfills? Reach out.

---

## 5. Errors

All errors have the shape:

```json
{ "error": { "code": "invalid_key", "message": "Unknown or invalid API key." } }
```

| HTTP | Code                  | Meaning                                                             |
| ---- | --------------------- | ------------------------------------------------------------------- |
| 400  | `invalid_url`         | Validation failed on the request body                               |
| 400  | `invalid_body`        | JSON body was malformed                                             |
| 401  | `missing_auth`        | No Bearer token in the Authorization header                         |
| 401  | `invalid_key`         | Unknown key                                                         |
| 401  | `revoked_key`         | Key was revoked                                                     |
| 401  | `expired_key`         | Key's `expiresAt` has passed                                        |
| 403  | `insufficient_scope`  | Key is missing a required scope                                     |
| 404  | `not_found`           | Resource doesn't exist in your org                                  |
| 429  | `rate_limited`        | More than 100 requests in the last 60 seconds                       |
| 5xx  | `server_error`        | We broke something — please include the request ID when reporting   |

---

## 6. Endpoints

### Providers

#### `GET /providers`

List providers in your organization.

**Scope:** `providers:read` (+ `phi:read` for DOB / SSN / full address)

**Query params:**

| Name            | Type    | Description                                               |
| --------------- | ------- | --------------------------------------------------------- |
| `status`        | string  | `onboarding` \| `active` \| `inactive` \| `archived`      |
| `clientGroupId` | string  | Restrict to one client group                              |
| `limit`         | integer | 1-200 (default 50)                                        |
| `cursor`        | string  | Pagination cursor                                         |

**Example:**

```bash
curl -H "Authorization: Bearer $KEY" \
     "https://app.rivon.health/api/v1/providers?status=active&limit=25"
```

**Response:**

```json
{
  "data": [
    {
      "id": "cmnpk...",
      "type": "individual",
      "status": "active",
      "clientGroupId": "cmclt...",
      "clientGroup": { "id": "cmclt...", "name": "Acme Medical Group" },
      "assignedSpecialist": { "name": "Elle Hong", "email": "elle@acme.com" },
      "personal": {
        "firstName": "Jane", "lastName": "Doe", "middleName": null,
        "orgName": null,
        "credentials": "MD", "specialty": "Internal Medicine",
        "npi": "1234567890", "taxonomyCode": "207R00000X"
      },
      "contact": {
        "email": "jane@acme.com", "phone": "614-555-0110",
        "city": "Columbus", "state": "OH", "zip": "43215", "country": "US"
      },
      "createdAt": "2026-04-01T12:34:56.000Z",
      "updatedAt": "2026-04-20T09:12:14.000Z"
    }
  ],
  "pagination": { "limit": 25, "nextCursor": "cmnpk..." }
}
```

#### `GET /providers/{id}`

Retrieve a single provider with credentials (state licenses, DEA, insurance).

**Scope:** `providers:read` (+ `phi:read` for sensitive fields)

**Response shape:** same as list, plus credential records (state licenses, DEA, liability insurance) **and** government-payer enrollments (Medicare and Medicaid). Government enrollments live in dedicated tables — surfaced here so you can pull effective dates and member IDs in one fetch.

```json
{
  "stateLicenses": [
    { "id": "...", "state": "OH", "licenseType": "RN", "licenseId": "RN12345",
      "issueDate": "2024-07-01", "expirationDate": "2027-06-30" }
  ],
  "deaCertificates": [
    { "id": "...", "state": "OH", "expirationDate": "2028-03-31" }
  ],
  "liabilityInsurance": [
    { "id": "...", "carrierName": "The Doctors Company", "policyNumber": "POL-2024-00123",
      "expirationDate": "2026-12-01" }
  ],
  "medicareEnrollments": [
    { "id": "...", "state": "OH", "medicareId": "M12345",
      "status": "approved", "effectiveDate": "2025-09-15", "expirationDate": null }
  ],
  "medicaidEnrollments": [
    { "id": "...", "state": "OH", "medicaidId": "MD98765",
      "status": "approved", "effectiveDate": "2025-08-01",
      "expirationDate": null, "reattestationDate": "2026-08-01" }
  ]
}
```

> **Status enum (Medicare/Medicaid):** `submitted`, `under_review`, `approved`, `denied`, `expired`, `revalidating`.

---

### Credentialing

#### `GET /credentialing/summary`

Aggregate counts for dashboards.

**Scope:** `credentialing:read`

**Response:**

```json
{
  "byStatus": { "active": 28, "credentialed": 142, "denied": 3, "panel_closed": 5 },
  "byStage": { "Pre-Credentialing": 4, "Application": 8, "Committee Review": 16 },
  "totalEnrollments": 178,
  "avgDaysToCredentialed": 67.4
}
```

#### `GET /credentialing/enrollments`

List enrollments. Filters: `?status=`, `?payerId=`, `?providerId=`.

**Scope:** `credentialing:read`

**Response row (abbreviated):**

```json
{
  "id": "cmenr...",
  "status": "active",
  "attemptNumber": 1,
  "initiatedAt": "2026-02-10T00:00:00.000Z",
  "credentialedDate": null,
  "providerPayerNumber": null,
  "currentStage": "Application",
  "daysInStage": 24,
  "payer": { "id": "cmins...", "insuranceName": "BCBS of Ohio",
             "state": "OH", "productName": "HMO" },
  // ↑ once the payer credentials the provider, `status` flips to
  // "credentialed", `credentialedDate` populates with the effective
  // date, and `providerPayerNumber` carries the payer-issued
  // member / provider ID (e.g. PIN, supplier #).
  "provider": { "id": "cmnpk...", "firstName": "Jane", "lastName": "Doe",
                "npi": "1234567890", "credentials": "MD" },
  "assignedSpecialist": { "name": "Elle Hong", "email": "elle@acme.com" },
  "stages": [
    { "name": "Pre-Credentialing", "status": "complete", "startedAt": "...", "completedAt": "..." },
    { "name": "Application",       "status": "in_progress", "startedAt": "...", "completedAt": null }
  ]
}
```

#### `GET /credentialing/enrollments/{id}`

Full enrollment detail including notes history.

**Scope:** `credentialing:read`

---

### Licensing

#### `GET /licensing/summary`

**Scope:** `licensing:read`

```json
{
  "byStatus": { "active": 42, "complete": 118, "denied": 2, "paused": 3 },
  "byState": { "OH": 31, "CA": 18, "TX": 22 },
  "byStage": { "Application Preparation": 6, "Under Board Review": 14 },
  "totalPipelines": 165
}
```

#### `GET /licensing/pipelines`

List pipelines. Filters: `?status=`, `?state=`, `?providerId=`.

**Scope:** `licensing:read`

**Response row (abbreviated):**

```json
{
  "id": "cmlpln...",
  "state": "OH",
  "providerType": "RN",
  "status": "complete",
  "submittedDate": "2025-11-08T00:00:00.000Z",
  "trackingNumber": "OH-RN-2025-44219",
  "tempLicenseNumber": null,
  "tempLicenseExpiration": null,
  "currentStage": null,
  "daysInStage": null,
  "provider": { "id": "cmnpk...", "firstName": "Jane", "lastName": "Doe",
                "npi": "1234567890", "credentials": "MD" },
  "issuedLicense": {
    "id": "cmsl...",
    "state": "OH",
    "licenseType": "RN",
    "licenseId": "RN445566",
    "issueDate": "2026-01-15",
    "expirationDate": "2028-01-15"
  },
  "stages": [ /* … */ ]
}
```

> **`issuedLicense`** is `null` until the pipeline reaches `complete` and a `StateLicense` row is created for the provider. Once present, `issueDate` is the license's effective date and `licenseId` is the permanent license number.

#### `GET /licensing/pipelines/{id}`

Full pipeline with stage checklists, plus the same `issuedLicense` field as the list response.

**Scope:** `licensing:read`

```json
{
  "id": "cmlic...",
  "state": "OH",
  "providerType": "RN",
  "status": "active",
  "trackingNumber": "OH-2026-12345",
  "tempLicenseNumber": null,
  "currentStage": "Under Board Review",
  "daysInStage": 31,
  "stageDetails": [
    {
      "name": "Pre-Application",
      "status": "complete",
      "checklists": [
        {
          "state": "OH",
          "items": [
            { "description": "Verify provider NPI number", "isRequired": true,
              "isCollected": true },
            { "description": "Confirm provider type and specialty", "isRequired": true,
              "isCollected": true }
          ]
        }
      ]
    }
  ]
}
```

---

### Tasks

#### `GET /tasks`

**Scope:** `tasks:read`

**Query params:**

| Name          | Type    | Description                                 |
| ------------- | ------- | ------------------------------------------- |
| `status`      | string  | `open` \| `completed` \| `overdue`          |
| `assignedTo`  | string  | Filter by assignee user ID                  |
| `providerId`  | string  | Filter by linked provider                   |
| `limit`       | integer | 1-200 (default 50)                          |
| `cursor`      | string  | Pagination cursor                           |

---

### Monitoring — Expirations

#### `GET /monitoring/expirations`

Unified list of upcoming credential expirations (state licenses, DEA, CS, liability, Medicare, Medicaid, certifications).

**Scope:** `monitoring:read`

**Query params:**

| Name     | Type    | Description                          |
| -------- | ------- | ------------------------------------ |
| `within` | integer | Days to look ahead, 1-365 (default 90) |

**Response:**

```json
{
  "withinDays": 60,
  "count": 12,
  "data": [
    { "providerId": "cmnpk...", "credentialType": "state_license",
      "label": "OH RN", "expirationDate": "2026-05-15", "daysUntil": 24,
      "recordId": "cm..." }
  ]
}
```

`credentialType` values: `state_license` · `dea` · `controlled_substance` · `liability_insurance` · `medicare` · `medicaid` · `certification`.

---

### Webhooks — Management

#### `GET /webhooks`

List webhook endpoints.

**Scope:** `webhooks:admin`

#### `POST /webhooks`

Create an endpoint.

**Scope:** `webhooks:admin`

**Body:**

```json
{
  "url": "https://your-app.com/hooks/rivon",
  "events": ["credentialing.stage_changed", "credential.expiring"],
  "description": "Main integration endpoint"
}
```

**Response (201):** the endpoint + a `secret` field that is **only returned at creation time**. Store it securely — you'll use it to verify signatures on incoming webhooks.

```json
{
  "id": "cmweb...",
  "url": "https://your-app.com/hooks/rivon",
  "events": ["credentialing.stage_changed", "credential.expiring"],
  "active": true,
  "secret": "a1b2c3...f0",
  "secretMessage": "Store this secret now — we will not show it again."
}
```

#### `PATCH /webhooks/{id}`

Update `url`, `events`, `active`, or `description`. Same body shape as create (any subset).

#### `DELETE /webhooks/{id}`

Soft-delete an endpoint.

---

## 7. Webhooks — Consuming events

When an event fires, Rivon sends a POST to your endpoint with:

**Headers:**

```
Content-Type: application/json
User-Agent: Rivon-Webhooks/1.0
X-Rivon-Version: 2026-04-20
X-Rivon-Event: credentialing.stage_changed
X-Rivon-Event-Id: 7a4e6c5e-2a7b-4b0d-9e8f-1c2d3e4f5a6b
X-Rivon-Signature: sha256=3c4f...a1
X-Rivon-Delivery-Id: cmdel...
```

**Body:**

```json
{
  "id": "7a4e6c5e-2a7b-4b0d-9e8f-1c2d3e4f5a6b",
  "type": "credentialing.stage_changed",
  "apiVersion": "2026-04-20",
  "createdAt": "2026-04-21T15:30:22.123Z",
  "orgId": "cmorg...",
  "data": {
    "enrollmentId": "cmenr...",
    "providerId": "cmnpk...",
    "payerId": "cmins...",
    "stageName": "Committee Review",
    "status": "in_progress"
  }
}
```

### Verifying the signature

```ts
import crypto from "crypto";

function verifyRivonSignature(rawBody: string, header: string, secret: string): boolean {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  // Constant-time comparison
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
```

**Always verify** before trusting the payload. Use the **raw** request body string — not a re-stringified JSON object.

### Event catalog

| Event                            | When it fires                                                   |
| -------------------------------- | --------------------------------------------------------------- |
| `provider.created`               | New provider added                                              |
| `provider.status_changed`        | Provider status transitions (onboarding → active, etc.)         |
| `credentialing.stage_changed`    | Any stage advance on an enrollment                              |
| `credentialing.credentialed`     | Enrollment resolves to credentialed                             |
| `credentialing.denied`           | Enrollment resolves to denied                                   |
| `licensing.stage_changed`        | Any stage advance on a licensing pipeline                       |
| `licensing.license_issued`       | Pipeline resolves to issued                                     |
| `credential.expiring`            | Fires at 60 / 30 / 7-day thresholds before a credential expires |
| `task.completed`                 | Task marked complete                                            |

### Delivery guarantees

- **At-least-once** delivery — return `2xx` on receipt; the `X-Rivon-Event-Id` is stable across retries, use it for idempotency.
- **Retries**: non-2xx triggers retries at 1 min, 5 min, 30 min, 2 hr, 12 hr (exponential backoff). After 5 failures the delivery is marked `failed`.
- **Timeout**: we give your endpoint 10 seconds to respond. Acknowledge quickly and queue background work asynchronously.
- **Order**: events are delivered in roughly chronological order but we don't guarantee strict ordering. Rely on `data.*` fields + your own re-fetch via the REST API when order matters.

---

## 8. Versioning

APIs are versioned by **release date**, not `v1` / `v2`. To pin to a specific version, send:

```
X-Rivon-Version: 2026-04-20
```

Pinning shields you from breaking changes. When we release a new version we keep the old one working for **at least 12 months**. Omit the header to always receive the latest. Deprecations are announced in-app and via email at least 90 days in advance.

Changelog: `https://rivon.health/changelog`

---

## 9. Support

- **Dev support**: Email `api@rivon.health` — response within 1 business day.
- **Bug reports**: Include the request ID (from `X-Rivon-Request-Id` on any response) so we can trace it in our logs.
- **Status**: `https://status.rivon.health` — live platform status + historical incidents.
- **Rate-limit increase / bulk backfill**: Email us with expected volume and we'll bump you.

---

## Quick-start — 10-minute integration

```bash
# 1. Request an API key — email api@rivon.health with your scopes + org name
export RIVON_API_KEY="rvn_live_..."

# 2. Fetch your credentialing snapshot
curl -H "Authorization: Bearer $RIVON_API_KEY" \
     https://app.rivon.health/api/v1/credentialing/summary

# 3. Subscribe to real-time events
curl -X POST -H "Authorization: Bearer $RIVON_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "url": "https://your-app.com/hooks/rivon",
       "events": ["credentialing.stage_changed", "credentialing.credentialed", "credential.expiring"]
     }' \
     https://app.rivon.health/api/v1/webhooks

# 4. Verify the first webhook and ship it
```

Welcome to Rivon. 🚀
