# ScopoHealth Partner API > ScopoHealth is an AI-assisted ACA health-insurance enrollment platform for licensed agents and agencies. The Partner API lets an agency's own website, CRM, or lead form start a ScopoHealth enrollment for a client: your **server** sends one `POST` with an agent API key and whatever client details you already have, and gets back a one-time URL. You redirect the client to it. ScopoHealth does not email or text the client. The redirect is the delivery. This file is written for coding agents (Claude Code, Cursor, Copilot, etc.) building that integration. Everything needed is on this page. There is one endpoint. ## Rules an integration must follow - Call the API **server-side only**. Never put the API key in browser JavaScript, a mobile app, a URL, a query string, or client-visible logs. A browser `fetch` straight to this endpoint is wrong. Put a route on your own backend in front of it. - Read the key from an environment variable or secret store (for example `SCOPO_API_KEY`). Never hardcode or commit it. - The returned `url` holds a **single-use** token. Redirect the client to it right away (HTTP 302/303 or `window.location` with a URL your server returned). Do not fetch it server-side, prefetch it, unfurl it, log it, store it longer than needed, or send it by email or SMS. - There is **no idempotency key**. Every successful call creates a new enrollment and a new link. Do not retry blindly. If a request times out with no response, it may have succeeded. Dedupe on your side, for example by your lead id in `external_reference`. - Unknown JSON fields are rejected with `422`. Send only the fields documented below. Omit a field rather than sending `null` for something you don't have. - Treat client data as PII/PHI. Don't log request bodies. ## Getting a key A ScopoHealth agent creates keys in the agent portal under **Profile → API keys**. Each key has a label, for example `Website`. The secret looks like `sk_scopo_…` and is shown **once**. An agent can hold up to 5 active keys, which allows rotation without downtime, and can revoke any key from the same page. A key acts as the agent who created it. Enrollments land in that agent's book, and the agent's approval and subscription status apply. ## Endpoint ``` POST https://app.scopohealth.ai/api/partner/v1/enrollments X-API-Key: sk_scopo_… Content-Type: application/json ``` Authentication is only the `X-API-Key` header. Do not send `Authorization` or cookies, because they are not forwarded. ## Request body Only `client.email` is required. Send everything else you have. The more you send, the fewer questions the client is asked. ```json { "client": { "email": "jane@example.com", "phone": "5555550100", "first_name": "Jane", "last_name": "Doe", "age": 34, "gender": "Female", "uses_tobacco": false, "sms_consent": true, "data_use_consent": true, "data_use_consent_language": "en", "data_use_consent_version": "your-consent-text-v3" }, "household": { "state": "TX", "zipcode": "76002", "countyfips": "48439", "income": 48000, "year": 2027, "has_married_couple": false, "members": [ { "age": 6, "gender": "Male", "relationship": "Child" } ] }, "language": "es", "expires_in_hours": 72, "external_reference": "your-lead-id-123" } ``` ### `client` (required object) | Field | Type | Notes | | --- | --- | --- | | `email` | string, **required** | 3–320 chars. Lower-cased and validated. | | `phone` | string | US number in any common format (`(555) 555-0100`, `+15555550100`). Normalized to E.164. | | `first_name`, `last_name` | string | ≤100 chars each. | | `age` | integer | 0–120. Send age, not date of birth (no DOB field exists). | | `gender` | string | `Male` or `Female` (case-insensitive). Anything else is `422`. This is the value marketplace rating uses. | | `uses_tobacco` | boolean | | | `sms_consent` | boolean | Must be `true` to skip the intake form when `phone` is sent. Only send `true` if the client actually agreed to texts. | | `data_use_consent` | boolean | Must be `true` to skip the intake form. **Sending `true` asserts that you collected the client's consent to share their data with ScopoHealth for shopping for and enrolling in health coverage.** ScopoHealth records when it received the assertion and the version you name. Never default this to `true` in code. Wire it to a real consent checkbox in your form. | | `data_use_consent_language` | string | Language the consent text was shown in: `en` or `es`. | | `data_use_consent_version` | string | ≤64 chars. Your consent text's version id. Defaults to `partner-asserted-v1`. | ### `household` (optional object) | Field | Type | Notes | | --- | --- | --- | | `state` | string | Two-letter code, for example `TX`. | | `zipcode` | string | Exactly 5 digits. | | `countyfips` | string | Exactly 5 digits. Optional when the ZIP maps to exactly one county. Required to skip the form when the ZIP spans several counties. | | `income` | number | ≥0. Annual household income (MAGI) in USD. | | `year` | integer | Plan year. Defaults to the latest year ScopoHealth is enrolling for. A closed year is `403`. | | `has_married_couple` | boolean | | | `effective_date` | string | ISO date `YYYY-MM-DD`. | | `members` | array | Up to 20. Everyone on the policy. If no member has `relationship: "Self"`, the primary applicant is added from `client`. | ### `household.members[]` | Field | Type | Notes | | --- | --- | --- | | `age` | integer, **required** | 0–120. | | `gender` | string | `Male` or `Female`. | | `relationship` | string | One of: `Self`, `Spouse`, `Life Partner`, `Child`, `Adopted Child`, `Foster Child`, `Stepson or Stepdaughter`, `Grandson or Granddaughter`, `Nephew or Niece`, `Brother or Sister`, `Brother-in-Law or Sister-in-Law`, `Son-in-Law or Daughter-in-Law`, `Stepparent`, `Ward`, `Guardian`, `Court Appointed Guardian`, `Collateral Dependent`, `Sponsored Dependent`, `Dependent of a Minor Dependent`, `Ex-Spouse`, `Annuitant`, `Other Relative`, `Other Relationship`. Exact spelling. | | `uses_tobacco` | boolean | Default `false`. | | `aptc_eligible` | boolean | Default `true`. | | `is_pregnant` | boolean | Default `false`. | | `is_parent` | boolean | Default `false`. | | `has_mec` | boolean | Default `false`. Member already has minimum essential coverage (for example Medicaid or employer coverage). | | `utilization` | string | `Low`, `Medium`, or `High`. Expected healthcare usage. | ### Top level | Field | Type | Notes | | --- | --- | --- | | `language` | string | Client's language for the experience: `en`, `es`, `hi`, or `pt`. Locale spellings like `es-MX` or `Spanish` are accepted. Falls back to `client.data_use_consent_language`. | | `expires_in_hours` | integer | 1–168, default 72. | | `external_reference` | string | ≤128 chars. Your own id (lead id, CRM record id). Echoed back unchanged, and shown to the agent. Don't put PII in it. | ## Skipping the intake form If **all** of these are present, the client skips ScopoHealth's intake form and lands directly in a secure chat with the agent's AI assistant: `client.first_name`, `client.last_name`, `client.age`, `client.gender`, `client.phone`, `client.sms_consent: true`, `client.data_use_consent: true`, `household.state`, `household.zipcode`, `household.income`, and a county (`household.countyfips`, or a ZIP that maps to one county). Anything less still works. The client sees the intake form pre-filled with what you sent and is asked only for what's missing. ## Response — `201 Created` ```json { "request_id": "…", "secure_link_id": "…", "url": "https://app.scopohealth.ai/vercel/intake?token=…", "expires_at": "2026-09-24T12:00:00Z", "wizard_skipped": true, "missing_fields": [], "external_reference": "your-lead-id-123" } ``` - `url`: redirect the client here. Single-use, and expires at `expires_at` (UTC). - `wizard_skipped`: `true` means the client goes straight to chat. `false` means they see the pre-filled intake form. - `missing_fields`: dotted paths on the request body that the form will still ask for, for example `["household.countyfips", "client.gender"]`. It is empty when `wizard_skipped` is `true`. Useful for improving your form, not an error. - `request_id` / `secure_link_id`: ScopoHealth ids. Store them next to your lead if you want to reference them with the agent. ## Errors Error bodies are `{"detail": ...}`. When a `reason` is listed below, the body is `{"detail": {"detail": "", "reason": ""}}`. | Status | `reason` | Meaning | What to do | | --- | --- | --- | --- | | `401` | `invalid_api_key` | Missing, unknown, revoked, or malformed key. | Don't retry. Check the key configuration. | | `402` | — | The agent's ScopoHealth subscription is not active. | Don't retry. The agent must fix billing. | | `403` | — | The agent is not approved, or the plan year is not open. | Don't retry. | | `404` | — | The Partner API is not enabled in this environment. | Don't retry. | | `422` | — | Validation error. `detail` lists the offending field paths. | Fix the payload. Don't retry unchanged. | | `429` | `key_rate_limited` | More than 30 calls/minute on this key. | Wait the number of seconds in the `Retry-After` header. | | `429` | `daily_cap` | The agent's 200 deep links per UTC day are used up. | `Retry-After` = seconds to midnight UTC. Fall back to telling the client the agent will follow up. | | `5xx` | — | Temporary failure. | Retry with exponential backoff, a few attempts max. See the idempotency rule above. | Your redirect handler should always have a fallback, such as a "thanks, your agent will contact you" page, for when the call fails. ## Examples ### curl ```bash curl -sS -X POST https://app.scopohealth.ai/api/partner/v1/enrollments \ -H "X-API-Key: $SCOPO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client": {"email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "phone": "5555550100", "age": 34, "gender": "Female", "sms_consent": true, "data_use_consent": true}, "household": {"state": "TX", "zipcode": "76002", "income": 48000}, "external_reference": "lead-123" }' ``` ### Node.js / Next.js route handler (server-side) ```ts // app/api/get-quote/route.ts: your form posts here; the browser never sees the key. export async function POST(req: Request) { const form = await req.json(); const res = await fetch("https://app.scopohealth.ai/api/partner/v1/enrollments", { method: "POST", headers: { "X-API-Key": process.env.SCOPO_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ client: { email: form.email, first_name: form.firstName, last_name: form.lastName, phone: form.phone, age: Number(form.age), gender: form.gender, // "Male" | "Female" sms_consent: form.smsConsent === true, data_use_consent: form.dataUseConsent === true, // from a real checkbox }, household: { state: form.state, zipcode: form.zip, income: Number(form.income) }, external_reference: form.leadId, }), }); if (res.status !== 201) { return Response.json({ redirect: "/thanks" }); // agent follows up manually } const { url } = await res.json(); return Response.json({ redirect: url }); // client-side: window.location.assign(redirect) } ``` ### Python (requests) ```python import os, requests resp = requests.post( "https://app.scopohealth.ai/api/partner/v1/enrollments", headers={"X-API-Key": os.environ["SCOPO_API_KEY"]}, json={ "client": {"email": "jane@example.com", "first_name": "Jane", "data_use_consent": True}, "household": {"state": "FL", "zipcode": "33101"}, "external_reference": "lead-123", }, timeout=20, ) if resp.status_code == 201: redirect_to = resp.json()["url"] # 302 the client here elif resp.status_code == 429: retry_after = int(resp.headers.get("Retry-After", "60")) else: resp.raise_for_status() ``` ## What happens after the redirect 1. The client lands on a page branded for the agent. The agent can set a logo and accent color under **Profile → Branding** in the portal. 2. They fill in any missing intake fields, then chat with the agent's AI assistant, compare ACA marketplace plans, and consent to a plan. 3. They create or sign in to a ScopoHealth account. For partner links **any** verified account works (Google, email code, password, or phone). It does not have to match the email you sent. 4. The agent is notified when the enrollment starts (with your `external_reference`), when the link is opened, and when the client is ready for the agent to review and submit. No webhooks or status endpoint are available to partners today. Status lives in the agent's ScopoHealth portal. ## Limits summary - 30 requests/minute per API key - 200 enrollments/day per agent (UTC day) - 5 active API keys per agent - Link lifetime 1–168 hours (default 72), single use - Up to 20 household members ## Contact API access questions: the contact form at https://app.scopohealth.ai/#contact