openapi: 3.1.0
info:
title: HealthSherpa Public Enrollment Sessions API
version: 0.1.0
description: 'Machine-readable contract for the currently available public HealthSherpa endpoints: GET /v1/ping, GET /v1/reference/counties, GET /v1/reference/issuers, GET /v1/reference/providers, POST /v1/quotes, the approval-gated POST /v1/enrollment-sessions, and the approval-gated direct enrollment endpoints GET /v1/enrollments, POST /v1/enrollments, PUT /v1/enrollments/{enrollment_id}, GET /v1/enrollments/{enrollment_id}, POST /v1/enrollments/{enrollment_id}/cancellations, POST /v1/enrollments/{enrollment_id}/terminations, POST /v1/enrollments/{enrollment_id}/submissions, GET /v1/enrollments/{enrollment_id}/payment_redirect, and POST /v1/enrollments/{enrollment_id}/supporting_documentation. Object schemas remain additive and clients should ignore response fields they do not recognize.
Most HealthSherpa-owned errors follow the unified `ErrorResponse` schema: `{ "error": { "code": string, "message": string, "details"?: object } }`. The direct enrollment endpoints instead return service failures as a top-level `errors[]` array: `{ "errors": [ { "code": string, "message": string, "field"?: string } ] }`. Edge-generated failures (API Gateway and WAF, before a request reaches the service) always use the unified `ErrorResponse` envelope, including on the direct enrollment endpoints — most notably a missing or invalid API key (`403 forbidden`).'
servers:
- url: https://api.one.healthsherpa.com
description: Production
security:
- ApiKeyAuth: []
tags:
- name: Enrollment Sessions
paths:
/v1/enrollment-sessions:
post:
tags:
- Enrollment Sessions
summary: Start an enrollment session
description: 'Returns URLs to continue enrollment in a browser-mediated HealthSherpa flow. This endpoint always returns deep links and does not create direct enrollment application records.
Both agent-assisted and consumer self-service use cases are supported. Set `context.flow` to `agent_assisted` for an agent walking a client through enrollment, or to `self_service` for a consumer enrolling themselves. The `campaign.*` block and the top-level `plan_id` field are only accepted in self-service flows.
The entire `context` object and every field inside it is required so partners declare product, exchange, coverage, plan year, flow, and locale on every request rather than relying on hidden defaults. In `self_service`, supply at least one of `location.state` or top-level `plan_id`. In `agent_assisted`, `location.state` is required.'
operationId: createEnrollmentSession
parameters:
- name: Idempotency-Key
in: header
required: false
description: 'Optional partner-supplied request identifier (1–255 chars, `A-Z`, `a-z`, `0-9`, `-`, `_`). Scoped by API key and retained for 24 hours.
- First call: processed normally; the response is captured.
- Replay (same key + same body): returns the original response verbatim with `Idempotent-Replay: true`.
- Same key + different body: `422 idempotency_mismatch`.
- Concurrent retry while the first call is still in flight: `409 idempotency_in_progress`.
- `429` responses are not stored, so partners can back off and retry transparently.
- `5xx` responses **are** stored. Retrying a 5xx with the same Idempotency-Key could risk duplicate work, so partners who believe a 5xx was transient must generate a fresh Idempotency-Key to retry.'
schema:
type: string
pattern: ^[A-Za-z0-9_\-]{1,255}$
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EnrollmentSessionRequest'
example:
external_id: crm-lead-abc-123
context:
product: aca
exchange: on_exchange
coverage_family: medical
coverage_type: medical
plan_year: 2026
flow: self_service
locale: en-US
plan_id: 12345NY0010001
location:
zip_code: '10001'
fips_code: '36061'
state: NY
household:
annual_income: 42000
household_size: 1
applicants:
- relationship: primary
first_name: Jane
last_name: Doe
email: jane@example.com
phone_number: '5551234567'
date_of_birth: '1990-01-01'
sex: female
uses_tobacco: false
responses:
'200':
description: Enrollment session started successfully.
headers:
X-Request-Id:
description: HealthSherpa ONE request id for tracing and support.
schema:
type: string
Idempotent-Replay:
description: Present and set to `true` only when this response is being replayed from a prior call that supplied the same `Idempotency-Key` and request body.
schema:
type: string
enum:
- 'true'
content:
application/json:
schema:
$ref: '#/components/schemas/EnrollmentSessionResponse'
example:
external_id: crm-lead-abc-123
links:
shopping_url: https://healthsherpa.com/public/shop?external_id=crm-lead-abc-123&fip_code=36061&household_income=42000&household_size=1&people%5Bprimary%5D%5Bage%5D=36&people%5Bprimary%5D%5Bgender%5D=female&people%5Bprimary%5D%5Btobacco%5D=false&state=NY&user_type=consumer&year=2026&zip_code=10001
client_apply_url: https://healthsherpa.com/public/apply?external_id=crm-lead-abc-123&fip_code=36061&household_income=42000&household_size=1&people%5Bprimary%5D%5Bage%5D=36&people%5Bprimary%5D%5Bgender%5D=female&people%5Bprimary%5D%5Btobacco%5D=false&plan_hios_id=12345NY0010001&state=NY&user_type=consumer&year=2026&zip_code=10001
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 'Returned when this API key is not authorized to call this endpoint, or when the developer''s HealthSherpa Marketplace setup is not in the state the requested flow needs.
- `context.flow = "agent_assisted"` requires approved on-exchange enrollment access (`onExchangeEnrollmentRequestApprovalStatus = "approved"`) and a healthy HealthSherpa Marketplace OAuth link (account approved, integration `active`, on-exchange capability `ready`, and the access token unexpired or refreshable). The OAuth-linked HealthSherpa Marketplace agent must also belong to an agency that is approved for enrollment deeplink URL generation. If HealthSherpa cannot generate `shopping_url` or `client_apply_url` because the agent account or agency is not approved for deeplink access, the API returns `403 forbidden` and does not treat the result as retryable service unavailability.
- `context.flow = "self_service"` requires approved on-exchange enrollment access (`onExchangeEnrollmentRequestApprovalStatus = "approved"`) and a configured deeplink agent ID.
Both flows require approved on-exchange enrollment access. The self-service gate does not consult the OAuth link, and the agent-assisted gate does not consult the deeplink agent ID. Each flow only fails when the configuration it actually uses is not satisfied.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
notAuthorized:
value:
error:
code: forbidden
message: This API key is not authorized to access this endpoint.
agentDeeplinksNotEnabled:
value:
error:
code: forbidden
message: Enrollment deeplinks are not available for this HealthSherpa Marketplace agent yet. Confirm the agent account is part of an agency and that the agency has been approved for deeplink access. Contact HealthSherpa support for help enabling this configuration.
'409':
$ref: '#/components/responses/IdempotencyInProgress'
'413':
$ref: '#/components/responses/PayloadTooLarge'
'415':
$ref: '#/components/responses/UnsupportedMediaType'
'422':
$ref: '#/components/responses/IdempotencyMismatch'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
'502':
$ref: '#/components/responses/BadGateway'
'503':
$ref: '#/components/responses/ServiceUnavailable'
'504':
$ref: '#/components/responses/GatewayTimeout'
components:
schemas:
EnrollmentSessionResponseLinks:
type: object
additionalProperties: true
required:
- shopping_url
- client_apply_url
properties:
shopping_url:
type: string
format: uri
description: HealthSherpa public shop URL (https://healthsherpa.com/public/shop?...).
client_apply_url:
type: string
format: uri
description: HealthSherpa public apply URL (https://healthsherpa.com/public/apply?...). Always present for enrollment-session responses.
EnrollmentSessionLocation:
type: object
additionalProperties: false
description: Routing geography plus optional street address. `zip_code`, `fips_code`, and `state` route the request; `address_line_1`, `address_line_2`, and `city` are used by the agent-assisted enrollment flow only and ignored on self-service deep links.
properties:
zip_code:
type: string
pattern: ^[0-9]{5}$
fips_code:
type: string
pattern: ^[0-9]{5}$
description: Five-digit county FIPS code. Mapped to the deep link's legacy `fip_code` parameter.
state:
type: string
description: Two-letter US state code (50 states plus DC).
pattern: ^[A-Za-z]{2}$
address_line_1:
type: string
description: First line of the street address (number and street). Used in the agent-assisted enrollment flow.
maxLength: 50
address_line_2:
type: string
description: Optional second line of the street address (apartment, suite). Used in the agent-assisted enrollment flow.
maxLength: 50
city:
type: string
description: City of the street address. Used in the agent-assisted enrollment flow; ignored on self-service deep links.
maxLength: 50
EnrollmentSessionClient:
type: object
additionalProperties: false
deprecated: true
description: 'DEPRECATED legacy contact block. Every property here has a 1:1 replacement on `household.applicants[]` and is preferred when both are supplied: `first_name` → `household.applicants[primary].first_name`, `last_name` → `household.applicants[primary].last_name`, `email` → `household.applicants[primary].email`, `phone_number` → `household.applicants[primary].phone_number`. Each field that is still supplied here surfaces a `deprecated_field` entry in the response''s `warnings[]` array. The block remains accepted for backwards compatibility but may be removed at any time; migrate to the applicant-level fields.'
properties:
first_name:
type: string
deprecated: true
description: DEPRECATED. Send as `household.applicants[primary].first_name` instead.
maxLength: 50
last_name:
type: string
deprecated: true
description: DEPRECATED. Send as `household.applicants[primary].last_name` instead.
maxLength: 50
email:
type: string
deprecated: true
description: DEPRECATED. Send as `household.applicants[primary].email` instead.
maxLength: 50
phone_number:
type: string
deprecated: true
description: DEPRECATED. Send as `household.applicants[primary].phone_number` instead.
maxLength: 50
EnrollmentSessionEnrollment:
type: object
additionalProperties: false
description: Accepts `hra`. Other enrollment subobjects (`special_enrollment_period`, `communication_preferences`) are not supported and are rejected with `400 invalid_request`.
properties:
hra:
$ref: '#/components/schemas/EnrollmentSessionEnrollmentHra'
EnrollmentSessionHousehold:
type: object
additionalProperties: false
description: '`household_size` must be at least 1. Applicants can include at most one `primary` and one `spouse`.'
properties:
annual_income:
type: number
minimum: 0
household_size:
type: integer
minimum: 1
someone_has_employer_coverage:
type: boolean
applicants:
type: array
items:
$ref: '#/components/schemas/EnrollmentSessionApplicant'
EnrollmentSessionEnrollmentHra:
type: object
additionalProperties: false
required:
- amount
description: When supplied, `amount` is required and must be greater than or equal to 0. `0` means the consumer declined available HRA funding; a positive value means funding was accepted. `frequency` is required when `amount > 0` and is omitted from the deep link when `amount == 0`.
properties:
amount:
type: number
minimum: 0
frequency:
type: string
enum:
- annually
- monthly
- quarterly
- one_time
ErrorResponse:
type: object
description: 'Canonical error envelope returned by HealthSherpa-owned API endpoint, infrastructure filter, and backend service failures. The direct enrollment endpoints return their own service failures using `EnrollmentErrorsResponse` (`errors[]`), but edge-generated failures on those endpoints (API Gateway and WAF, e.g. a missing or invalid API key) still use this `ErrorResponse` envelope. Clients should treat `error.code` as the stable programmatic identifier and surface `error.message` to humans. Recognized codes include:
- `unauthorized` (401) - backend runtime authentication failure when a request reaches a runtime that performs its own authentication
- `forbidden` (403) - missing/invalid/unauthorized API key at the API edge, or edge policy deny
- `invalid_request` (400) - request body or parameter validation failed
- `not_found` (404) - resource or route not found
- `payload_too_large` (413) - request body exceeded edge limits
- `unsupported_media_type` (415) - unsupported Content-Type
- `rate_limited` (429) - edge rate-limiting
- `service_unavailable` (503) - the edge is failing to reach the backend, or an upstream catalog/quoting/lookup service is unavailable
- `bad_gateway` (502) - the edge received an invalid response from the backend
- `gateway_timeout` (504) - the edge did not receive a timely response from the backend
- `internal_error` (500) - unexpected server error'
additionalProperties: true
required:
- error
properties:
error:
type: object
additionalProperties: true
required:
- code
- message
properties:
code:
type: string
description: Stable, machine-readable identifier for the error class (see ErrorResponse description for recognized values). Clients should branch on this field rather than on `message` or HTTP status alone.
message:
type: string
description: Human-readable explanation of the error, suitable for logging or surfacing to an end user.
details:
type: object
description: Optional per-field validation details. Keys are dotted/indexed paths (e.g. `household.applicants[0].age`) and values are arrays of full-sentence error messages.
additionalProperties:
type: array
items:
type: string
EnrollmentSessionApplicantIncomeSource:
type: object
additionalProperties: false
required:
- amount
description: One income line attributed to this applicant. `amount` is required so the enrollment flow receives usable income information.
properties:
employer:
type: string
description: Employer or payer attributed to this income line.
maxLength: 50
amount:
type: number
description: Annual income from this source in dollars.
minimum: 0
EnrollmentSessionResponse:
type: object
additionalProperties: true
required:
- links
description: Response envelope. Fields may be null when the request omits optional inputs (for example, `external_id`). Clients must tolerate new top-level keys appearing additively over time. `warnings` is emitted only when the request used a deprecated field; clients should treat its absence as 'no warnings'.
properties:
external_id:
type: string
nullable: true
description: Echoed from the request when supplied; otherwise null.
links:
$ref: '#/components/schemas/EnrollmentSessionResponseLinks'
warnings:
type: array
description: Non-fatal warnings about the request. Today only the legacy `client.*` contact block emits warnings — one entry per deprecated field that was supplied. The key is omitted entirely when there are no warnings. Clients that want to detect deprecations programmatically should branch on `warnings[].code`.
items:
$ref: '#/components/schemas/EnrollmentSessionResponseWarning'
EnrollmentSessionContext:
type: object
additionalProperties: false
required:
- product
- exchange
- coverage_family
- coverage_type
- plan_year
- flow
- locale
properties:
product:
type: string
enum:
- aca
exchange:
type: string
enum:
- on_exchange
coverage_family:
type: string
enum:
- medical
coverage_type:
type: string
enum:
- medical
plan_year:
type: integer
minimum: 2020
maximum: 2099
flow:
type: string
enum:
- agent_assisted
- self_service
description: Mapped to the deep link's `user_type` (`agent_assisted` -> `agent`, `self_service` -> `consumer`).
locale:
type: string
enum:
- en-US
- es-MX
description: '`es-MX` enables the Spanish flow via the deep link''s `ljs` parameter.'
EnrollmentSessionRequest:
type: object
additionalProperties: false
required:
- context
description: POST /v1/enrollment-sessions request body. Unsupported fields anywhere in the body are rejected with `400 invalid_request`. In `self_service`, supply at least one of `location.state` or top-level `plan_id`. In `agent_assisted`, `location.state` is required and `plan_id` is not accepted.
properties:
external_id:
type: string
description: Partner-supplied identifier echoed in the response and used for CRM correlation. Strongly recommended; must not contain PII.
plan_id:
type: string
description: Plan ID (HIOS ID). Available only for the `self_service` flow, where it identifies the plan for the generated apply link. Invalid for `agent_assisted` — requests carrying `plan_id` with `context.flow` set to `agent_assisted` are rejected with `400 invalid_request`.
notes:
type: string
description: Free-form note for the assisting agent. Used only in the agent-assisted enrollment flow; ignored on self-service deep links. Capped at 500 characters; longer values are rejected with `400 invalid_request`.
maxLength: 500
providers:
type: array
description: Provider identifiers (NPI strings) the household cares about. Used by the agent-assisted enrollment flow and added to the self-service `shopping_url` as a network filter (shop only; not added to `client_apply_url`).
items:
type: string
description: Ten-digit National Provider Identifier (NPI).
pattern: ^[0-9]{10}$
context:
$ref: '#/components/schemas/EnrollmentSessionContext'
client:
$ref: '#/components/schemas/EnrollmentSessionClient'
location:
$ref: '#/components/schemas/EnrollmentSessionLocation'
household:
$ref: '#/components/schemas/EnrollmentSessionHousehold'
enrollment:
$ref: '#/components/schemas/EnrollmentSessionEnrollment'
campaign:
$ref: '#/components/schemas/EnrollmentSessionCampaign'
EnrollmentSessionResponseWarning:
type: object
additionalProperties: true
required:
- code
- field
- message
description: One non-fatal warning about a single request field. New `code` values may be introduced additively over time; clients should treat unknown codes as 'log and continue'.
properties:
code:
type: string
description: Stable, machine-readable warning category. Currently only `deprecated_field` is emitted (when the request uses a `client.*` contact field that has been replaced by a `household.applicants[]` equivalent).
field:
type: string
description: Dotted request path of the field this warning is about (for example, `client.email`).
message:
type: string
description: Human-readable explanation. For deprecation warnings, includes the suggested replacement field path.
EnrollmentSessionCampaign:
type: object
additionalProperties: false
description: 'Marketing attribution. Only accepted when `context.flow` is `self_service`; sending any field with `flow: "agent_assisted"` is rejected with `400 invalid_request`.'
properties:
cid:
type: string
utm_source:
type: string
utm_medium:
type: string
utm_campaign:
type: string
utm_term:
type: string
utm_content:
type: string
display_phone_number:
type: string
pattern: ^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$
description: Phone number HealthSherpa renders in the page header. Must be a 10-digit US phone number (any of `8005551234`, `800-555-1234`, `800.555.1234`, `(800) 555-1234` are accepted); `+1`-prefixed and non-10-digit values are rejected with `400 invalid_request`. Forwarded to the deep link's `call` parameter unchanged.
EnrollmentSessionApplicantIchra:
type: object
additionalProperties: false
properties:
offered:
type: boolean
offered_cafeteria:
type: boolean
employee_amount:
type: number
minimum: 0
family_amount:
type: number
minimum: 0
EnrollmentSessionApplicantPrescription:
type: object
additionalProperties: false
description: Medication identification used for Rx-matched plan ranking. At least one of `id` or `rx_norm_identifier` must be supplied; entries with neither are rejected with `400 invalid_request`.
properties:
id:
type: string
description: HealthSherpa medication catalog identifier.
duration:
type: integer
description: Days-of-supply per fill (informational; not used to filter plans).
minimum: 0
rx_norm_identifier:
type: string
description: RxNorm CUI for the medication.
EnrollmentSessionApplicant:
type: object
additionalProperties: false
required:
- relationship
description: '`date_of_birth` and `age` are mutually exclusive on the same applicant, and `date_of_birth` must not be in the future. `sex` accepts `male` and `female`. `first_name` / `last_name` are accepted on every applicant so non-primary members can carry their own identity for the agent-assisted enrollment flow. `email` and `phone_number` are accepted only on the applicant whose `relationship` is `primary` — supplying them on a spouse or dependent is rejected.'
properties:
relationship:
type: string
enum:
- primary
- spouse
- dependent
first_name:
type: string
description: Given name. Used by the agent-assisted enrollment flow only; ignored on self-service deep links. Helps prefill household member names in the flow.
maxLength: 50
last_name:
type: string
description: Family name. Used by the agent-assisted enrollment flow only; ignored on self-service deep links. Helps prefill household member names in the flow.
maxLength: 50
email:
type: string
description: Email for the primary applicant. Used in the agent-assisted enrollment flow and lead lookup. Replaces the deprecated `client.email`; only accepted when `relationship` is `primary`.
pattern: ^[^@\s]+@[^@\s]+\.[^@\s]+$
maxLength: 50
phone_number:
type: string
description: Phone number for the primary applicant. Used in the agent-assisted enrollment flow. Replaces the deprecated `client.phone_number`; only accepted when `relationship` is `primary`. Permissive shape — digits with optional spaces, dots, parentheses, hyphens, and a leading `+` (7–50 characters); HealthSherpa normalizes the phone number during the flow.
pattern: ^[0-9\s().+\-]{7,50}$
date_of_birth:
type: string
format: date
age:
type: integer
minimum: 0
maximum: 130
sex:
type: string
enum:
- male
- female
uses_tobacco:
type: boolean
pregnant:
type: boolean
parent_caretaker:
type: boolean
rejected_by_medicaid_or_chip:
type: boolean
unemployment:
type: boolean
has_existing_coverage:
type: boolean
ichra:
$ref: '#/components/schemas/EnrollmentSessionApplicantIchra'
prescriptions:
type: array
description: Medications this applicant takes. Used by the agent-assisted enrollment flow only; ignored on self-service deep links. Nested under the applicant rather than carrying an `applicant_index` linkage field.
items:
$ref: '#/components/schemas/EnrollmentSessionApplicantPrescription'
income_sources:
type: array
description: Income lines attributed to this applicant. Used by the agent-assisted enrollment flow only; ignored on self-service deep links. When any applicant supplies `income_sources`, the per-applicant entries replace the household-level `annual_income` aggregation for the agent-assisted flow; `annual_income` continues to drive the self-service deep link regardless.
items:
$ref: '#/components/schemas/EnrollmentSessionApplicantIncomeSource'
responses:
InternalError:
description: An unexpected error occurred.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: internal_error
message: An unexpected error occurred.
RateLimited:
description: Rate limit exceeded. Clients should back off and retry after the delay advertised in the Retry-After header.
headers:
Retry-After:
description: Number of seconds the client should wait before retrying.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: rate_limited
message: Request rate limit exceeded. Please slow down and retry shortly.
IdempotencyInProgress:
description: A previous request with the same `Idempotency-Key` is still being processed by the API. The partner should wait briefly and retry. The response is not produced by the upstream service — the in-flight request from the first call will succeed or fail on its own.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: idempotency_in_progress
message: A request with this Idempotency-Key is already being processed. Retry once it completes.
GatewayTimeout:
description: The API edge did not receive a timely response from the backend.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: gateway_timeout
message: The API edge did not receive a timely response from the backend. Please try again later.
UnsupportedMediaType:
description: The request `Content-Type` is not supported by the endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: unsupported_media_type
message: The request Content-Type is not supported by this endpoint.
ServiceUnavailable:
description: The API is temporarily unable to serve the request. This may be returned by the API edge when the backend is unhealthy, or by the backend when an upstream catalog, quoting, or lookup service is unavailable.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: service_unavailable
message: The service is temporarily unavailable. Please try again later.
BadGateway:
description: The API edge received an invalid response from the backend.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: bad_gateway
message: The API edge received an invalid response from the backend. Please try again later.
BadRequest:
description: 'Request validation failed. The response body''s `error.details` map contains per-field messages when the backend produced the error; edge-generated 400s omit `details`.
A malformed `Idempotency-Key` header is also rejected here with `code: idempotency_key_invalid`.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: invalid_request
message: Validation failed.
details:
zip_code:
- ZIP code must be 5 digits
PayloadTooLarge:
description: The request body exceeded the maximum size accepted by the API edge.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: payload_too_large
message: The request payload exceeded the maximum allowed size.
IdempotencyMismatch:
description: The `Idempotency-Key` has been used before within the 24-hour retention window, but the current request body is not byte-for-byte identical to the original. To resolve, either resubmit the exact original payload or generate a fresh `Idempotency-Key` for the new request.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
err
# --- truncated at 32 KB (32 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/healthsherpa/refs/heads/main/openapi/healthsherpa-enrollment-sessions-api-openapi.yml