Flexpa
Developer PortalGet SandboxTry it yourself

All docs

Introduction

  • API Keys
  • Modes

Quickstart setup

  • Environment variables
  • Running
  • Your first consent

How it works

  • Consent flow
  • Retrieving records
  • Advanced
  • AI-powered assistant

Next steps

    Quickstart

    #Introduction

    Build a working Flexpa integration using the Quickstart app.

    You'll need API keys from the Developer Portal. Flexpa operates in Test and Live modes—we'll start in Test mode.

    #API Keys

    • Publishable Key

      A client-side key that is okay to share

    • Secret Key

      A server-side key that you must keep private

    The OAuth PKCE flow used in this quickstart only requires your Publishable Key.

    #Modes

    • Test

      Test credentials and synthetic data

    • Live

      Launch your integration with real patient data

    Flexpa Quickstart

    Try it yourself

    Go through a Patient Access API flow yourself! See exactly what patients will see

    Try it yourself →

    #Quickstart setup

    Make sure you have Node.js and npm installed before following along.

    Let's start by cloning the Quickstart repository from GitHub.

    The repository includes a .env.example file. You will copy this template file and use it as a starting point for your own .env file.

    Clone and copy template

    # Clone the repository
    git clone https://github.com/flexpa/quickstart.git
    cd quickstart
    
    # Copy the environment variables template
    cp .env.example .env
    

    #Environment variables

    Open .env in a text editor.

    Add your Publishable Key for Test mode from the Developer Portal → API Keys.

    The redirect URI is where users return after authorization. For local development, use http://localhost:3000/callback. Register this URI in the Developer Portal.

    You'll also need to generate a random secret for session signing. Your own app will have its own authentication.

    Update the .env file

    NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY=pk_test_...
    NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000/callback
    

    Generate a random session key

    # Generate a random secret key and add it to the .env file
    echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
    

    Optional: AI assistant (added later in guide)

    # Get your key from https://console.anthropic.com
    ANTHROPIC_API_KEY=sk-ant-...
    

    #Running

    The Flexpa Quickstart is a Node.js app built with Next.js.

    To get it started, install dependencies with npm install and start the app with npm run dev.

    The application will be running at http://localhost:3000.

    Quickstart landing page

    Install dependencies

    # Install dependencies
    npm install
    
    # Start the development server
    npm run dev
    
    # Go to http://localhost:3000
    

    #Your first consent

    A Patient Authorization represents consent to share healthcare records. Each authorization links a person, your application, and their healthcare records.

    Flexpa supports a 3-in-1 network for patient access to health insurance payers, healthcare providers, and national TEFCA networks.

    Now that you have Quickstart running, click Launch Consent to start the OAuth flow. You'll be redirected to Flexpa where you can select a health plan.

    Select Foo Medical and log in with the test credentials:

    Test credentials

    # Username
    patient10@flexpa.com
    
    # Password
    examplepatient
    

    Foo Medical is a fake sandbox we use to test the integration and it provides synthetic records. Learn more about test mode.

    After completing consent, you'll be redirected back to Quickstart. You have created your first Patient Authorization! You can now make API calls by using the buttons in Quickstart. In the next section, we'll explain what actually happened and how Quickstart works.

    Flexpa consent flow in action

    #How it works

    Quickstart demonstrates the OAuth 2.0 PKCE flow:

    1. Authorization - Redirect users to Flexpa for consent
    2. Code Exchange - Exchange the authorization code for an access token
    3. API Calls - Use the access token to query FHIR resources
    OAuth PKCE authorization flow

    #Consent flow

    The OAuth 2.0 PKCE (Proof Key for Code Exchange) flow securely authorizes your application without exposing secrets on the client.

    The Quickstart uses a Next.js server action to handle PKCE generation server-side:

    Building the authorization URL

    'use server'
    
    import FlexpaClient from '@flexpa/node-sdk'
    import { setCodeVerifier } from './session'
    
    export async function startOAuthFlow() {
      // Generate PKCE credentials on the server
      const codeVerifier = FlexpaClient.generateCodeVerifier()
      const codeChallenge = FlexpaClient.generateCodeChallenge(codeVerifier)
    
      // Store codeVerifier securely in server-side session
      await setCodeVerifier(codeVerifier)
    
      // Build the authorization URL
      const authUrl = FlexpaClient.buildAuthorizationUrl({
        publishableKey: process.env.NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY,
        redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI,
        codeChallenge,
        externalId: crypto.randomUUID(),
      })
    
      return authUrl
    }
    

    After a patient completes their consent on Flexpa, they are redirected back to your redirectUri with an authorization code parameter.

    Your callback handler should:

    1. Extract the code from the URL query parameters
    2. Retrieve the stored code_verifier
    3. Exchange them for an access token

    Exchanging the code for an access token

    import FlexpaClient from '@flexpa/node-sdk'
    
    // Get the authorization code from the callback URL
    const code = new URL(request.url).searchParams.get('code')
    
    // Retrieve the code verifier you stored earlier
    const codeVerifier = await getStoredCodeVerifier()
    
    // Exchange for access token
    const client = await FlexpaClient.fromAuthorizationCode(
      code,
      codeVerifier,
      process.env.NEXT_PUBLIC_REDIRECT_URI,
      process.env.NEXT_PUBLIC_FLEXPA_PUBLISHABLE_KEY
    )
    
    // Store the access token securely
    const accessToken = client.getAccessToken()
    

    The access_token grants access to a Patient Authorization's records. Store it securely—you'll need it for all FHIR API requests.

    #Retrieving records

    Now that we've covered the OAuth flow, let's explore how to make API calls.

    As an example, let's take a look at some of the requests made on the dashboard at http://localhost:3000/dashboard.

    The first request, $everything, is to /fhir/Patient/$PATIENT_ID/$everything - which retrieves all of the available data for a patient. The request uses the access_token like all consented records requests.

    In this code we see how the Quickstart makes this call using Node SDK but you can make it with any HTTP client:

    src/app/api/fhir/Patient/$everything/route.ts

    import { NextResponse } from 'next/server'
    import FlexpaClient from '@flexpa/node-sdk'
    import { getSession } from '@/lib/session';
    
    export async function GET() {
      const session = await getSession();
    
      if (!session?.accessToken) {
        return NextResponse.json(
          { error: 'Unauthorized' },
          { status: 401 }
        )
      }
    
      const client = FlexpaClient.fromBearerToken(session.accessToken);
      const everything = await client.$everything();
    
      return NextResponse.json(everything)
    }
    

    Flexpa API supports reading and searching a variety of FHIR Resources. The Quickstart demonstrates a few common requests:

    • Patient $everything
    • Patient Read
    • Explanation of Benefit Search
    • Coverage Search

    When making FHIR API requests, you may encounter 429 status codes while Flexpa syncs data from the payer. This happens during the initial sync period, which typically lasts less than 1 minute. Implement retry logic to handle these, or use the Node SDK which retries automatically.

    How to query for records

    #Advanced

    The FHIR responses above contain rich healthcare data, but they're deeply nested and complex to parse. For most applications, you want specific fields—not raw FHIR structures.

    The ViewDefinition/$run endpoint lets you define exactly what data you need and receive it as simple rows and columns. No FHIR parsing required.

    Here's an example that extracts a summary from ExplanationOfBenefit resources (claims data):

    src/app/api/fhir/claims-summary/route.ts

    import { NextResponse } from 'next/server'
    import { getSession } from '@/app/lib/session';
    
    const claimsView = {
      resourceType: 'ViewDefinition',
      name: 'claims_summary',
      status: 'active',
      resource: 'ExplanationOfBenefit',
      select: [{
        column: [
          { name: 'claim_id', path: 'id' },
          { name: 'type', path: "type.coding.where(system='http://terminology.hl7.org/CodeSystem/claim-type').code.first()" },
          { name: 'service_date', path: 'billablePeriod.start' },
          { name: 'provider', path: 'provider.display' },
          { name: 'total_billed', path: "total.where(category.coding.code='submitted').amount.value.first()" },
          { name: 'you_paid', path: "total.where(category.coding.code='memberliability').amount.value.first()" },
          { name: 'diagnosis_codes', path: 'diagnosis.diagnosisCodeableConcept.coding.code.distinct()', collection: true },
        ]
      }]
    };
    
    export async function GET() {
      const session = await getSession();
      if (!session?.accessToken) {
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
      }
    
      const response = await fetch('https://api.flexpa.com/fhir/ViewDefinition/$run', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${session.accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          resourceType: 'Parameters',
          parameter: [{ name: 'viewResource', resource: claimsView }]
        })
      });
    
      return NextResponse.json(await response.json());
    }
    

    Instead of parsing nested FHIR structures, you get clean, flat data:

    Response

    {
      "rows": [
        {
          "claim_id": "eob-12345",
          "type": "professional",
          "service_date": "2024-03-15",
          "provider": "Dr. Sarah Chen",
          "total_billed": 250.00,
          "you_paid": 35.00,
          "diagnosis_codes": ["Z00.00", "J06.9"]
        }
      ],
      "resourceCount": 1
    }
    

    Need help building ViewDefinitions? We're FHIR experts. Tell us what data your application needs, and we can help you craft a ViewDefinition that extracts exactly that. Reach out to your Flexpa contact or our support team.

    Learn more about ViewDefinitions in our parsing guide or see the API reference.

    #AI-powered assistant

    ViewDefinitions give you clean, structured data — the perfect input for an AI model. In this section, you'll build a simple chat assistant that answers questions about a patient's health records using the Vercel AI SDK.

    The pattern is straightforward: define a ToolLoopAgent with a tool that calls ViewDefinition/$run, then stream responses to a chat UI.

    Install dependencies

    npm install ai @ai-sdk/anthropic @ai-sdk/react zod streamdown
    

    We recommend using the test credentials (Foo Medical) from earlier in this guide when building this section. Sending real patient data to third-party LLMs like Anthropic or OpenAI requires a signed BAA with the model provider. Alternatively, de-identify data before sending it or use a self-hosted model.

    Add your Anthropic API key to .env if you haven't already — you can get one from the Anthropic Console.

    .env

    ANTHROPIC_API_KEY=sk-ant-...
    

    Prefer OpenAI? Install @ai-sdk/openai instead of @ai-sdk/anthropic, replace ANTHROPIC_API_KEY with OPENAI_API_KEY in your .env, and swap anthropic('claude-sonnet-4-6') for openai('gpt-5') in the server route. The AI SDK supports many providers with the same interface.

    Add Streamdown's dist path to your Tailwind content array so its classes are included:

    tailwind.config.ts (content array)

    content: [
      // ...your existing paths
      './node_modules/streamdown/dist/*.js',
    ]
    

    Import the stylesheet in your root layout:

    src/app/layout.tsx

    import 'streamdown/styles.css'
    

    Define a server route that creates an agent with a search_records tool. The tool calls ViewDefinition/$run using the session access token, and the agent turns the structured data into a plain-language response. In production, you'd add more tools for different record types — coverage, medications, lab results, and more.

    src/app/api/chat/route.ts

    import { ToolLoopAgent, tool, createAgentUIStreamResponse } from 'ai'
    import { anthropic } from '@ai-sdk/anthropic'
    import { z } from 'zod'
    import { getSession } from '@/lib/session'
    
    // Sample ViewDefinition — customize columns or create additional
    // ViewDefinitions for other resource types (Coverage, Observation, etc.)
    const claimsView = {
      resourceType: 'ViewDefinition',
      name: 'claims_summary',
      status: 'active',
      resource: 'ExplanationOfBenefit',
      select: [{
        column: [
          { name: 'claim_id', path: 'id' },
          { name: 'type', path: "type.coding.where(system='http://terminology.hl7.org/CodeSystem/claim-type').code.first()" },
          { name: 'service_date', path: 'billablePeriod.start' },
          { name: 'provider', path: 'provider.display' },
          { name: 'total_billed', path: "total.where(category.coding.code='submitted').amount.value.first()" },
          { name: 'you_paid', path: "total.where(category.coding.code='memberliability').amount.value.first()" },
          { name: 'diagnosis_codes', path: 'diagnosis.diagnosisCodeableConcept.coding.code.distinct()', collection: true },
          // Add more columns as needed — see the parsing guide for FHIRPath examples
        ]
      }]
    }
    
    async function runViewDefinition(
      accessToken: string,
      viewDefinition: Record<string, unknown>,
    ) {
      const response = await fetch(
        'https://api.flexpa.com/fhir/ViewDefinition/$run',
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            resourceType: 'Parameters',
            parameter: [{ name: 'viewResource', resource: viewDefinition }],
          }),
        },
      )
      if (!response.ok) {
        const errorText = await response.text()
        console.error(
          `ViewDefinition/$run failed (${response.status}):`,
          errorText,
        )
        return { error: `API returned ${response.status}`, details: errorText }
      }
      return response.json()
    }
    
    export async function POST(req: Request) {
      const session = await getSession()
      if (!session?.accessToken) {
        return new Response('Unauthorized', { status: 401 })
      }
    
      const { messages } = await req.json()
    
      const agent = new ToolLoopAgent({
        model: anthropic('claude-sonnet-4-6'),
        instructions: 'You are a helpful health records assistant. Answer questions about the patient\'s health records. Be concise and translate medical jargon into plain language.',
        tools: {
          search_records: tool({
            description: 'Search the patient\'s health records',
            inputSchema: z.object({}),
            execute: async () => runViewDefinition(session.accessToken, claimsView),
          }),
        },
      })
    
      return createAgentUIStreamResponse({
        agent,
        uiMessages: messages,
      })
    }
    

    Create a client component that connects to the route with useChat. This renders streamed responses with Streamdown, shows tool state so users see what the agent is doing, and auto-scrolls to keep the latest messages visible.

    src/app/chat/page.tsx

    'use client'
    
    import { useChat } from '@ai-sdk/react'
    import { DefaultChatTransport } from 'ai'
    import { useEffect, useRef, useState } from 'react'
    import { Streamdown } from 'streamdown'
    
    export default function ChatPage() {
      const { messages, sendMessage, status } = useChat({
        transport: new DefaultChatTransport({ api: '/api/chat' }),
      })
      const [input, setInput] = useState('')
      const bottomRef = useRef<HTMLDivElement>(null)
    
      // Auto-scroll to bottom on new messages and streaming chunks
      useEffect(() => {
        bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
      }, [messages])
    
      return (
        <div>
          <div style={{ overflow: 'auto' }}>
            {messages.map((m) => (
              <div key={m.id}>
                <strong>{m.role === 'user' ? 'You' : 'Assistant'}:</strong>
                {m.parts.map((part, i) =>
                  part.type === 'text' ? (
                    m.role === 'assistant' ? (
                      <Streamdown key={`${m.id}-${i}`} isAnimating={status === 'streaming'}>
                        {part.text}
                      </Streamdown>
                    ) : (
                      <span key={`${m.id}-${i}`}>{part.text}</span>
                    )
                  ) : part.type.startsWith('tool-') && 'state' in part ? (
                    <p key={`${m.id}-${i}`} style={{ color: 'gray', fontSize: '0.875rem' }}>
                      {part.state === 'output-available'
                        ? '✓ Fetched health records'
                        : 'Fetching health records...'}
                    </p>
                  ) : null
                )}
              </div>
            ))}
            <div ref={bottomRef} />
          </div>
          <form onSubmit={(e) => {
            e.preventDefault()
            if (input.trim()) {
              sendMessage({ text: input })
              setInput('')
            }
          }}>
            <input
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Ask about your health records..."
            />
            <button type="submit" disabled={status !== 'ready'}>Send</button>
          </form>
        </div>
      )
    }
    

    Try asking questions like:

    • "What were my most recent claims?"
    • "How much have I paid out of pocket?"
    • "Summarize my health records"

    Want a zero-code approach? The Flexpa MCP server lets you connect health data to Claude, ChatGPT, and other AI assistants without writing any code.

    Health Records Agent chat interface

    #Next steps

    Want to see an example of how to integrate Flexpa into an existing application? Check out our integration tutorial!

    Congratulations, you have completed the Flexpa Quickstart! There are a few directions you can go in now:

    • Learn more about ViewDefinitions and FHIRPath expressions
    • Build an AI-powered healthcare assistant with the Flexpa MCP server
    • Try accessing your own health claims records on MyFlexpa
    • Learn how to add Medplum or AWS HealthLake to the Quickstart
    • Schedule a demo to discuss your integration needs
    Status TwitterGitHub

    © 2026 Flexpa. All rights reserved.