API reference

Graft API

Quickstart

Setup and journey: /onboarding. Live sandbox calls: /try. This page is field-level reference (shapes, fields, curl).

Base: https://api.usegraft.dev/v1. Auth: Authorization: Bearer $GRAFT_KEY.

bash
curl https://api.usegraft.dev/v1/check \
  --request POST \
  --header "Authorization: Bearer $GRAFT_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "payer_id": "graft_sandbox",
    "member": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "member_id": "GRAFT-APPROVE-NOW",
      "dob": "1985-12-10"
    },
    "service": {
      "cpt": "72148",
      "icd10": ["M54.16"],
      "service_date": "2026-09-01"
    },
    "provider": {
      "npi": "1669613154"
    }
  }'
json
{
  "auth_required": true,
  "method": "api",
  "requirements_available": true,
  "check_id": "chk_9f3c2a1b8d7e4f60"
}

Introduction

Graft is the payer connecting layer: one JSON API across payers. Describe member, service, and provider; Graft returns whether auth is required, what documentation is needed, and the decision. Base: https://api.usegraft.dev/v1 over HTTPS.

bash
curl https://api.usegraft.dev/v1/check \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data @check.json
json
{
  "auth_required": true,
  "method": "api",
  "requirements_available": true,
  "check_id": "chk_9f3c2a1b8d7e4f60"
}

Authentication

API calls use a Bearer token in the Authorization header. Sandbox keys look like grft_sandbox_*.

Sandbox keys require login. Sign in with a magic link (homepage or POST /auth/login), then create keys in the dashboard. The dashboard calls POST /v1/account/keys with a session cookie. Anonymous key creation is impossible; there is no unauthenticated create-key route.

Production keys (grft_live_*) are CLI/admin only (not self-serve). They require a BAA (coming). Authenticated calls are rate-limited per key (dashboard default 60/min) with X-RateLimit-* headers; over-limit returns 429 with Retry-After.

bash
curl https://api.usegraft.dev/v1/status/auth_5b1e9d7c3a2f4008 \
  --header "Authorization: Bearer grft_live_9k2m47xq..."
json
{
  "detail": "Unknown API key."
}

Auth failures return HTTP 401 with a detail string and WWW-Authenticate: Bearer. Common messages: missing bearer token, unrecognized prefix, unknown key, revoked key.

POST /auth/login

Request a magic-link email. No authentication. Always returns the same message (no email enumeration). The link hits GET /auth/verify, sets a session cookie, and redirects to /dashboard where you create and revoke keys. Rate-limited to 3 requests per IP per hour (HTTP 429 with Retry-After).

bash
curl https://api.usegraft.dev/v1/auth/login \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{"email":"you@clinic.com"}'
json
{
  "message": "Check your email for a login link."
}

request fields

email string, required
Work email. A one-time magic link is sent; clicking it opens the dashboard session.
next string, optional
Post-login URL after verify (e.g. https://usegraft.dev/dashboard). Must match an allowed origin.

response fields

message string
Always the same text. Does not reveal whether the email is known.

GET /coverage

Public payer capability matrix. No auth. Use it to discover which payer_id values exist and what each adapter honestly supports. Sandbox keys (grft_sandbox_*) call non-production payers (graft_sandbox and inferno). Production adapters like aetna return 403 until you use a live key. Live keys cannot call Inferno or graft_sandbox.

bash
curl https://api.usegraft.dev/v1/coverage
json
[
  {
    "payer_id": "graft_sandbox",
    "display_name": "Graft Sandbox",
    "mode": "simulated",
    "check": true,
    "requirements": "simulated",
    "submit": true,
    "status": false,
    "last_verified": null
  },
  {
    "payer_id": "inferno",
    "display_name": "ONC Inferno (PAS / CRD / DTR)",
    "mode": "live-verified",
    "check": true,
    "requirements": "payer",
    "submit": true,
    "status": true,
    "last_verified": null
  },
  {
    "payer_id": "aetna",
    "display_name": "Aetna",
    "mode": "spec-built",
    "check": true,
    "requirements": "payer",
    "submit": true,
    "status": true,
    "last_verified": null
  }
]

row fields

payer_id string
Graft's identifier for the payer. Pass it as payer_id on check and enroll.
display_name string
Human-readable payer name.
mode "simulated" | "spec-built" | "live-verified"
Integration maturity. Honest about what is fabricated vs exercised live.
check boolean
Whether POST /check is supported for this payer.
requirements "payer" | "simulated" | "none"
Questionnaire source for GET /requirements.
submit boolean
Whether POST /submit is supported.
status boolean
Whether the payer supports status polling. Sandbox is push-driven (false).
last_verified date | null
When a live integration was last verified end-to-end.

POST /check

Ask whether a service needs prior authorization for a given member, plan, and date. This is the entry point for everything else: the returned check_id threads through requirements and submission. Optional hook and payer_context drive CRD timing and Inferno session routing.

bash
curl https://api.usegraft.dev/v1/check \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data '{
    "payer_id": "graft_sandbox",
    "hook": "sign",
    "member": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "member_id": "GRAFT-APPROVE-NOW",
      "dob": "1985-12-10"
    },
    "service": {
      "cpt": "72148",
      "icd10": ["M54.16"],
      "service_date": "2026-09-01",
      "place_of_service": "11"
    },
    "provider": {
      "npi": "1669613154"
    }
  }'
json
{
  "auth_required": true,
  "method": "api",
  "requirements_available": true,
  "check_id": "chk_9f3c2a1b8d7e4f60"
}

request fields

payer_id string, required
Graft's identifier for the payer. Use graft_sandbox in the sandbox; inferno for ONC suites; see GET /coverage for the full list.
plan_id string, optional
Narrows the check to a specific plan when the payer's rules differ by plan. Omit to use payer-level rules.
hook "sign" | "dispatch", optional
Clinical ordering moment for CRD. Defaults to sign (order finalization). Use dispatch when the order is handed off to a performing organization (Inferno CRD 1.2.5).
member.first_name string, required
Member's legal first name as it appears on the insurance card.
member.last_name string, required
Member's legal last name.
member.member_id string, required
Member ID from the insurance card. In sandbox, magic values simulate payer behavior (see Sandbox).
member.dob date, required
Date of birth, ISO 8601 (YYYY-MM-DD).
service.cpt string, required
CPT or HCPCS for the service. Sandbox auth is CPT-driven: 70551-70553, 72148, 72141, 97110, J1*/J2*/J3* → auth required; e.g. 99213 → not required.
service.icd10 string[], required
ICD-10 diagnosis codes. Does not decide sandbox auth_required (Z00.00 alone will not clear auth if CPT still requires it).
service.service_date date, required
Planned date of service. Rules are evaluated as of this date.
service.place_of_service string, optional
CMS place-of-service code (e.g. 11 for office). Some payers key rules off it.
provider.npi string, required
Rendering provider's 10-digit NPI (Luhn-validated).
provider.organization_npi string, optional
Billing organization's NPI, when different from the rendering provider.
payer_context.session_id string, optional
Shared Inferno session id (alphanumeric, 6-32 chars). Used when a per-IG field is omitted. Never interpolated into a URL.
payer_context.crd_session_id string, optional
Inferno CRD Client Suite session id (preferred over session_id for CRD).
payer_context.pas_session_id string, optional
Inferno PAS Client Suite session id (preferred for PAS submit / update / cancel).
payer_context.dtr_session_id string, optional
Inferno DTR Full EHR Suite session id (preferred for GET /requirements).
payer_context.crd_prefetch "complete" | "subset", optional
Inferno CRD only: which simulated prefetch server to call. Defaults to complete. Use both profiles (separate checks) for Cross Hook 1.3.05.

response fields

auth_required boolean
Whether this payer requires prior authorization for this service, member, and date.
method "api" | "portal_only" | "unknown"
How the payer accepts submissions. Graft handles electronic submission behind POST /submit when method is api.
requirements_available boolean
Whether a structured questionnaire is available via GET /requirements.
check_id string
Reference to this check. Pass it to GET /requirements and POST /submit.
summary string | null
Payer CRD card summary text when present. Null for sandbox determinations.
documentation_links array
Documentation / SMART app links from CRD cards. Empty when the payer returned none.

GET /requirements

Fetch the payer's questionnaire for a check as structured items: the questions, their types, and which are required. Answer them in answers[] on POST /submit. Sandbox returns the generic fallback (8 items including attachments); truncated below.

bash
curl "https://api.usegraft.dev/v1/requirements?check_id=chk_9f3c2a1b8d7e4f60" \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "questionnaire_id": "generic-fallback-v1",
  "title": "Prior authorization - general documentation requirements",
  "source": "simulated",
  "meta": {
    "source_payer": null,
    "extraction_completeness": "generic",
    "generated_at": "2026-07-17"
  },
  "items": [
    {
      "link_id": "indication",
      "text": "What is the clinical indication for this service?",
      "type": "string",
      "required": true,
      "options": null,
      "prefill": null
    },
    {
      "link_id": "conservative_therapy_tried",
      "text": "Has conservative treatment been attempted for this condition?",
      "type": "boolean",
      "required": true,
      "options": null,
      "prefill": null
    },
    {
      "link_id": "symptom_duration",
      "text": "How long has the patient had these symptoms?",
      "type": "choice",
      "required": true,
      "options": [
        { "value": "less-than-4-weeks", "label": "Less than 4 weeks" },
        { "value": "4-12-weeks", "label": "4-12 weeks" },
        { "value": "more-than-12-weeks", "label": "More than 12 weeks" }
      ],
      "prefill": null
    },
    {
      "link_id": "clinical_notes",
      "text": "Attach the relevant clinical notes.",
      "type": "attachment",
      "required": true,
      "options": null,
      "prefill": null
    }
  ]
}

item fields

link_id string
Stable identifier for the item. Echo it back in answers[] on POST /submit.
text string
Human-readable question, taken from the payer's own questionnaire wherever possible.
type "boolean" | "string" | "choice" | "attachment"
Expected answer shape. Use value_boolean, value_string, or value_choice; attachment items go in attachments[].
required boolean
Whether the payer rejects submissions missing this item.
options {value, label}[] | null
Allowed values for choice items. Null otherwise. Submit value_choice with the option's value.
prefill null
Reserved for future clinical prefill. Always null in v1.
source "payer_dtr" | "payer" | "simulated"
Top-level field: payer_dtr means live DTR $questionnaire-package; payer is legacy; simulated means Graft reconstructed it from published policy.
meta object
Top-level provenance (extraction_completeness, generated_at, etc.).

POST /submit

Submit the answered questionnaire to the payer. Returns 201 Created. With GRAFT-APPROVE-NOW the call resolves to approved after a brief pended transition; GRAFT-MODIFY-001 returns modified; other magic IDs pend and decide later via webhook. Poll GET /status/{id} or GET /events if you can't receive webhooks. An Idempotency-Key header is required so network retries never double-submit (replays set Idempotent-Replay: true).

bash
curl https://api.usegraft.dev/v1/submit \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Idempotency-Key: 4d2ba8c0-6c1f-4d3e-9b1a-2f83a01e6b7d" \
  --header "Content-Type: application/json" \
  --data '{
    "check_id": "chk_9f3c2a1b8d7e4f60",
    "answers": [
      { "link_id": "indication", "value_string": "Lumbar radiculopathy" },
      { "link_id": "conservative_therapy_tried", "value_boolean": true },
      { "link_id": "prior_imaging_or_testing", "value_boolean": true },
      { "link_id": "symptom_duration", "value_choice": "more-than-12-weeks" },
      { "link_id": "red_flags", "value_boolean": false }
    ],
    "attachments": [],
    "urgency": "standard"
  }'
json
{
  "auth_id": "auth_5b1e9d7c3a2f4008",
  "status": "approved",
  "auth_number": "AUTH-2026-0098231",
  "tracking_id": "trk-a1b2c3d4e5f6",
  "payer_decision_deadline": "2026-09-15T14:11:09Z"
}

request fields

check_id string, required
The check this submission answers. Requirements are validated against it.
answers[] array
One { link_id, value_* } per questionnaire item. Use value_boolean, value_string, or value_choice.
attachments[] array, optional
Supporting documents as { link_id, title, content_type, data_base64 }. Max 20 MB decoded per attachment.
urgency "standard" | "urgent", optional
Defaults to standard. Urgent follows the payer's expedited path (72-hour decisions under CMS-0057-F).
Idempotency-Key header, required
Any unique string. Retries with the same key return the original 201 and set Idempotent-Replay: true.

response fields

auth_id string
Graft's identifier for the authorization. Use it with GET /status/{id}, update, cancel, and chain.
status "pended" | "approved" | "denied" | "modified"
Initial state. GRAFT-APPROVE-NOW decides synchronously after a brief pended transition; GRAFT-MODIFY-001 returns modified; most other flows stay pended first.
auth_number string | null
The payer's authorization number. null until approved or modified.
tracking_id string
Payer-side tracking reference (sandbox shape: trk- + 12 hex).
payer_decision_deadline datetime
When the payer must decide under CMS-0057-F timelines, given the urgency (14 days standard, 72 hours urgent).

GET /status/{id}

The current state of an authorization plus its full history: every public transition with a timestamp, in order. Public status is pended, approved, denied, modified, or cancelled (internal info_requested collapses to pended). Sandbox approvals are valid for ~90 days from the approval date. The sandbox is push-driven (status: false on coverage), but Graft still serves status from its own store after you submit. For Inferno, this reads Graft's authoritative record - it does not call the payer's Claim/$inquire.

bash
curl https://api.usegraft.dev/v1/status/auth_5b1e9d7c3a2f4008 \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "auth_id": "auth_5b1e9d7c3a2f4008",
  "status": "approved",
  "auth_number": "AUTH-2026-0098231",
  "tracking_id": "trk-a1b2c3d4e5f6",
  "denial_reason": null,
  "valid_from": "2026-09-01",
  "valid_until": "2026-11-30",
  "payer_decision_deadline": "2026-09-15T14:11:09Z",
  "history": [
    { "status": "pended", "occurred_at": "2026-09-01T14:11:09Z", "note": null },
    { "status": "approved", "occurred_at": "2026-09-01T14:11:09Z", "note": null }
  ]
}

POST /auths/{id}/update

Amend line items on an authorization (PAS Claim Update). Returns 201 Created with a new derived auth_id - the parent is never mutated. Body is delta-only: list only the items you are adding, modifying, or cancelling. Unlisted items carry forward unchanged. Response includes the complete resulting item set.

bash
curl https://api.usegraft.dev/v1/auths/auth_5b1e9d7c3a2f4008/update \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data '{
    "items": [
      {
        "sequence": 2,
        "change": "added",
        "cpt": "97110",
        "icd10": ["M54.5"],
        "service_date": "2026-09-01",
        "place_of_service": "11"
      }
    ]
  }'
json
{
  "auth_id": "auth_a1b2c3d4e5f60001",
  "parent_auth_id": "auth_5b1e9d7c3a2f4008",
  "status": "pended",
  "auth_number": null,
  "tracking_id": "trk-b2c3d4e5f6a7",
  "payer_decision_deadline": "2026-09-15T14:11:09Z",
  "items": [
    {
      "sequence": 1,
      "cpt": "72148",
      "icd10": ["M54.16"],
      "service_date": "2026-09-01",
      "place_of_service": "11",
      "change": null,
      "cancelled": false
    },
    {
      "sequence": 2,
      "cpt": "97110",
      "icd10": ["M54.5"],
      "service_date": "2026-09-01",
      "place_of_service": "11",
      "change": "added",
      "cancelled": false
    }
  ],
  "decision_items": []
}

request fields

items[] array, required
Delta only - items being acted on. Unlisted parent items carry forward unchanged.
items[].sequence integer, required
Claim.item.sequence. Assign new sequences for added lines; preserve parent sequences for modified/cancelled.
items[].change "added" | "modified" | "cancelled", required
How this line changes relative to the immediate parent. Omitting change is a 422.
items[].cpt string
Required when change is added or modified.
items[].icd10 string[]
ICD-10 codes for added/modified lines.
items[].service_date date
Planned date of service for added/modified lines.
items[].place_of_service string, optional
CMS place-of-service code.

response fields

auth_id string
New derived authorization id (append-only; parent is never mutated).
parent_auth_id string
Immediate parent authorization id.
status AuthStatus
Payer adjudication for this version: pended, approved, denied, modified, or cancelled.
items array
Complete resulting item set after merge (every line, including carried-forward).
decision_items array
Per-line payer adjudication when returned synchronously.

POST /auths/{id}/cancel

Cancel an entire authorization (PAS Claim Update with certificationType 3). Returns 201 Created with a new derived auth whose status is cancelled. Irreversible and separate from update by design - no request body. Fires auth.cancelled when the payer acknowledges.

bash
curl https://api.usegraft.dev/v1/auths/auth_5b1e9d7c3a2f4008/cancel \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "auth_id": "auth_c3d4e5f6a7b80002",
  "parent_auth_id": "auth_5b1e9d7c3a2f4008",
  "status": "cancelled",
  "auth_number": null,
  "tracking_id": "trk-c3d4e5f6a7b8",
  "items": [],
  "decision_items": []
}

response fields

auth_id string
New derived authorization id (append-only; parent is never mutated).
parent_auth_id string
Immediate parent authorization id.
status AuthStatus
Payer adjudication for this version: pended, approved, denied, modified, or cancelled.
items array
Complete resulting item set after merge (every line, including carried-forward).
decision_items array
Per-line payer adjudication when returned synchronously.

GET /auths/{id}/chain

Return the full derivation lineage for an authorization. Pass any member of the chain; Graft walks up via derived_from_auth_id and down via children. Versions are oldest → newest.

bash
curl https://api.usegraft.dev/v1/auths/auth_5b1e9d7c3a2f4008/chain \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "auth_id": "auth_5b1e9d7c3a2f4008",
  "current_auth_id": "auth_c3d4e5f6a7b80002",
  "versions": [
    {
      "auth_id": "auth_5b1e9d7c3a2f4008",
      "status": "approved",
      "derivation_type": null,
      "derived_from_auth_id": null,
      "has_successor": true
    },
    {
      "auth_id": "auth_a1b2c3d4e5f60001",
      "status": "pended",
      "derivation_type": "correct",
      "derived_from_auth_id": "auth_5b1e9d7c3a2f4008",
      "has_successor": true
    },
    {
      "auth_id": "auth_c3d4e5f6a7b80002",
      "status": "cancelled",
      "derivation_type": "cancel",
      "derived_from_auth_id": "auth_a1b2c3d4e5f60001",
      "has_successor": false
    }
  ]
}

response fields

auth_id string
The auth_id used to resolve the chain.
current_auth_id string
Tip of the chain (version with no successor).
versions array
Oldest → newest. Each entry has its own status, derivation_type, items, and has_successor.

POST /providers/enroll

Enroll a provider with a payer for electronic transactions. Returns 201 Created. Many payers require this before submit; the sandbox activates immediately. Poll with GET /enrollments/{id}.

bash
curl https://api.usegraft.dev/v1/providers/enroll \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data '{
    "npi": "1669613154",
    "payer_id": "graft_sandbox"
  }'
json
{
  "enrollment_id": "enr_3a9f1c8b2d7e",
  "npi": "1669613154",
  "organization_npi": null,
  "payer_id": "graft_sandbox",
  "status": "active",
  "created_at": "2026-09-01T14:00:00Z",
  "updated_at": "2026-09-01T14:00:00Z"
}
bash
curl https://api.usegraft.dev/v1/enrollments/enr_3a9f1c8b2d7e \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"

request fields

npi string, required
Rendering provider's 10-digit NPI (Luhn-validated).
organization_npi string, optional
Billing organization NPI when enrolling under a group.
payer_id string, required
Payer to enroll with, e.g. graft_sandbox.

response fields

enrollment_id string
Pass to GET /enrollments/{id}.
status "pending" | "active" | "rejected"
Sandbox enrollments activate immediately.

Webhooks

Register an HTTPS URL, then Graft POSTs signed JSON events to it. The signing secret (whsec_…) is returned once from POST /webhook_endpoints - store it. Omit subscribed_events to receive every type. Retries: 1m → 5m → 30m → 2h → 12h → failed. Deduplicate on Graft-Event-Id (retries reuse it).

bash
curl https://api.usegraft.dev/v1/webhook_endpoints \
  --request POST \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://yourapp.example.com/webhooks/graft",
    "subscribed_events": ["auth.approved", "auth.denied", "auth.modified", "auth.cancelled"]
  }'
json
{
  "endpoint_id": "whe_3a9f1c8b2d7e",
  "url": "https://yourapp.example.com/webhooks/graft",
  "subscribed_events": ["auth.approved", "auth.denied", "auth.modified", "auth.cancelled"],
  "secret": "whsec_2Yq8xxxxxxxxxxxxxxxx",
  "active": true,
  "created_at": "2026-09-01T14:00:00Z"
}
http
POST https://yourapp.example.com/webhooks/graft
Graft-Event-Id: evt_1a2b3c4d5e6f
Graft-Signature: t=1754140061,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_1a2b3c4d5e6f",
  "type": "auth.approved",
  "created_at": "2026-09-01T14:11:09Z",
  "data": {
    "auth_id": "auth_5b1e9d7c3a2f4008",
    "auth_number": "AUTH-2026-0098231",
    "tracking_id": "trk-a1b2c3d4e5f6"
  }
}
js
// Verify Graft-Signature before trusting a delivery.
// secret is the whsec_… value from POST /webhook_endpoints (shown once).
const parts = Object.fromEntries(
  signature.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) throw new Error("stale");
const expected = crypto
  .createHmac("sha256", secret)
  .update(`${t}.${rawBody}`)
  .digest("hex");
const ok = crypto.timingSafeEqual(
  Buffer.from(parts.v1),
  Buffer.from(expected),
);

events

auth.approved event
Decision made in the member's favor. Carries auth_number and tracking_id.
auth.denied event
Denied. Carries a structured denial_reason plus the payer's own codes.
auth.pended event
The payer needs more time. Carries an updated payer_decision_deadline. Fired before approve/deny in the sandbox.
auth.info_requested event
Payer wants more information. Webhook fires; GET /status collapses this to pended. No re-submit-for-info API yet.
auth.modified event
Payer authorized something different from what was requested (e.g. GRAFT-MODIFY-001). Carries auth_id, tracking_id, optional auth_number.
auth.cancelled event
Payer acknowledged a full-request cancel via POST /auths/{id}/cancel.
enrollment.active event
A provider enrollment reached active status.

Can't receive inbound POSTs? Poll GET /events instead - every event is recorded either way. Page with after / next_cursor until has_more is false.

bash
curl "https://api.usegraft.dev/v1/events?limit=50" \
  --header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"

Future events, such as auth.expiring ahead of a valid_until date, will carry suggested-next hints in the payload, so handlers should ignore unknown fields.

Sandbox

Simulated payer end to end (no real PHI). Use payer_id: "graft_sandbox". POST /check is CPT-driven: auth-required codes include 72148 / 70553 / 97110 / J1*; use 99213 for auth_required: false (ICD like Z00.00 does not decide). Magic member_id values steer submit outcomes. Claim update and cancel are accepted against sandbox auths.

GRAFT-APPROVE-NOW member_id
Emits auth.pended then auth.approved within seconds, with auth_number and ~90-day validity.
GRAFT-MODIFY-001 member_id
Returns modified immediately (payer-changed line items / addItem pairing).
GRAFT-PEND-N member_id
Pends, then approves after N seconds (N must be digits). Example: GRAFT-PEND-5.
GRAFT-DENY-001 member_id
Pends, then denies after ~60 seconds with a structured denial_reason.
GRAFT-INFO-001 member_id
Fires auth.info_requested. GET /status shows pended (public status collapses info_requested).
(anything else) member_id
Pends forever - useful for testing an open pending state.

Example: GRAFT-APPROVE-NOW through POST /submit approves within seconds (~90-day validity):

json
{
  "payer_id": "graft_sandbox",
  "member": {
    "first_name": "Ada",
    "last_name": "Lovelace",
    "member_id": "GRAFT-APPROVE-NOW",
    "dob": "1985-12-10"
  },
  "service": {
    "cpt": "72148",
    "icd10": ["M54.16"],
    "service_date": "2026-09-01"
  },
  "provider": { "npi": "1669613154" }
}
json
{
  "auth_id": "auth_5b1e9d7c3a2f4008",
  "status": "approved",
  "auth_number": "AUTH-2026-0098231",
  "tracking_id": "trk-a1b2c3d4e5f6",
  "payer_decision_deadline": "2026-09-15T14:11:09Z"
}

Inferno

ONC Inferno test payer - real suite HTTP for CRD, DTR, and PAS. Use payer_id: "inferno" with a sandbox key. Inferno is a public ONC tool; Graft exposes it as a normal sandbox payer. Production keys (grft_live_*) cannot call Inferno (403).

Suite click-path and session setup: /onboarding#inferno. Below: JWKS, check shape, and environment notes.

JWKS

Paste into each Inferno session's Client Registration. See also GET /inferno/jwks.json.

bash
curl https://api.usegraft.dev/v1/inferno/jwks.json
json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "graft-inferno",
      "use": "sig",
      "alg": "RS384",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

POST /check with payer_id=inferno

Session ids go in payer_context (JWT identifiers only; never interpolated into a URL). Then the same happy path: GET /requirements, POST /submit, GET /status/{id}, optional /auths/{id}/update|cancel.

bash
curl https://api.usegraft.dev/v1/check \
  --request POST \
  --header "Authorization: Bearer $GRAFT_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "payer_id": "inferno",
    "hook": "sign",
    "payer_context": {
      "crd_session_id": "Cd34Ef56",
      "pas_session_id": "Ab12Cd34",
      "dtr_session_id": "Ef56Gh78"
    },
    "member": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "member_id": "W123456789",
      "dob": "1985-12-10"
    },
    "service": {
      "cpt": "72148",
      "icd10": ["M54.5"],
      "service_date": "2026-09-01",
      "place_of_service": "11"
    },
    "provider": {
      "npi": "1669613154"
    }
  }'

Environment

On by default (GRAFT_INFERNO_ENABLED=true). Needs a signing key for JWKS. Sandbox keys only. Bad session ids return 422 with setup instructions. Group must be RUNNING (waiting) or Inferno returns Unable to find test run with identifier.

GET /inferno/jwks.json

Public JWKS for Inferno Client Registration. No API key. Returns Graft's RS384 public key for pasting into each Inferno session. Returns 503 if the signing key is not configured on the API.

bash
curl https://api.usegraft.dev/v1/inferno/jwks.json
json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "graft-inferno",
      "use": "sig",
      "alg": "RS384",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Create a test NPI

POST /check and POST /providers/enroll require a 10-digit NPI that passes the standard Luhn check (CMS prepends80840 to the first nine digits). Use any valid number in the sandbox. It does not need to match a real provider.