Sandbox is live. Developer onboarding → · Sign in →
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.
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"
}
}'{
"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.
curl https://api.usegraft.dev/v1/check \
--request POST \
--header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx" \
--header "Content-Type: application/json" \
--data @check.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.
curl https://api.usegraft.dev/v1/status/auth_5b1e9d7c3a2f4008 \
--header "Authorization: Bearer grft_live_9k2m47xq..."{
"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).
curl https://api.usegraft.dev/v1/auth/login \
--request POST \
--header "Content-Type: application/json" \
--data '{"email":"you@clinic.com"}'{
"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.
curl https://api.usegraft.dev/v1/coverage[
{
"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.
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"
}
}'{
"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.
curl "https://api.usegraft.dev/v1/requirements?check_id=chk_9f3c2a1b8d7e4f60" \
--header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"{
"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).
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"
}'{
"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.
curl https://api.usegraft.dev/v1/status/auth_5b1e9d7c3a2f4008 \
--header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"{
"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.
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"
}
]
}'{
"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.
curl https://api.usegraft.dev/v1/auths/auth_5b1e9d7c3a2f4008/cancel \
--request POST \
--header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"{
"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.
curl https://api.usegraft.dev/v1/auths/auth_5b1e9d7c3a2f4008/chain \
--header "Authorization: Bearer grft_sandbox_xxxxxxxxxxxxxxxxxxxxxxxx"{
"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}.
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"
}'{
"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"
}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).
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"]
}'{
"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"
}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"
}
}// 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.
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):
{
"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" }
}{
"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.
curl https://api.usegraft.dev/v1/inferno/jwks.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.
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.
curl https://api.usegraft.dev/v1/inferno/jwks.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.