FlexpaFlexpa
Developer PortalGet a DemoTry it yourself

All docs

Overview

  • How it works
  • Requirements

Models and pipelines

  • Model behavior
  • Model configuration
  • GLiNER zero-shot NER

Deployment

  • CPU and GPU
  • Getting access
  • Getting the image
  • Performance

API reference

  • Extract a document
  • Health checks
  • Refusals

Limitations & disclosures

  • Intended use
  • Evaluation limits
  • AI transparency
Document parsing + NERPrivate alpha

Flexpa Extract

Clinical document parsing and NER.
OpenMed + optional GLiNER, run locally.

Deterministic parsing turns coded measurements into FHIR R4. OpenMed identifies disease and anatomy spans; optional GLiNER finds entities using labels you supply. Named entity recognition (NER) results are returned for review, with their source text.

Extract a document Get alpha access
Inside your environmentYour documents never leave your network

01 / Input

  • Lab PDF

    With a text layer

  • C-CDA XML

    Clinical document export

  • FHIR JSON

    Existing structured data

02 / Extract

Flexpa Extract

Deterministic parsing
Coded measurements → FHIR
OpenMed NER
Disease and anatomy spans
GLiNER · optional
Zero-shot NER with your labels
A container you run

03 / Output

FHIR R4 Bundle

Coded observations and units

Review report

Source locations and NER candidates

NER spans are returned for review. Models do not assign codes or clinical status. No outbound network calls.

ExploreModels and pipelinesDeploymentAPI referencePerformanceLimitations & disclosures

#Overview

#How it works

  1. Your service posts a document to the container on your own network
  2. Deterministic parsing reads what the document already codes: lab tables, vital signs, C-CDA entries
  3. OpenMed NER identifies disease and anatomy spans in narrative text. The optional GLiNER pipeline recognizes entities using labels you supply. Neither assigns a code or a clinical status
  4. You get a FHIR R4 Bundle, plus a report naming the source location behind every value and every value that was skipped

#Requirements

  • A host that can run a container. CPU is enough; GPU is a build option
  • The AWS account ID that will pull the image, which we name in the repository policy
  • Documents with a text layer. Scans and images are refused rather than guessed at, because there is no OCR lane

Alpha access, available to a small number of partners under agreement. These interfaces can change without notice and there is no uptime commitment. This page is unlisted; please do not share the link outside your organization.

Run it

docker run --rm -p 8000:8000 \
  flexpa/extract:alpha

Extract a document

curl -X POST http://localhost:8000/extract \
  -F file=@lab-report.pdf

#Models and pipelines

Everything the service infers comes from three open-weight checkpoints, all Apache-2.0, all carried in the image. Nothing is sent to a third-party model provider, and there is no large language model anywhere in the pipeline.

The OpenMed checkpoints come from the OpenMed suite (paper): BERT-family encoders domain-adapted on PubMed and clinical corpora, published as 2,266 repositories spanning 33M to 568M parameters.

#Model behavior

This is the part worth being skeptical about, so it is stated plainly.

  • They classify tokens. They do not generate text. Each model assigns a label to spans of the document. It can mis-label a span, and it cannot invent a finding that is not in the document.
  • They emit no codes and no clinical status. A model says "this span looks like a disease". It does not say which SNOMED or ICD code that is, whether it is active, or whether it belongs to this patient. Recognized entities are therefore not exported as Condition Resources. They are returned as review candidates.
  • Codes come from an explicit map, not a model. LOINC codes and UCUM units come from a reviewable terminology file. A value with no mapped code and no matching unit is reported with the reason rather than exported with a guessed code.
  • Zero-shot scores are uncalibrated. The zero-shot lane scores each candidate span against each label you supply. Those are match scores, not probabilities, they do not sum to one across labels, and they move when a label is reworded.
  • Published accuracy is thin. The OpenMed catalog carries a micro_f1 figure for 2 of its 2,266 entries. Model size buys latency, not verified accuracy. Evaluate on your own labelled set before relying on any of it.

Checkpoints

OpenMed/OpenMed-NER-DiseaseDetect-ElectraMed-33M33M

ELECTRA encoder, token classification. Disease lane over narrative text.

OpenMed/OpenMed-NER-AnatomyDetect-ElectraMed-33M33M

ELECTRA encoder, token classification. Anatomy lane over narrative text.

urchade/gliner_small-v2.1153M

GLiNER over a DeBERTa-v3-small backbone. Zero-shot lane, off by default.

Check what is actually loaded

curl -s http://localhost:8000/health

#Model configuration

Either fixed lane can be pointed at a larger checkpoint from the same family. A checkpoint swapped this way has to be present in the image, so change it at build time.

Span formation is configurable too: EXTRACT_AGGREGATION decides how subword tokens become spans, and the window and overlap settings decide how a long document is split before inference.

Environment

EXTRACT_MODEL_DISEASEOpenMed/...DiseaseDetect-ElectraMed-33M

Disease checkpoint. Must be present in the image.

EXTRACT_MODEL_ANATOMYOpenMed/...AnatomyDetect-ElectraMed-33M

Anatomy checkpoint. Must be present in the image.

EXTRACT_LANESdisease,anatomy

Lanes to run. Empty runs the deterministic path alone.

EXTRACT_THRESHOLD0.5

Minimum span confidence.

EXTRACT_AGGREGATIONaverage

How subword tokens become spans: average, first, max or simple.

EXTRACT_WINDOW_SIZE256

Token window a long document is split into.

EXTRACT_OVERLAP32

Token overlap between windows.

Run with a larger checkpoint

docker run --rm -p 8000:8000 \
  -e EXTRACT_MODEL_DISEASE=OpenMed/OpenMed-NER-DiseaseDetect-SuperClinical-434M \
  flexpa/extract:alpha

#GLiNER zero-shot NER

The fixed lanes answer the schema they were trained on and nothing else. The zero-shot lane answers labels you give it per request, which is how a caller reaches a field no fixed checkpoint emits.

It is off by default, because it adds roughly 580 MB and a second backbone to the image.

The default labels are the structural fields a FHIR export wants and no fixed checkpoint emits: specimen, collection method, performing organization, authorizing provider, result status, imaging modality and anatomical region. EXTRACT_ZEROSHOT_LABELS changes the default, EXTRACT_ZEROSHOT_THRESHOLD sets the minimum score, and GET /health reports whether the lane is enabled and whether its checkpoint is present.

Build with the zero-shot lane

docker build \
  --build-arg EXTRACT_EXTRAS=cpu,gliner \
  --build-arg EXTRACT_ZEROSHOT=1 \
  -t flexpa/extract:alpha-zeroshot .

Ask for your own labels

curl -X POST http://localhost:8000/extract \
  -F file=@lab-report.pdf \
  -F labels="specimen,result status"

#Deployment

A fully built image. There is no installer, no model download on first run, and no callback to Flexpa.

PropertyValue
Basepython:3.12-slim-trixie
Entrypointuvicorn as PID 1, so SIGTERM reaches it directly
Port8000
Usernon-root, uid 10001
NetworkHF_HUB_OFFLINE=1; no outbound calls at runtime
Size2.39 GB, of which a 1.27 GB Torch layer; about 580 MB more with the zero-shot lane
Authenticationnone — run it on a private network behind your own authentication

#CPU and GPU

The published image is CPU-only by design: Torch is installed from the CPU wheel index, so the image does not carry the CUDA runtime, which is most of Torch's size. CPU is the supported path and the one the performance figures below describe.

For GPU, build against a CUDA wheel index and select the device at runtime. Nothing in the pipeline imports a device-specific module directly, so one image definition serves both. GPU builds have not been benchmarked by us.

Built and verified on linux/arm64. An linux/amd64 build has not been produced: an emulated build was abandoned, and the deployment target should be built on a native amd64 runner. Nothing in the Dockerfile is architecture-specific, but that is an expectation, not a result.

Runtime selection

EXTRACT_DEVICEauto | cpu | cuda | mps

cuda needs a CUDA-built image and a GPU on the host.

EXTRACT_BACKENDauto | hf | onnx | mlx

mlx is Apple-only and is not in the container.

EXTRACT_ONNX_PROVIDERSprovider list

Execution providers, highest priority first. Empty lets the backend choose from the device.

Build for CUDA

docker build \
  --build-arg TORCH_INDEX=https://download.pytorch.org/whl/cu124 \
  -t flexpa/extract:alpha-cuda .

#Getting access

Alpha access is arranged directly. Contact your Flexpa representative, or email support@flexpa.com, with the AWS account ID that will pull the image, the documents you intend to process and the environment you intend to run the container in.

#Getting the image

Flexpa Extract is distributed from a private Amazon ECR repository. There is no public image and no shared registry password. Access is granted per AWS account: you tell us the account that will pull, and we name it in the repository policy. Nothing to rotate, and you can revoke by asking us to drop the statement.

What we need from you

  • The 12-digit AWS account ID that will pull the image
  • The region you will pull from, so we can replicate the repository if it is not already there

What we add on our side, a pull-only statement in the repository policy.

What you attach on your side. A repository policy grants your account; the principal doing the pull still needs its own IAM permissions. ecr:GetAuthorizationToken is account-level and takes no resource.

Running on ECS or EKS: the task execution role, not the task role, is the principal that pulls the image, so the IAM policy belongs there. If your environment cannot pull from ECR at all, tell us and we will arrange another channel.

ECR repository policy, Flexpa side

{
  "Sid": "AllowPartnerPull",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::<your-account-id>:root" },
  "Action": [
    "ecr:BatchGetImage",
    "ecr:GetDownloadUrlForLayer",
    "ecr:BatchCheckLayerAvailability"
  ]
}

IAM policy, your side

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "arn:aws:ecr:<region>:<flexpa-account-id>:repository/flexpa-extract"
    }
  ]
}

Authenticate and pull

aws ecr get-login-password --region <region> \
  | docker login --username AWS \
      --password-stdin <flexpa-account-id>.dkr.ecr.<region>.amazonaws.com

docker pull <flexpa-account-id>.dkr.ecr.<region>.amazonaws.com/flexpa-extract:alpha

#Performance

Measured on linux/arm64, CPU, with both fixed lanes loaded:

Offline readiness
2.5s
Both fixed lanes loaded
Warm extraction
1.38s
One two-page lab PDF
Documents per core
~50/ min
~1.2 s per six-page document

An INT8 ONNX CPU fast path exists upstream in OpenMed, with AVX-512, AVX2 and NEON dispatch. We will incorporate support for it soon.

#API reference

#Extract a document

One endpoint does the work. Post a document as multipart form data and read the Bundle out of the response.

InputWhat is extracted
Text-layer PDFLab tables to Observation with LOINC codes and UCUM units; vital signs to Observation
C-CDA XMLCoded observations, values, units and effective times, taken from the document's own codes
FHIR JSONReturned unchanged. Resources, identifiers, references and Bundle metadata are preserved

The format is detected from the content of the file, not from its name or the declared content type.

Parameters

filefileRequired

The document, as multipart form data. Up to 15 MB, 100 pages, and 120,000 characters once normalized.

bundle_onlyboolean

Query parameter. Returns the Bundle alone, without the review report.

labelsstring

Comma-separated labels for the zero-shot lane, when that lane is enabled. Falls back to the configured defaults.

Response

bundleBundle

A FHIR R4 collection Bundle. Entry identifiers are urn:uuid values derived from the document digest, so repeated processing keeps those identifiers stable. Bundle contents also depend on the resolved configuration and package versions.

documentobject

What was read: filename, kind, bytes, page count, SHA-256, character count and table count.

factsarray

Every value the deterministic path parsed, each with its code, unit, source unit, method, locator, page and bounding box, and whether it was eligible for export.

unmappedarray

Values that parsed but were not exported, each with the reason: no terminology mapping, or a unit that is missing or not convertible to UCUM.

provenancearray

One row per resource in the Bundle, naming the fact and the source location behind it.

entitiesarray

Spans the fixed model lanes recognized in narrative text. Review candidates: they are never exported as Resources.

zeroshotarray

Spans the zero-shot lane returned for your labels. Empty when the lane is off.

table_decisionsarray

Each table found in a PDF, and whether it was parsed or skipped, with the reason.

sections, negation_scopesarray

Detected narrative sections, and the negation cues resolved over them.

warningsarray

The limits that applied to this run, in plain language.

stagesarray

Per-stage timings: ingest, structure, model, context and fhir.

versions, configobject

The package versions and the resolved configuration that produced this output.

run_fingerprintstring

A digest over the document, the configuration and the versions. Two runs that share a fingerprint should produce the same Bundle.

#Health checks

Operations

GET /health

Resolved runtime, device, zero-shot state and package versions.

GET /livez

The process is up.

GET /readyz

A backend resolved and every configured lane loads. Use this one as the readiness probe.

Request

POST
/extract
curl -X POST http://localhost:8000/extract \
  -F file=@lab-report.pdf

Response

{
  "bundle": {
    "resourceType": "Bundle",
    "type": "collection",
    "id": "extract-69da7cfde2749f53",
    "entry": [
      {
        "fullUrl": "urn:uuid:34252bca-b614-515c-99a1-d04fe9d71a2f",
        "resource": {
          "resourceType": "Patient",
          "id": "source-patient"
        }
      },
      {
        "fullUrl": "urn:uuid:0974ca87-8aba-5158-9c34-7bbf62394684",
        "resource": {
          "resourceType": "Observation",
          "id": "obs-1",
          "status": "unknown",
          "code": {
            "coding": [
              {
                "code": "6690-2",
                "display": "Leukocytes [#/volume] in Blood by Automated count",
                "system": "http://loinc.org"
              }
            ],
            "text": "WBC"
          },
          "subject": {
            "reference": "urn:uuid:34252bca-b614-515c-99a1-d04fe9d71a2f"
          },
          "valueQuantity": {
            "value": 6.8,
            "unit": "10*3/uL",
            "code": "10*3/uL",
            "system": "http://unitsofmeasure.org"
          },
          "referenceRange": [
            {
              "low": {
                "value": 4.0,
                "unit": "10*3/uL"
              },
              "high": {
                "value": 11.0,
                "unit": "10*3/uL"
              }
            }
          ],
          "interpretation": [
            {
              "coding": [
                {
                  "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
                  "code": "N"
                }
              ]
            }
          ]
        }
      }
    ]
  },
  "document": {
    "filename": "lab_report_synthetic.pdf",
    "kind": "PDF",
    "bytes": 3843,
    "pages": 2,
    "sha256": "69da7cfde2749f53972eb418da99abb5a2affba7ad08c72e3323692dc1ee0a4f",
    "characters": 1145,
    "tables": 2
  },
  "facts": [
    {
      "label": "WBC",
      "code": {
        "code": "6690-2",
        "display": "Leukocytes [#/volume] in Blood by Automated count",
        "system": "http://loinc.org"
      },
      "value": 6.8,
      "unit": "10*3/uL",
      "source_unit": "10^3/uL",
      "value_kind": "quantity",
      "method": "table + curated map",
      "locator": "PDF page 2 / table 0 / WBC",
      "effective": null,
      "reference_range": {
        "low": 4.0,
        "high": 11.0,
        "low_inclusive": true,
        "high_inclusive": true
      },
      "flag": "normal",
      "exportable": true,
      "bbox": [
        129.6,
        152.79999999999995,
        482.4,
        332.8
      ],
      "page": 2,
      "id": "fact-1"
    }
  ],
  "unmapped": [],
  "provenance": [
    {
      "resource": "Patient/source-patient",
      "method": "Local subject placeholder; identity not extracted"
    },
    {
      "resource": "Observation/obs-1",
      "fact_index": 0,
      "locator": "PDF page 2 / table 0 / WBC",
      "method": "table + curated map",
      "source_time": null,
      "note": "No model-generated code or clinical status"
    }
  ],
  "table_decisions": [
    {
      "page": 2,
      "table": 0,
      "decision": "parse",
      "rows": 9,
      "reason": "Lab header matched"
    }
  ],
  "stages": [
    {
      "id": "ingest",
      "description": "PDF geometry, XML tree or FHIR resources",
      "ms": 321.635
    },
    {
      "id": "structure",
      "description": "Source codes and deterministic measurements",
      "ms": 388.643
    }
  ],
  "versions": {
    "openmed": "2.5.0",
    "pdfplumber": "0.11.10"
  },
  "config": {
    "backend": "auto",
    "device": "auto",
    "lanes": [],
    "threshold": 0.5,
    "aggregation": "average",
    "zeroshot_labels": [
      "specimen",
      "collection method",
      "performing organization",
      "authorizing provider",
      "result status",
      "imaging modality",
      "anatomical region"
    ]
  },
  "elapsed_ms": 719.2,
  "run_fingerprint": "7d5a3664d79260df2e75120cf88edb78d14824eefd3d156f1e8c880844b68888"
}

A real run against the two-page synthetic fixture. The Bundle's remaining 17 Observation entries, and the longer arrays, are trimmed here.

#Refusals

Every refusal is a FHIR OperationOutcome carrying a stable code, so a caller can branch without reading prose.

CodeStatusMeaning
no-text-layer422The PDF parsed and holds no extractable text. Almost always a scan
unsupported-input415Not a recognized document format
too-large413Over 15 MB, or over 120,000 characters once normalized
too-many-pages413Over 100 pages
malformed-input400Broken XML or JSON, or an XML DTD
empty-input400No bytes
model-unavailable503No backend resolved, or weights are missing

A scanned document is refused rather than answered with an empty Bundle. There is no OCR lane, so images are not supported.

#Limitations & disclosures

#Intended use

Flexpa Extract is intended for direct individual use for personal health: a person, or a service acting for that person, turning health documents they already hold into structured data they can read, search and keep. It is not part of Flexpa API.

Flexpa Extract is not a medical device. It is not intended to diagnose, treat, cure or prevent any disease, and its output is not medical advice. It restructures what a document already states. No output should be relied on for a clinical decision without review by a qualified professional, and no output should be treated as a substitute for the source document.

#Evaluation limits

We would rather you read this than discover it.

  • No evaluation on a labelled real-world corpus. The deterministic table path scores 18 of 18 analytes exactly on a synthetic fixture, on every test run. That is a fixture, not a benchmark.
  • No accuracy figures for the models themselves, as described above.
  • English only. Nothing has been measured on non-English documents.
  • Validation is offline and partial. Bundles are checked against a base R4 subset. Run the HL7 validator or Inferno before importing anywhere that matters.
  • Terminology is a starter map. The analyte-to-LOINC and unit-to-UCUM mapping is an explicit file drawn from a fixture and a small number of real documents. Check it against a terminology service before production use.

#AI transparency

Flexpa participates in the HL7 AI Transparency on FHIR project and implemented the data producer role at HL7 FHIR Connectathon 43 in September 2026. That work labels every resource an extraction authors with the AIAST security label, and emits a conforming Provenance naming the Device, the model checkpoints that ran and the input document.

Labeling is implemented in the reference implementation and is planned for this service. The container described on this page does not yet emit AIAST labels or AI Provenance.

Status TwitterGitHub

© 2026 Flexpa. All rights reserved.

FHIR® is the registered trademark of Health Level Seven International and its use does not constitute endorsement by HL7.