Metadata-Version: 2.4
Name: x12sdk
Version: 2.1.0
Summary: Typed Pydantic models and a streaming SDK/CLI for HIPAA ASC X12 5010 health care transactions (837P, 837I, 835, 834, 270, 271, 276, 277).
Author: owgreen-dev
License: Apache-2.0
Project-URL: Homepage, https://github.com/owgreen-dev/x12sdk
Project-URL: Repository, https://github.com/owgreen-dev/x12sdk
Project-URL: Issues, https://github.com/owgreen-dev/x12sdk/issues
Project-URL: Changelog, https://github.com/owgreen-dev/x12sdk/blob/main/CHANGELOG.md
Project-URL: Upstream (LinuxForHealth x12), https://github.com/LinuxForHealth/x12
Keywords: x12,edi,hipaa,837,835,834,270,271,276,277,claims,pydantic
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Intended Audience :: Healthcare Industry
Classifier: Intended Audience :: Developers
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: pydantic<3,>=2
Requires-Dist: pydantic-settings>=2
Requires-Dist: python-dotenv>=0.19.0
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Provides-Extra: dev
Requires-Dist: pandas>=2.0; extra == "dev"
Requires-Dist: pytest>=7.1; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: pre-commit>=2.14; extra == "dev"
Requires-Dist: hypothesis>=6; extra == "dev"
Dynamic: license-file

# x12sdk

Typed [Pydantic v2](https://docs.pydantic.dev/) models and a streaming SDK/CLI
for HIPAA ASC X12 5010 health care transactions.

![License](https://img.shields.io/github/license/owgreen-dev/x12sdk)
![CI](https://github.com/owgreen-dev/x12sdk/actions/workflows/continuous-integration.yml/badge.svg)
![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)

> x12sdk is the maintained continuation of
> [LinuxForHealth x12](https://github.com/LinuxForHealth/x12), which stopped at
> 0.57.0 in June 2022. It runs on **Pydantic v2 and Python 3.10–3.13**.

Supported transaction sets:

| Set | Implementation | What it is |
|---|---|---|
| 837P | 005010X222A2 | Professional claim |
| 837I | 005010X223A3 | Institutional claim |
| 835 | 005010X221A1 | Claim payment / remittance advice |
| 834 | 005010X220A1 | Benefit enrollment and maintenance |
| 270 / 271 | 005010X279A1 | Eligibility inquiry / response |
| 276 / 277 | 005010X212 | Claim status inquiry / response |

Every transaction is parsed into a validated Pydantic model and can be
serialized back to X12; the test suite asserts that round trip reproduces
each sample file byte for byte.

## Install

```shell
pip install x12sdk
```

From source:

```shell
git clone https://github.com/owgreen-dev/x12sdk
cd x12sdk
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install -e .
```

## SDK

The `x12sdk.io` module streams either raw segments or validated transaction
models from a file.

Stream segments (each segment becomes its name plus a list of fields):

```python
from x12sdk.io import X12SegmentReader

with X12SegmentReader("/home/edi/270.x12") as r:
    for segment_name, segment_fields in r.segments():
        print(segment_name, segment_fields)
```

Stream models (the payload is validated; one model per transaction set):

```python
from x12sdk.io import X12ModelReader

with X12ModelReader("/home/edi/270.x12") as r:
    for model in r.models():
        print(model.header)   # common attributes: header, footer
        print(model.footer)
        model.x12()           # serialize back to X12
```

## Reaching the claims

The models mirror the X12 loop hierarchy, so a claim is several levels down.
On an 837 it is down one of *two* paths, because a claim sits under the
subscriber when the patient is the subscriber and under a dependent when they
are not:

```
loop_2000a[i].loop_2000b[j].loop_2300[k]                  patient = subscriber
loop_2000a[i].loop_2000b[j].loop_2000c[l].loop_2300[k]    patient = dependent
```

Both are ordinary. Code written against one runs happily on a file that uses
the other and reports no claims at all, so `claims()` walks both and yields a
flat record. It is a generator, so a large file is never materialized.

```python
for claim in model.claims():
    print(claim.patient_control_number, claim.charge, claim.patient_name)
```

Each record carries the claim plus the context you would otherwise re-derive:
`billing_provider`, `subscriber`, `payer`, `patient`, `is_dependent` and
`relationship`. `patient` already points at whoever was treated, so you never
need to know which branch the claim came from. `subscribers()` yields the
subscribers and their dependents.

`claims()` on an 835 yields the claim payments, each with `charge`, `paid`,
`status`, `adjustments`, `service_lines` and the LX `header_number`.

The eligibility and claim status pairs branch the same way, so they have
accessors too. `members()` on a 270 or 271 yields whoever the transaction is
about, with their benefits; `claims()` on a 276 or 277 yields the tracked
claims. Both hide the subscriber and dependent branch the same way `claims()`
does on an 837:

```python
for member in eligibility.members():
    print(member.name, member.is_dependent, member.service_type_codes)

for claim in status.claims():
    print(claim.trace_number, claim.charge, claim.paid)
```

A tracked claim reads its charge from AMT on an inquiry and from STC on a
response, so the caller does not have to know which it is holding.

## CLI

```shell
x12sdk --help
usage: x12sdk [-h] [-s | -m] [-x] [-p] [-d] file

The x12sdk CLI parses and validates X12 messages.
Messages are returned in JSON format in either a segment or transactional format.

positional arguments:
  file              The path to a ASC X12 file

options:
  -h, --help        show this help message and exit
  -s, --segment     Returns X12 segments
  -m, --model       Returns X12 models
  -x, --exclude     Exclude fields set to None in model output
  -p, --pretty      Pretty print output
  -d, --delimiters  Include X12 delimiters in output (model mode only)
```

```shell
x12sdk -s -p demo-file/demo.270   # segments
x12sdk -m -p demo-file/demo.270   # models
```

## Writing X12

The transaction models cover ST through SE. `write_transactions` adds the
interchange and functional group envelopes and keeps the control numbers
consistent, so you get a file a trading partner would accept.

```python
from x12sdk.io import X12ModelReader, write_transactions

with X12ModelReader("in.835") as reader:
    transactions = list(reader.models())

out = write_transactions(transactions, sender_id="SENDERID", receiver_id="RECEIVERID")
```

## Generating synthetic files

Real claims and remittances contain PHI, and there is no public X12 corpus to
test against. `x12sdk.generate` builds valid transactions from the same models
the parser produces, so your test data is guaranteed synthetic.

```python
from x12sdk.generate import generate_835

remittance = generate_835(seed=7, claims=25)   # a complete file, envelope included
```

The same seed always produces the same bytes, and generation never touches the
global random state, so it is safe inside someone else's test suite.

To build a specific scenario, describe it:

```python
from x12sdk.generate import ClaimSpec, ServiceLineSpec, denial, generate_835

spec = [
    ClaimSpec(
        charge="900.00",
        lines=[ServiceLineSpec(charge="900.00", procedure="99214",
                               adjustments=[denial("CO", "97", "300.00")])],
    )
]
remittance = generate_835(seed=1, claims=spec, payer_name="EXAMPLE HEALTH PLAN")
```

A claim's payment is derived as charge minus adjustments, so a specification
that would break the 835 balance rule cannot be written down.

Claim submissions work the same way:

```python
from x12sdk.generate import generate_837p

submission = generate_837p(seed=7, claims=25)
```

In an 837 a claim sits under the subscriber when the patient is the
subscriber, and under a dependent when they are not. Code that walks the
hierarchy often handles only the first, so generated files contain both by
default. Set `dependent_rate` to choose the mix, or pass a `SubmissionSpec`
to place each claim yourself:

```python
from x12sdk.generate import (
    ClaimSpec, PatientSpec, ServiceLineSpec, SubmissionSpec, generate_837p
)

spec = SubmissionSpec(
    patients=[
        PatientSpec(
            claims=[ClaimSpec(charge="450.00",
                              lines=[ServiceLineSpec(charge="450.00",
                                                     procedure="99214")])],
            dependent=True,
            relationship="19",   # child
        )
    ]
)
submission = generate_837p(seed=1, claims=spec)
```

## Denial analytics

An 835 tells you what a payer did to a claim, but in a shape built for
transmission: adjustments nested at claim and service line level, up to six
reason/amount pairs per CAS segment, remark codes in a different segment
again. `x12sdk.denials` flattens that to one record per reason code and
aggregates it the way a recovery or program integrity analyst asks the
question.

```python
from x12sdk.io import X12ModelReader
from x12sdk.denials import denial_summary, iter_adjustments

with X12ModelReader("remit.835") as reader:
    for transaction in reader.models():
        rows = list(iter_adjustments(transaction))
        for row in denial_summary(rows):
            print(row.payer_name, row.group_code, row.reason_code,
                  row.category, row.claim_count, row.total_amount)
```

`denial_summary` counts payer-side groups (`CO`, `OA`, `PI`) by default and
leaves out patient cost share (`PR`), because a deductible is not a denial;
pass `include_patient_responsibility=True` to keep it. Amounts stay `Decimal`,
so totals are exact. `claim_count` counts distinct claims, so a reason hitting
three lines of one claim counts once.

For DataFrame work, install the extra and use `to_dataframe`:

```shell
pip install 'x12sdk[pandas]'
```

### Code lists

CARC and RARC **descriptions** are published by X12 and the Washington
Publishing Company and are licensed separately, so **x12sdk ships none of that
text**. What it ships is `categorize()`, x12sdk's own grouping of reason codes
into analysis categories such as `eligibility`, `authorization`, `duplicate`
and `timely_filing`, with anything unmapped resolving to `other`.

If you need the official wording, obtain the list from
[x12.org/codes](https://x12.org/codes) under whatever licence applies to you
and load it yourself:

```python
from x12sdk.denials import describe, load_code_descriptions

descriptions = load_code_descriptions("carc.csv")   # your file, not ours
for row in describe(denial_summary(rows), descriptions):
    print(row["reason_code"], row["description"], row["total_amount"])
```

Eligibility works the same way, and a 270 and the 271 answering it can be
generated as a matched pair from one specification. One inquiry may ask about
several service types, since EQ repeats:

```python
from x12sdk.generate import BenefitSpec, EligibilitySpec, MemberSpec
from x12sdk.generate import generate_270, generate_271

spec = EligibilitySpec(
    members=[MemberSpec(benefits=(BenefitSpec(service_type="35"),), dependent=True)]
)
inquiry = generate_270(seed=1, members=spec)
response = generate_271(seed=1, members=spec)
```

The eligibility transactions carry the same subscriber and dependent branch as
the 837, so generated files contain both by default here too.

Claim status works the same way. The 276 states what was billed, the 277
answers with an STC status, and on one seed the pair describes the same people
and the same claims:

```python
from x12sdk.generate import generate_276, generate_277

inquiry = generate_276(seed=9, patients=8)
response = generate_277(seed=9, patients=8)
```

Enrollment has no hierarchy to branch on. A dependent on an 834 is a separate
member record told apart by INS01 and INS02, not a loop nested under the
subscriber, and both kinds appear by default:

```python
from x12sdk.generate import generate_834

roster = generate_834(seed=3, enrollees=20)
```

All eight supported transaction sets can be generated. The institutional
claim takes the same specification as the professional one:

```python
from x12sdk.generate import generate_837i

submission = generate_837i(seed=7, claims=25)
```

## Migrating from `linuxforhealth-x12`

| before | after |
|---|---|
| `pip install linuxforhealth-x12` | `pip install x12sdk` |
| `from linuxforhealth.x12.io import X12ModelReader` | `from x12sdk.io import X12ModelReader` |
| `lfhx12 -m -p file.x12` | `x12sdk -m -p file.x12` |
| `lfhx12-api` (FastAPI endpoint) | removed; wrap the SDK in your own service |

See [CHANGELOG.md](CHANGELOG.md) for everything that changed.

## Development

```shell
pip install -e ".[dev]"
ruff check src
pytest --cov
```

`src/tests/audit/` is a suite of generic detectors, one per bug class that has
shipped here, run over every transaction set on every commit; it is described,
limits included, in [repo-docs/AUDIT.md](repo-docs/AUDIT.md).

Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md) (Apache-2.0,
DCO sign-off, no copyrighted standards text, no real PHI). To add a
transaction set, see [repo-docs/NEW_TRANSACTION.md](repo-docs/NEW_TRANSACTION.md);
the design is described in [repo-docs/DESIGN.md](repo-docs/DESIGN.md).

## Provenance and related work

x12sdk is a fork of **[LinuxForHealth x12](https://github.com/LinuxForHealth/x12)**
by Dixon Whitmire and the LinuxForHealth contributors (IBM), released under
the Apache License 2.0. The models, parser, readers, and test corpus
originate there; x12sdk exists to keep that work usable on current Python
and Pydantic. The original LICENSE is retained, and attribution and
trademark notes are in [NOTICE](NOTICE) and [TRADEMARK.md](TRADEMARK.md).
x12sdk is not affiliated with or endorsed by IBM, LinuxForHealth, or the
Linux Foundation.

- **[MdClarity/x12](https://github.com/MdClarity/x12)** — an independent
  fork by MD Clarity (Cary Lee) that completed a Pydantic v2 migration and
  added type checking and fuzzing in 2026. x12sdk's port is written
  separately from the 2022 upstream; their work is acknowledged here and
  their fixes are welcome upstream in x12sdk.
- **[pyx12](https://github.com/azoner/pyx12)** — the long-standing Python X12
  validator/converter (XML/dict output, map-driven). Choose pyx12 for
  validation against X12 maps; choose x12sdk for typed Python models.
- **[edi-835-parser](https://github.com/keiron-stoddart/edi-835-parser)** —
  a popular 835-only parser with pandas output.

## License

Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
