API reference

Contract v1.0.0

One JSON shape goes to every endpoint. Learn PriorAuthRequestV1 once and the whole surface follows. Nine endpoints are implemented; eight more are specified and marked as planned, because you should be able to read the roadmap rather than extract it from a call.

Conventions

Six rules that hold across every endpoint. None of them have exceptions, which is the point of writing them down once.

Base URLs

Production https://api.preauthrail.com

Sandbox is the same host — your key decides which world you are in, so promoting an integration is a credential swap, not a URL rewrite.

Authentication

Authorization: Bearer rail_sandbox_…

Every rejection is a 401 with an error kind and nothing more — an invalid, revoked and expired key are deliberately indistinguishable, so a partial key is never an oracle. Keys idle for 90 days expire.

Result envelope

Every JSON response is { ok: true, value } or { ok: false, error }. Errors are values with a stable kind, never a message to string-match. Pattern-match on kind.

Idempotency

Idempotency-Key is required on /submit, the one mutating call. Same key, same body replays the stored response with X-Rail-Idempotent-Replay: true. Same key, different body is 409 — a client bug, never a silent overwrite. Keys live 24 hours.

Rate limits

60 requests per minute per partner on sandbox. Exceeding it returns 429 with Retry-After in seconds. Production tiers negotiate their own ceiling.

Metering

We never bill a failure on our side, never bill an idempotent replay, and never bill the validations that precede a case you submit. Every call is metered with its billability recorded at write time, so an invoice line traces to an event.

The request object

PriorAuthRequestV1 is the single body shape every prior-auth endpoint accepts. Fields marked required are enforced by the schema; several nullable fields are nonetheless mandatory in practice because payers reject without them, and each says so.

Top level

Scalars that sit directly on the request.

FieldTypeReqNotes
profile"aba.v1"yesSelects which vertical’s rules, code catalog and form registry apply. Only "aba.v1" exists today. This is the seam a second vertical hangs off without forcing a v2 — a future "optpt.v1" or "bh.v1" is an added literal, never a breaking change.
partner_referencestring ≤128noYour own identifier for this request, echoed back on every response and used to group events into one billable case. Optional, but send it: without it, correlating a submission with its later status callbacks is your problem rather than ours.
requested_hours_per_weeknumber | nullnoTotal requested weekly hours across all service lines. Null is accepted; several payer forms leave the field blank when the per-code units carry the detail.

Payer — payer

Which plan the request goes to. Matched against the form-mapper registry by name, together with client.state — the same pair drives channel resolution on /submit.

FieldTypeReqNotes
namestringyesThe payer’s name as you hold it, e.g. "Aetna Better Health of New Jersey". Normalised internally to a slug; you do not need to know our slugs. An unmatched name returns payer_not_supported rather than guessing.

Authorization period — period

The service window being requested. Half-open in spirit: `from` inclusive, `to` the end date the payer will see.

FieldTypeReqNotes
fromstring (YYYY-MM-DD)yesStart of the requested authorization period. Rejected unless it matches YYYY-MM-DD exactly — no timestamps, no locale formats.
tostring (YYYY-MM-DD)yesEnd of the requested period. Same format rule.

Service lines — service_codes[]

At least one line is required. Each line is one procedure code and the volume requested for it.

FieldTypeReqNotes
cptstringyesThe procedure code, e.g. "97153". Named `cpt` because most ABA lines are CPT; HCPCS lines use the identical shape and the field name does not change.
descriptionstringyesHuman-readable description of the line, rendered onto payer forms that print it. Send an empty string rather than omitting the key if you have nothing.
units_per_periodnumber | nullnoUnits requested for this line across the whole period. Null when the payer form asks for hours per week instead — see requested_hours_per_week.

Client — client

The person receiving services. This is PHI: in the default stateless mode none of it is persisted — we hold a SHA-256 of the request and nothing else.

FieldTypeReqNotes
namestringyesClient’s legal name as it appears on the plan. Mismatches here are a common payer rejection.
dobstring | nullnoDate of birth, YYYY-MM-DD. Nullable in the schema, but effectively mandatory for any real submission — most payers reject without it.
sexstring | nullnoFree-form rather than an enum, deliberately. Mapped to a FHIR gender code downstream. It will become a closed enum once there is a real inventory of payer-accepted values to widen into — the versioning rule only permits adding enum members, so guessing now would bake in an incomplete vocabulary permanently.
statestring | nullnoTwo-letter state code. Pair it with payer.name: together they select the form mapper and the submission channel, so a null state narrows form-fill to payer-wide mappers only.
diagnosis_icd10string | nullnoPrimary diagnosis, e.g. "F84.0". Drives medical-necessity rules; a null here will fail most payer completeness checks at /validate.
service_delivery_settingstring | nullnoWhere services are delivered — "home", "clinic", "school". Free-form for the same reason as sex.

Enrollment — enrollment

The client’s coverage under this payer. Every field is nullable in the schema; in practice member_id is required by every payer we map.

FieldTypeReqNotes
member_idstring | nullnoMember/subscriber ID on the card. Schema-nullable, payer-mandatory.
group_numberstring | nullnoGroup number where the plan issues one. Commercial plans usually do; Medicaid MCOs usually do not.
subscriber_namestring | nullnoThe subscriber, when that is not the client — typically a parent for a paediatric ABA case.
subscriber_relationshipstring | nullnoRelationship of subscriber to client. Free-form today; the eventual enum will be widened from the X12/FHIR standard relationship codes rather than from our own sparse production data.

Rendering organization — organization

The provider organization billing for the services. Not gated by the PHI trigger — this is your customer’s own configuration, not client data.

FieldTypeReqNotes
namestringyesLegal name of the provider organization.
npistring | nullnoGroup/organizational NPI. The single most common cause of a blank field on a generated form — validate it before you send.
tax_idstring | nullnoEIN. Required by most payer forms even where the schema tolerates null.
phonestring | nullnoContact phone printed on the form.
faxstring | nullnoFax number for payers whose channel resolves to manual submission — which today is all of them.
addressobject | nullnoService/billing address. See the fields below; the whole object may be null.

Organization address — organization.address

Nullable as a whole. When present, payer forms print it verbatim.

FieldTypeReqNotes
streetstring | nullnoStreet line. Multi-line addresses should be joined with a comma — there is no second street field.
citystring | nullnoCity.
statestring | nullnoTwo-letter state code for the organization, which need not match client.state.
zipstring | nullnoPostal code.

Rendering clinician — clinician

The supervising or rendering practitioner. For ABA this is the BCBA who authored the plan.

FieldTypeReqNotes
namestring | nullnoClinician’s name as licensed.
npistring | nullnoIndividual NPI, distinct from the organizational NPI above.
credentialstring | nullnoCredential string, e.g. "BCBA", "BCBA-D".
licensestring | nullnoState licence number where the state licenses the profession.
license_statestring | nullnoJurisdiction that issued the licence.

Clinical — clinical

The clinical substance of the request. This is the part payers actually adjudicate.

FieldTypeReqNotes
treatment_plan_datestring (YYYY-MM-DD)yesDate of the treatment plan backing this request. Required and format-checked — payers reject stale plans, and several mappers print this date on the form.
diagnosing_provider_namestring | nullnoWho made the diagnosis, where the payer requires it to differ from the rendering clinician.
narrativestring | nullnoThe medical-necessity narrative. Carried into the PAS bundle as Claim.supportingInfo, so it is machine-submitted rather than only attached as a PDF. You supply this; drafting it is a separate planned add-on.

Authorization

POST/v1/prior-auth/validatelive

Check a request before anyone sees it

Parses and validates the request against PriorAuthRequestV1 and returns the normalised value, or every failing field path with its message. No payer contact, no submission, no liability — this is the endpoint to call on every keystroke-settled form, not just before submitting.

Meters as
validate
Billing
Billed as a standalone validation when it stands alone. A validate that precedes a submitted case is reconciled into that case at invoicing, not charged twice.
Returns
application/json

Errors

invalid_jsoninvalid_inputunauthorizedrate_limited

Request

{
  "profile": "aba.v1",
  "partner_reference": "clinic-4417/auth-9",
  "payer": { "name": "Aetna Better Health of New Jersey" },
  "period": { "from": "2026-09-01", "to": "2027-03-01" },
  "requested_hours_per_week": 25,
  "service_codes": [
    { "cpt": "97153", "description": "Adaptive behavior treatment by protocol", "units_per_period": 2600 },
    { "cpt": "97155", "description": "Adaptive behavior treatment with protocol modification", "units_per_period": 208 }
  ],
  "client": {
    "name": "Sam Doe",
    "dob": "2020-03-15",
    "sex": "male",
    "state": "NJ",
    "diagnosis_icd10": "F84.0",
    "service_delivery_setting": "home"
  },
  "enrollment": {
    "member_id": "W248166043",
    "group_number": null,
    "subscriber_name": "Alex Doe",
    "subscriber_relationship": "parent"
  },
  "organization": {
    "name": "Northwind Behavioral Health",
    "npi": "1548392017",
    "tax_id": "82-4419907",
    "phone": "609-555-0142",
    "fax": "609-555-0143",
    "address": { "street": "18 Mercer St", "city": "Princeton", "state": "NJ", "zip": "08540" }
  },
  "clinician": {
    "name": "Lisa Provider",
    "npi": "1932847561",
    "credential": "BCBA",
    "license": "35BX00123400",
    "license_state": "NJ"
  },
  "clinical": {
    "treatment_plan_date": "2026-08-14",
    "diagnosing_provider_name": "Dr. Eval Provider",
    "narrative": "The client demonstrates significant deficits across…"
  }
}

Response

{ "ok": true, "value": { /* the normalised request */ } }
POST/v1/prior-auth/official-formlive

Fill the payer’s own PDF

Selects the form mapper from payer.name plus client.state, fills the payer’s real authorization form, and returns the PDF bytes. 22 payer forms are mapped today, each covered by a golden-file test. Needs no payer connection — the form is a document, not a transaction.

Meters as
form.filled
Billing
Billed per filled form when used standalone; included in the case when the same partner_reference is submitted.
Returns
application/pdf

Errors

invalid_jsoninvalid_inputno_form_mapperunauthorizedrate_limited

Request

{
  "profile": "aba.v1",
  "partner_reference": "clinic-4417/auth-9",
  "payer": { "name": "Aetna Better Health of New Jersey" },
  "period": { "from": "2026-09-01", "to": "2027-03-01" },
  "requested_hours_per_week": 25,
  "service_codes": [
    { "cpt": "97153", "description": "Adaptive behavior treatment by protocol", "units_per_period": 2600 },
    { "cpt": "97155", "description": "Adaptive behavior treatment with protocol modification", "units_per_period": 208 }
  ],
  "client": {
    "name": "Sam Doe",
    "dob": "2020-03-15",
    "sex": "male",
    "state": "NJ",
    "diagnosis_icd10": "F84.0",
    "service_delivery_setting": "home"
  },
  "enrollment": {
    "member_id": "W248166043",
    "group_number": null,
    "subscriber_name": "Alex Doe",
    "subscriber_relationship": "parent"
  },
  "organization": {
    "name": "Northwind Behavioral Health",
    "npi": "1548392017",
    "tax_id": "82-4419907",
    "phone": "609-555-0142",
    "fax": "609-555-0143",
    "address": { "street": "18 Mercer St", "city": "Princeton", "state": "NJ", "zip": "08540" }
  },
  "clinician": {
    "name": "Lisa Provider",
    "npi": "1932847561",
    "credential": "BCBA",
    "license": "35BX00123400",
    "license_state": "NJ"
  },
  "clinical": {
    "treatment_plan_date": "2026-08-14",
    "diagnosing_provider_name": "Dr. Eval Provider",
    "narrative": "The client demonstrates significant deficits across…"
  }
}

Response

Binary PDF. `X-Rail-Form-Mapper` names the mapper that produced it; `Content-Disposition` carries a filename built from the mapper slug and your partner_reference.

POST/v1/prior-auth/bundlelive

Build the FHIR bundle, don’t send it

Returns the Da Vinci PAS request bundle we would transmit — assembled, normalised, ready for your own connection to carry. This is the bring-your-own-rail product: you keep the payer relationship and the submission liability, we do the assembly. Pure and side-effect free; it never resolves a channel or touches payer config.

Meters as
bundle.built
Billing
Priced as its own line at roughly 60% of a full authorization case.
Returns
application/fhir+json

Errors

invalid_jsoninvalid_inputunauthorizedrate_limited

Request

{
  "profile": "aba.v1",
  "partner_reference": "clinic-4417/auth-9",
  "payer": { "name": "Aetna Better Health of New Jersey" },
  "period": { "from": "2026-09-01", "to": "2027-03-01" },
  "requested_hours_per_week": 25,
  "service_codes": [
    { "cpt": "97153", "description": "Adaptive behavior treatment by protocol", "units_per_period": 2600 },
    { "cpt": "97155", "description": "Adaptive behavior treatment with protocol modification", "units_per_period": 208 }
  ],
  "client": {
    "name": "Sam Doe",
    "dob": "2020-03-15",
    "sex": "male",
    "state": "NJ",
    "diagnosis_icd10": "F84.0",
    "service_delivery_setting": "home"
  },
  "enrollment": {
    "member_id": "W248166043",
    "group_number": null,
    "subscriber_name": "Alex Doe",
    "subscriber_relationship": "parent"
  },
  "organization": {
    "name": "Northwind Behavioral Health",
    "npi": "1548392017",
    "tax_id": "82-4419907",
    "phone": "609-555-0142",
    "fax": "609-555-0143",
    "address": { "street": "18 Mercer St", "city": "Princeton", "state": "NJ", "zip": "08540" }
  },
  "clinician": {
    "name": "Lisa Provider",
    "npi": "1932847561",
    "credential": "BCBA",
    "license": "35BX00123400",
    "license_state": "NJ"
  },
  "clinical": {
    "treatment_plan_date": "2026-08-14",
    "diagnosing_provider_name": "Dr. Eval Provider",
    "narrative": "The client demonstrates significant deficits across…"
  }
}

Response

{ "resourceType": "Bundle", "type": "collection", "entry": [ /* Claim, Patient, Practitioner, Organization, Coverage… */ ] }
POST/v1/prior-auth/submitsandbox onlyIdempotency-Key

Submit the authorization

Resolves the channel for this payer and state, then submits. Requires an Idempotency-Key: the same key with the same body replays the stored response rather than submitting twice; the same key with a different body is a client bug and returns 409 rather than silently overwriting.

Meters as
case.submitted
Billing
The authorization case — the headline line item. Covers CRD, DTR, the filled form, 180 days of status, and one resubmission within 90 days. An idempotent replay records its own event marked as a retry and is never charged again.
Returns
application/json

Errors

invalid_jsoninvalid_inputidempotency_key_requiredidempotency_key_reusedunauthorizedrate_limited

Request

{
  "profile": "aba.v1",
  "partner_reference": "clinic-4417/auth-9",
  "payer": { "name": "Aetna Better Health of New Jersey" },
  "period": { "from": "2026-09-01", "to": "2027-03-01" },
  "requested_hours_per_week": 25,
  "service_codes": [
    { "cpt": "97153", "description": "Adaptive behavior treatment by protocol", "units_per_period": 2600 },
    { "cpt": "97155", "description": "Adaptive behavior treatment with protocol modification", "units_per_period": 208 }
  ],
  "client": {
    "name": "Sam Doe",
    "dob": "2020-03-15",
    "sex": "male",
    "state": "NJ",
    "diagnosis_icd10": "F84.0",
    "service_delivery_setting": "home"
  },
  "enrollment": {
    "member_id": "W248166043",
    "group_number": null,
    "subscriber_name": "Alex Doe",
    "subscriber_relationship": "parent"
  },
  "organization": {
    "name": "Northwind Behavioral Health",
    "npi": "1548392017",
    "tax_id": "82-4419907",
    "phone": "609-555-0142",
    "fax": "609-555-0143",
    "address": { "street": "18 Mercer St", "city": "Princeton", "state": "NJ", "zip": "08540" }
  },
  "clinician": {
    "name": "Lisa Provider",
    "npi": "1932847561",
    "credential": "BCBA",
    "license": "35BX00123400",
    "license_state": "NJ"
  },
  "clinical": {
    "treatment_plan_date": "2026-08-14",
    "diagnosing_provider_name": "Dr. Eval Provider",
    "narrative": "The client demonstrates significant deficits across…"
  }
}

Response

{
  "ok": true,
  "value": {
    "ok": false,
    "kind": "manual",
    "status": "not_capable",
    "httpStatus": null,
    "outcome": null,
    "preAuthRef": null,
    "message": "No electronic rail configured for this payer — use /official-form and the payer's fax."
  }
}
POST/v1/prior-auth/inquiresandbox only

Where did it get to?

Retrieves a decision for a prior submission, including asynchronous ones a payer pended. Read-only, so no Idempotency-Key. Pass `trace_number` alongside the request body when you have one. When the resolved channel cannot inquire, you get a well-formed not_capable answer rather than an error.

Meters as
inquire
Billing
Never billed. Every status inquiry for 180 days is included in the case.
Returns
application/json

Errors

invalid_jsoninvalid_inputunauthorizedrate_limited

Request

{ /* PriorAuthRequestV1 */, "trace_number": "PA-2026-8841" }

Response

{ "ok": true, "value": { "status": "pended", "preAuthRef": null, "disposition": null } }

Discovery

POST/v1/prior-auth/crdsandbox only

What does this payer require?

Coverage Requirements Discovery: whether prior authorization is needed for these codes under this coverage, and what documentation the payer wants. Always answers 200 — a payer that has no CRD service is a legitimate answer about capability, not a request error.

Meters as
crd
Billing
Priced as a standalone discovery call; included when the case is submitted.
Returns
application/json

Errors

invalid_jsoninvalid_inputunauthorizedrate_limited

Request

{
  "profile": "aba.v1",
  "partner_reference": "clinic-4417/auth-9",
  "payer": { "name": "Aetna Better Health of New Jersey" },
  "period": { "from": "2026-09-01", "to": "2027-03-01" },
  "requested_hours_per_week": 25,
  "service_codes": [
    { "cpt": "97153", "description": "Adaptive behavior treatment by protocol", "units_per_period": 2600 },
    { "cpt": "97155", "description": "Adaptive behavior treatment with protocol modification", "units_per_period": 208 }
  ],
  "client": {
    "name": "Sam Doe",
    "dob": "2020-03-15",
    "sex": "male",
    "state": "NJ",
    "diagnosis_icd10": "F84.0",
    "service_delivery_setting": "home"
  },
  "enrollment": {
    "member_id": "W248166043",
    "group_number": null,
    "subscriber_name": "Alex Doe",
    "subscriber_relationship": "parent"
  },
  "organization": {
    "name": "Northwind Behavioral Health",
    "npi": "1548392017",
    "tax_id": "82-4419907",
    "phone": "609-555-0142",
    "fax": "609-555-0143",
    "address": { "street": "18 Mercer St", "city": "Princeton", "state": "NJ", "zip": "08540" }
  },
  "clinician": {
    "name": "Lisa Provider",
    "npi": "1932847561",
    "credential": "BCBA",
    "license": "35BX00123400",
    "license_state": "NJ"
  },
  "clinical": {
    "treatment_plan_date": "2026-08-14",
    "diagnosing_provider_name": "Dr. Eval Provider",
    "narrative": "The client demonstrates significant deficits across…"
  }
}

Response

{ "ok": true, "value": { "status": "not_configured", "authRequired": null, "questionnaires": [] } }
POST/v1/prior-auth/dtrsandbox only

Fetch and prefill the questionnaire

Documentation Templates and Rules: pulls the payer’s questionnaire for the canonical URLs a prior CRD response named, and pre-populates every answer derivable from the request you already sent. Pass those canonicals as `questionnaire_canonicals` — it is DTR-specific and deliberately outside the contract schema.

Meters as
dtr
Billing
Priced standalone; included when the case is submitted.
Returns
application/json

Errors

invalid_jsoninvalid_inputunauthorizedrate_limited

Request

{ /* PriorAuthRequestV1 */, "questionnaire_canonicals": ["http://payer.example/Questionnaire/aba-initial"] }

Response

{ "ok": true, "value": { "status": "not_configured", "questionnaire": null, "response": null, "answered": 0 } }

Account

POST/v1/sandbox/signupliveno auth

Issue a sandbox key

Name and email in, a working API key out, immediately. No BAA, no contract, no card, no approval step. The key is returned exactly once — only its SHA-256 is stored, so a lost key means a new signup.

Meters as
nothing
Billing
Never billed.
Returns
application/json

Errors

invalid_jsonname_requiredvalid_email_required

Request

{ "name": "Northwind Health", "email": "[email protected]" }

Response

{
  "ok": true,
  "value": {
    "partner_id": "b3f1…",
    "api_key": "rail_sandbox_9Xq2…",
    "key_prefix": "rail_sandbox_9Xq2",
    "tier": "sandbox",
    "warning": "This key is shown once and cannot be retrieved again…"
  }
}

Operations

GET/healthliveno auth

Liveness and build version

Unauthenticated. Returns the running core version and uptime. Poll it from your own monitoring — it is the same check our deploy gate uses.

Meters as
nothing
Billing
Never billed.
Returns
application/json

Errors

Response

{ "status": "ok", "core_version": "2026.08.21", "uptime_seconds": 41022 }

Error taxonomy

Errors are typed values, not thrown exceptions and not prose. Branch on kind. Note that several kinds arrive inside a 200 — a payer having no electronic rail is a true answer about capability, not a failed request, and conflating the two would make you retry things that will never succeed.

KindHTTPMeaning
invalid_json400The body did not parse as JSON at all.
invalid_input400Schema validation failed. Carries an `issues` array — one entry per failing path, with the field path and message, so you can surface field-level feedback without guessing.
idempotency_key_required400A mutating call arrived without an Idempotency-Key header.
idempotency_key_reused409That Idempotency-Key was already used with a different body. Almost always a key-generation bug on your side.
no_form_mapper404No official form is mapped for this payer and state. Fall back to /bundle or request the payer.
payer_not_supported400No payer entry matches that name at all — as distinct from a payer we know but cannot submit to.
not_capable200The payer is known but has no electronic rail configured, so the manual/fax path applies. A status, not a failure — which is why it arrives inside a 200.
not_configured200The rail exists but its credentials or endpoint config are missing.
submission_failed200The submission itself failed after passing validation.
unauthorized401Missing, malformed, unknown, revoked, suspended or idle-expired key. Deliberately indistinguishable from each other.
rate_limited429Over your per-minute ceiling. Retry-After tells you how long to wait.
provider_error200The upstream payer or FHIR endpoint returned an error.
network_error200Timeout, DNS failure or connection reset reaching the payer.
internal_error500Our bug, not your mistake. Never billed.

Planned surface

Specified, not shipped. Each of these traces to something already half-built — a usage event type with no route, a table with no reader — rather than to an aspiration. Dates are deliberately absent; a roadmap with dates on a pre-revenue product is a work of fiction.

GET/v1/prior-auth/{id}planned

Status by id

Fetch a submission’s current state by the id we returned, instead of re-posting the whole request to /inquire. The ergonomic version of status polling.

What existsWaiting on rail.submissions, the durable store that replaces today’s in-memory idempotency map. The columns are specified — request_sha256, status, trace_number, pre_auth_ref — they just have no reader yet.

GET/v1/payersplanned

Capability matrix

Which payers we map, which rail each resolves to, which form spec applies, and what each is capable of. Generated from the registry so it cannot drift from behaviour.

What existsThe 22 mappers and their slugs already exist and are golden-file tested; what is missing is the rail.payer_registry table that exposes them with effective dating.

GET/v1/codesplanned

Date-effective code resolution

Resolve service codes for a date of service, including the CPT 2027 ABA crosswalk. Ask for a code on a future date and get the code that will be correct then.

What existsThe catalog and its effective dating exist in the platform today; this is an extraction into the rail package, not new logic.

POST/v1/narrativeplanned

Draft the medical-necessity narrative

Generate the clinical.narrative from the structured request, citing only facts present in it. An add-on, not part of the case — you may well have your own.

What existsThe narrative.drafted usage event type and its rate-card row already exist; the generation pipeline runs in the Mand product and has not been extracted into a rail route.

GET/v1/usageplanned

Read your own metering

Your usage events — event type, case grouping, idempotency key, billable or not and why — so you can reconcile an invoice yourself rather than trusting ours.

What existsEvery call you make is already metered into rail.usage_events with its billability classified at write time. There is simply no endpoint that reads it back — which is why the portal shows no usage rather than showing zeros.

POST/v1/webhooksplanned

Register a webhook endpoint

Signed, retried delivery of asynchronous payer decisions, with replay from the portal. Webhooks rather than polling is the intended way to learn a pended case resolved.

What existsSpecified down to the tables — rail.webhook_endpoints and rail.webhook_deliveries with per-partner secrets and exponential backoff — and not yet built. Until it exists, poll /inquire.

POST/oauth/tokenplanned

OAuth2 client credentials

Exchange a client id and secret for a short-lived access token, for partners whose security review requires it over a long-lived API key.

What existsSandbox is API-key only by design. This lands with the Embedded tier, alongside IP allowlisting — and is the moment to fix key revocation properly rather than carrying a stateless-token pattern into the rail.

POST/v1/keysplanned

Rotate and manage keys

Issue an additional key, roll one without downtime, and revoke by prefix. Today a sandbox partner gets exactly one key at signup and cannot rotate it.

What existsrail.partner_api_keys already carries revoked_at, prefix and last_used_at — the schema supports rotation; the routes do not exist.

Versioning

  • Additive only within a major. New optional fields and new enum members at the end. Never a removal, never a tightened constraint, never a narrowed enum.
  • Payer forms, rules and codes are data, versioned independently with effective dating. A new state rule is never a contract change.
  • Every response carries its versions. rail-contract-version and rail-ruleset-version, so a result you got today is reproducible a year from now — which is also our answer when a payer decision is disputed.
  • Twelve-month deprecation window, announced in Sunset headers and the changelog.