endear / oe-module-endear-dscsa
Endear DSCSA Module — DSCSA serialized inventory, inbound scan/verify, and dispense scan-out for OpenEMR. Connects to Endear's EPCIS/AS2 service.
Package info
github.com/endearai/oe-module-endear-dscsa
Type:openemr-module
pkg:composer/endear/oe-module-endear-dscsa
Requires
- php: >=8.1
- firebase/php-jwt: ^7.1
- openemr/oe-module-installer-plugin: ^0.1.0
README
DSCSA serialized-inventory integration for OpenEMR 8.0.1. Pulls serialized
units from the Endear DSCSA Module API (dispense.endear.com), files them against
OpenEMR's in-house pharmacy inventory, and provides inbound scan/verify and
dispense scan-out with point-of-care verification.
This module holds only the operational subset needed to dispense. Your Postgres (AWS RDS) behind the API remains the DSCSA compliance system of record.
License
The code in this repository is licensed under GPL-3.0-or-later (see
LICENSE). There are no license keys, obfuscated code, or disabled features
hidden in this module — everything here is inspectable, forkable, and free to
modify under GPL terms.
This module is a connector, not the service it connects to. It talks to
Endear's hosted AS2/EPCIS compliance service (dispense.endear.com) over the
documented REST API in BILLING-API.md and the other *-API.md files. That
hosted service is not part of this repository, is not open source, and is not
free.
| Free / GPL (this repo) | The OpenEMR-side scan-in, scan-out, inventory, DEA/PDMP, and billing UI, plus the API client that talks to Endear's service. |
| Paid, separate from this repo | The Endear DSCSA+ hosted service — EPCIS archive, AS2 transport, entitlement/billing. Requires an active subscription: see endear.com/pricing or the Billing tab inside the module. |
Unlike some ePrescribing modules in this ecosystem, there is currently no free tier of the hosted service — using this module for DSCSA/DSCSA+ inventory sync requires a paid Endear subscription. Reading, forking, or building your own connector against the documented API does not.
This module is developed and maintained by Endear Inc. and is not officially affiliated with, or endorsed by, the OpenEMR Foundation.
DSCSA vs. DSCSA+
One module, one package name — the deeper controlled-substance features sit behind an Endear subscription tier, not a separate install:
| DSCSA (base) | DSCSA+ | |
|---|---|---|
| Serialized inventory (scan-in/out, verify, dispense) | ✔ | ✔ |
| GTIN↔NDC mapping, sync, returns, investigations | ✔ | ✔ |
DEA schedule delineation (dea_schedule on every serial) |
✔ | ✔ |
Named 21 CFR 1304.04 dispensing record (endear_dea_dispense_record) |
— | ✔ |
| State controlled-substance add-ons (e.g. MA PDMP/MassPAT) | — | ✔ |
CSA (Endear's SharePoint/SPFx compliance app) is not bundled here — it's a
separate product a practice purchases on its own. What this module provides
either way is the correlation_token: a random, information-free id persisted
on every controlled-substance dispense, safe to hand to CSA or any external
compliance system without exposing patient identity. DSCSA+ is what makes that
token worth having — it's the tier that actually populates a named record
behind it (patient/prescriber/PDMP detail, all kept local to OpenEMR).
Plan-tier enforcement lives server-side (GET /billing/plans,
GET /billing/subscription, see Subscription & billing below) — this
client stays the same GPL code regardless of tier; what dispense-api accepts
back is what actually gates it.
State coverage today: Massachusetts only.
PdmpPolicyand the PDMP Reports screen are hardcoded to MA's schedule list and MassPAT/PMP AWARxE terminology. A practice in another state can still use DSCSA+ for DEA recordkeeping, but the PDMP draft feature isn't yet state-configurable — treat it as "MassPAT support," not generic PDMP support, until that's split out.
How it fits together
Trading partners ──AS2──▶ Endear API (dispense.endear.com) ──REST──▶ this module ──▶ OpenEMR inventory
│
scan-in (+on_hand) ◀───┤
scan-out (−on_hand) ────┘──POST /dispense──▶ Endear
The module never alters core OpenEMR tables. It adds tables and links to
drugs / drug_inventory:
DSCSA (base):
endear_dispense_inventory_serial— one row per serialized unit (the serial layer core OpenEMR lacks)endear_dispense_gtin_ndc_map— GTIN → NDC →drugs.drug_idcrosswalkendear_dispense_sync_log— audit of each pull cycleendear_dispense_external,endear_dispense_removal,endear_dispense_investigation— 3PL dispensing, returns/destruction, and investigation workflows
DSCSA+:
endear_dea_dispense_record— the named 21 CFR 1304.04 dispensing record (patient/prescriber/schedule). Never transmitted; only itscorrelation_tokenis shared externally.endear_pdmp_report— auto-generated PDMP draft for a take-home dispense of a reportable schedule; holds the clean summary staff key into the state portal (MassPAT/PMP AWARxE today), plus who/when/reference once marked submitted.
Install
Option A — Composer (recommended once published on Packagist):
composer require endear/oe-module-endear-dscsa
The openemr/oe-module-installer-plugin dependency places it at
interface/modules/custom_modules/oe-module-endear-dscsa/. Then run
composer dump-autoload from your OpenEMR root so the namespace registers.
Option B — Manual (today's actual path — not yet on Packagist): download
the release zip and extract it into
<openemr>/interface/modules/custom_modules/oe-module-endear-dscsa/, then run
the bundled fix-up script once, before Register/Install/Enable:
cd <openemr>/interface/modules/custom_modules/oe-module-endear-dscsa/ sh install.sh
Run this as root (or whoever's shell account owns the files you just
copied in) — not as an OpenEMR login, and never as the web server's own
account (apache/www-data). This is an OS-level shell command on the
server/container, unrelated to logging into OpenEMR itself; in a typical
Docker deployment that means docker exec -it <container> sh as root, cd
into the module directory, then run the script. It specifically can't run as
the web server's account, since that account not yet being able to read
these files is the exact problem it fixes — only the file owner or root can
grant that. Once this script has run, Register/Install/Enable in the OpenEMR
web UI is the normal admin-user step, same as any other module.
install.sh does two things, both of which matter regardless of which one
bites you first:
- Permissions. Whatever copied the folder in (
docker cp, a zip extract,scp) almost always leaves it owned byrootor your own host user, not the user the web server runs as. OpenEMR's module manager checksis_readable()on the module'sopenemr.bootstrap.phpon every request; if the web server user can't read it, OpenEMR retries 3 times, logsCustom module bootstrap file ... is not readable, then silently force-disables the module — Enable appears to work for a moment and then reverts, with no error shown in the UI. The script fixes this withchmod -R a+rX, which works regardless of which account the web server actually runs as (apache,www-data, a php-fpm pool user — it varies by image), so there's nothing to detect. - Dependencies, but only if
vendor/isn't already there. Release zips ship it pre-bundled, so this is normally a no-op; it only runscomposer install --no-devif you're working from a source checkout instead. The module's own fallback autoloader only covers its own classes, not the one real third-party dependency (firebase/php-jwt, used to verify signed DSCSA+ entitlement tokens) — skipping this whenvendor/really is missing doesn't break Enable, it fails later and silently, the first time a page callsEntitlementPolicy::hasDscsaPlus().
Neither step can be automated further than "run one script" — a Unix process
can only chmod/chown files it already owns (or run as root), and the web
server's own account is neither, since it didn't create these files. Some
privileged step (root, or whoever owns the files) has to run once, no matter
what. If you'd rather chown to a specific user than open permissions to
everyone, that works too, in place of the script's chmod:
ps aux | grep -E 'apache|httpd|php-fpm' # find the user Apache/PHP runs as chown -R apache:apache <openemr>/interface/modules/custom_modules/oe-module-endear-dscsa
Then, either way, in OpenEMR: Modules → Manage Modules → Unregistered,
Register "Endear DSCSA Module", then Install, then Enable. Install
creates the schema and the background-service row from table.sql (idempotent).
If your build doesn't auto-run it on enable:
mysql -u <user> -p <openemr_db> < table.sql
Troubleshooting
- Enable reverts to Inactive with no visible error → ownership issue above.
Check the web server's error log for
oe-module-endear-dscsa— a real, literal message (... is not readable. Retry Count: N, thenSystem will force disable module.) confirms it. - Any module page 500s immediately after Enable → check the same error
log for the actual PHP exception. If it's
ArgumentCountErroronCsrfUtils::collectCsrfToken()/verifyCsrfToken(), you're on an older copy predating the OpenEMR 8 CSRF API fix in this module (fixed 07292026) — pull the current release. composer installfails citing a security advisory onfirebase/php-jwt→ shouldn't happen on a current release (composer.jsonrequires^7.1, clear of the CVE-2025-45769 advisory affecting<7.0.0). If you see this, your copy'scomposer.jsonis stale; re-download the current release.
Publishing to the OpenEMR community
- Push to a public GitHub repo; tag a release (e.g.
v0.2.0) matching thecomposer.jsonversion. - Submit the repo to packagist.org so users can
composer requireit. Enable the GitHub→Packagist webhook for auto-updates on new tags. - Announce on the OpenEMR community forum (community.open-emr.org).
Note: third-party modules can't appear by default on the Manage Modules list the way Weno/Fax do — that requires inclusion in OpenEMR core. Packagist + composer is the distribution path.
Configure
Administration → Globals → Connectors → Endear DSCSA Module
| Setting | Notes |
|---|---|
| Environment | prd or stg — must match the API key |
| API base URL | blank = default (https://dispense.endear.com/api/v1 or …stg…) |
| Practice API key | the X-Practice-API-Key issued by Endear (stored encrypted) |
| OpenEMR instance label | sent as openemr_instance on dispense events |
| Pull look-back (days) | window for /inventory/pending (default 30) |
| Transmit patient MRN | OFF by default — see PHI note below |
| Reconcile dispense against native drug sales | ON by default. Lets the standalone Dispense screen match a scanned serial to a recent native drug_sales row so the dispense carries patient/Rx linkage — read-only, writes nothing to native tables. One of two ways (see "Patient/Rx linkage" below) the standalone screen can supply the linked patient DSCSA+ requires for a take-home or outpatient-pickup dispense of a PDMP-reportable schedule. |
The base URL + key + environment are a matched set: a staging key against the production URL returns 401 by design.
Patient/Rx linkage on the Dispense screen
This fork does not depend on any core-file patch. The standalone Dispense
(Scan Out) screen has its own patient/prescription picker: search for a
patient by name, then pick one of their prescriptions for the scanned drug
— this ties the dispense to a real patient/Rx/prescriber without any
pre-existing native drug_sales row. It coexists with the
reconcile-against-recent-native-sale picker (endear_dispense_reconcile,
ON by default); either one, both, or neither (typed Rx#/encounter only)
all work. For DSCSA+ practices, at least one of these two linkage paths is
required for any take-home or outpatient-pickup dispense of a
PDMP-reportable schedule — that's the one case the code hard-blocks
without a linked patient.
(07282026: this same picker also exists in Endear's internal OpenEHR-specific module, as an alternative alongside its own core-file-patch reconcile path — see that module's README for how the two installs differ.)
Permissions, menu, cron
- ACL: every page is gated on the
inventory / lotsACO. Grant it to the pharmacy roles under Administration → ACL. - Menu: appears under Modules → Endear DSCSA Module (Inventory, Scan In, Dispense, Internal, DEA Records, PDMP Reports, External (3PL), Investigate, Returns, Billing, Setup). DEA Records and PDMP Reports are DSCSA+ screens — DEA Records is named-record recordkeeping (21 CFR 1304.04); PDMP Reports is the draft/review queue for take-home dispenses of a reportable schedule. Both stay fully local to OpenEMR; nothing on either screen is transmitted.
- Background sync: registers an OpenEMR background service
(
Endear_Dispense_Sync, every 15 min). It runs wheneverrun_background_services.phpis invoked, so make sure OpenEMR's cron / scheduled task is active. You can also pull on demand with Sync now on the dashboard.
Drug mapping prerequisite
A serial can only be received once its GTIN maps to an OpenEMR drug. Resolution runs highest-confidence first:
- Crosswalk — a confirmed
endear_dispense_gtin_ndc_mapentry. - Deterministic GTIN→NDC — for FDA-namespace GTINs (
indicator + "03" + NDC10 + check), the native NDC10 is sliced from the GTIN and check-digit validated, then matched to the formulary. Stored NDC11 (5-4-2) values are matched by enumerating the ≤3 native configs (4-4-2 / 5-3-2 / 5-4-1). The module auto-maps only on a single unambiguous match — true one-to-one. - Direct NDC — the NDC carried on the record vs
drugs.ndc_number.
Anything ambiguous, or any GTIN that uses a manufacturer's own GS1 company prefix (no embedded NDC), is left unmapped for the Modules → Endear DSCSA Module → GTIN Mapping screen. There you can confirm a suggested one-to-one match, choose among ambiguous candidates, search the formulary by name/NDC, or run Auto-map resolvable to clear every unambiguous one in a batch.
Highest accuracy comes from seeding the crosswalk from the DSCSA app's FDA data (which knows each labeler's native segmentation authoritatively and can cover non-NDC GTINs). If Endear later exposes a master-data endpoint returning GTIN↔NDC↔drug, point the crosswalk at it; the deterministic local calc remains the offline fallback.
Endpoints consumed
| Module action | Endear endpoint |
|---|---|
| Background / Sync now | GET /inventory/pending, then POST /inventory/acknowledge |
| Scan-in & scan-out verify | GET /inventory/verify?serial=>in= |
| Dispense | POST /inventory/dispense |
Sync safety: pull → write locally → acknowledge only what committed. A crash
mid-sync leaves rows un-acked; they reappear next cycle and de-duplicate on
staging_id. The module does not rely on the server since cursor (it is
currently ignored server-side) — it de-dups locally instead. If you later add
since to the API's get_pending signature, this module is already safe to
switch over.
PHI note
POST /inventory/dispense hashes patient MRN server-side, but sending the raw
MRN is still a PHI transmission. The module therefore does not send MRN by
default — it sends only rx_number and encounter_id for traceability. Turn
on "Transmit patient MRN" only if your BAA with Endear covers it.
Subscription & billing
Modules → Endear DSCSA Module → Subscription & Billing shows the practice's current plan and quota, lists plans, and starts checkout. Design rules:
- Prices are never hardcoded. The page renders whatever
GET /billing/plansreturns, so Endear controls pricing server-side (and this GPL code carries no price list). - No card data touches OpenEMR. "Subscribe" calls
POST /billing/checkout, receives a processor-hosted URL (e.g. Stripe Checkout), and opens it in a new tab — payment happens entirely on the hosted page (keeps PCI scope off the EMR and off this module). "Manage billing" usesPOST /billing/portalthe same way. - The page degrades gracefully if the billing endpoints aren't live yet.
The backend contract Endear must implement is in BILLING-API.md.
File list
composer.json type: openemr-module + installer-plugin require
table.sql schema + background_services row (install)
LICENSE / CHANGELOG.md
openemr.bootstrap.php module load entry
src/Bootstrap.php globals, menu, ACL, install
src/Client/DispenseApiClient.php inventory + billing REST calls
src/Lib/Gs1Parser.php GS1 DataMatrix parser (AIs 01/17/10/21, FNC1)
src/Lib/GtinNdc.php deterministic GTIN-14 <-> NDC conversion
src/Support/PdmpPolicy.php DSCSA+: which DEA schedules require PDMP reporting (MA only today) + due-date calc
src/Service/SerialInventoryService.php local DB, mapping, drug_inventory linkage, DEA record + PDMP draft persistence
src/Service/SyncService.php one pull cycle (ack-after-commit)
src/Controller/DispenseController.php page + JSON dispatch, incl. DEA Records / PDMP Reports
public/index.php front controller
public/cron-sync.php background-service hook
templates/*.html.twig dashboard, scan-in, scan-out, mapping, billing,
dea_dispense_records (DSCSA+), pdmp_review (DSCSA+)