Skip to main content

mock-edi

CI Python 3.8+ License: MIT PyPI

A mock EDI trading partner. Not an EDI library and not an AS2 server — the thing on the other end. Send it an 850 and it sends back a 997, then an 855 that answers line by line, then an 856 with a shipment tree, then an 810 that bills what shipped. Send it an EDIFACT ORDERS and the same thing happens in CONTRL / ORDRSP / DESADV / INVOIC.

   you ──850──▶  mock-edi
       ◀──997──  the syntax parsed
       ◀──855──  2 lines: one confirmed, one short
       ◀──856──  shipment / order / item, with a tracking number
       ◀──810──  1132.80, terms 2% 10 net 30

There is plenty of open source for speaking EDI — OpenAS2 and mendelson will terminate an AS2 connection, and a dozen libraries will parse an X12 segment. What none of them is, is a counterparty. To test the code that runs when an 856 arrives unannounced, or when the invoice comes twice, or when the acknowledgment never comes at all, you need a partner that does those things on demand. Real ones do them on their own schedule, and getting one to do it deliberately is a support ticket and a fortnight.

  • Zero dependencies. Python 3.8+ standard library and SQLite, nothing else. It installs in a locked-down CI image.
  • Real wire shapes. A 106-character fixed-width ISA that declares its own delimiters, EDIFACT's ? release character, composite elements, TDS with two implied decimals, HL parent pointers, AK3/AK4 error codes, MDNs with a Received-Content-MIC.
  • Failure on demand. Short shipments, rejected lines, refused orders, duplicate invoices, a strict partner, and a partner that never answers — each one PATCH away.
  • It validates its own output. Every document the mock writes is checked against the same dictionary it checks yours against. There is a test for it.

MIT licensed. ASC X12 and UN/EDIFACT are standards published by their respective bodies; AS2 is RFC 4130. This project implements publicly documented wire formats for testing purposes and is not affiliated with or endorsed by any standards body or vendor.


Quick start

pip install mock-edi
mock-edi --port 8080
curl -X POST --data-binary @order.edi http://127.0.0.1:8080/edi
{
  "accepted": true,
  "partner": "ACME",
  "dialect": "X12",
  "orders": ["4500000042"],
  "transactionSets": [
    {"code": "850", "control": "0001", "kind": "order", "accepted": true, "findings": []}
  ],
  "queued": [
    {"kind": "acknowledgment", "code": "997", "reference": "000000077", "dueAt": "..."},
    {"kind": "response",       "code": "855", "reference": "4500000042", "dueAt": "..."},
    {"kind": "despatch",       "code": "856", "reference": "4500000042", "dueAt": "..."},
    {"kind": "invoice",        "code": "810", "reference": "4500000042", "dueAt": "..."}
  ]
}

Then collect what it sent you:

curl "http://127.0.0.1:8080/_mock/mailbox?raw"
ISA*00*          *00*          *ZZ*MOCKEDI        *ZZ*ACME           *260924*1030*U*00401*000000001*0*T*>~
GS*PR*MOCKEDI*ACME*20260924*1030*2*X*004010~
ST*855*0002~
BAK*00*AD*4500000042*20260924***20260924*5100002~
...
PO1*1*100*EA*12.50**VP*WIDGET-001*UP*076123400003~
ACK*IA*100*EA*068*20260926~

Run it from a checkout with no install at all, or in a container:

python3 -m mockedi --port 8080
docker build -t mock-edi . && docker run -p 8080:8080 mock-edi

A guided tour of every endpoint, in curl:

bash examples/demo.sh

And an example of the code it exists to test: examples/po_bridge.py sends SAP purchase orders as 850s and posts the 855s back into SAP, and examples/test_po_bridge.py tests it against this mock and mock-sap. There is a walkthrough. The other half of the same integration lives in mock-sap: examples/invoice_check.py checks this mock's 810 invoices against the purchase order and the 856 ship notice before posting them into SAP, and its tests cover a short shipment, a price disagreement and the duplicate-invoice behaviour.

What it serves

Surface Endpoint
AS2 inbound POST /as2 — answers with an MDN, synchronous or asynchronous
Asynchronous MDN inbound POST /as2/mdn — a partner's receipt for something the mock sent
Plain EDI inbound POST /edi — the same pipeline, answering with a JSON summary
Validate only POST /_mock/validate — findings, and nothing changed
Mailbox GET /_mock/mailbox — collect what is waiting; ?leave to peek, ?raw for payloads
Outbox GET /_mock/outbox — the queue, including what is not due yet
Release the queue POST /_mock/advance — ?seconds=N or ?all
Send out of band POST /_mock/send — replay an invoice, or send one unprompted
Partners GET/POST /_mock/partners, GET/PATCH/DELETE /_mock/partners/<id>
Orders GET /_mock/orders, GET /_mock/orders/<po>
Archive GET /_mock/documents, GET /_mock/interchanges, GET /_mock/interchanges/<id>?raw
Receipts GET /_mock/mdns
Outstanding documents GET /_mock/unacknowledged?older-than=60
Work promised, not done GET /_mock/scheduled
Directory trading GET /_mock/drop, POST /_mock/drop/scan
The dictionary GET /_mock/dictionary, /_mock/dictionary/X12/850
Health and state GET /_mock/health, GET /_mock/state, GET /_mock/requests
Reset POST /_mock/reset
Index page GET /

The documents

Business document X12 EDIFACT
Purchase order 850 ORDERS
Purchase order response 855 ORDRSP
Purchase order change 860 ORDCHG
Change acknowledgment 865 ORDRSP
Despatch advice / ship notice 856 DESADV
Invoice 810 INVOIC
Syntax acknowledgment 997 CONTRL

Both dialects are read and written from one dictionary (mockedi/schema.py), and one pipeline drives both, so what you assert about an X12 flow holds for the EDIFACT one. GET /_mock/dictionary/X12/850 serves that dictionary as JSON — the actual rules, not a description of them that can go stale.

Coverage is the commonly traded core of each set, not the full standard. A real 850 admits some fifty segment types and almost nobody sends more than a dozen; the mock implements the dozen, validates them properly, and reports an unrecognised segment rather than pretending to understand it.

Partner behaviours

Four partners are seeded. Change any of them at runtime:

curl -X PATCH -H 'Content-Type: application/json' \
     -d '{"behaviour":"short-ship"}' \
     http://127.0.0.1:8080/_mock/partners/ACME
Behaviour What the partner does
accept Confirms everything in full and ships what was ordered.
short-ship Confirms less than was ordered (855 IQ, ORDRSP QTY+83), and ships and invoices the confirmed quantity.
reject-line Refuses one line outright (IR) and leaves it out of the shipment and the invoice.
reject-all Acknowledges the syntax, then refuses the order (BAK RJ).
no-ack Says nothing at all. No 997, no 855. For testing your chase-up timer — the failure that actually costs money.
duplicate-invoice Sends the invoice twice with the same invoice number, as a partner with a retry bug does.
strict Rejects a transaction set for any finding, not only a fatal one.

Two rules apply whatever the behaviour says, because they are what real sellers actually do:

  • an item that is not in the catalogue is rejected (IR), and
  • a price the seller disagrees with is billed at the seller's price and flagged IP. Price discrepancies are the commonest EDI dispute there is.

Timing

By default every document is released the moment it is produced, so a test can POST an order and read four documents back on the next line. Give them delays when what you are testing is the waiting:

mock-edi --ack-delay 2000 --response-delay 30000 --invoice-delay 86400000

Nothing is released on a timer of its own. POST /_mock/advance?all releases whatever is queued, whenever it was due — a test that has to sleep is slow and flaky, and one that advances the clock is neither.

AS2

curl -X POST --data-binary @order.edi \
  -H 'Content-Type: application/edi-x12' \
  -H 'AS2-From: ACME' -H 'AS2-To: MOCKEDI' \
  -H 'Message-ID: <po-1@acme.example>' \
  -H 'Disposition-Notification-To: edi@acme.example' \
  -H 'Disposition-Notification-Options: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha256' \
  http://127.0.0.1:8080/as2

comes back as a multipart/report MDN with the MIC of what arrived:

Disposition: automatic-action/MDN-sent-automatically; processed
Received-Content-MIC: +H1EWvEMSJH/IHGsjy7c/dviFRwLgRoGBmxnTEbMkGA=, sha256

Name a Receipt-Delivery-Option and the response is 202 with the MDN posted back to that URL instead.

S/MIME is deliberately not implemented. Signing and encrypting AS2 payloads needs certificates and a cryptography library, and this project has no dependencies on purpose. A message that arrives encrypted or signed is refused with an MDN saying exactly that, rather than being mangled. If your integration must be tested against signed AS2, this mock is the wrong tool and will tell you so on the first message.

Making the mock come to you

A partner with no as2_url is a mailbox. Give one a URL and the mock stops being something you poll and becomes something that arrives:

curl -X PATCH -H 'Content-Type: application/json' \
     -d '{"as2_url":"http://localhost:9000/as2"}' \
     http://127.0.0.1:8080/_mock/partners/ACME

Documents are then POSTed to your listener with AS2 headers, in the order they were queued, and whatever MDN you return is recorded against them in /_mock/outbox.

Changing an order

A buyer changes an order it has already placed with an 860 (or an ORDCHG, or an 850 restated with BEG01 = 04), and the seller answers with an 865. EDIFACT has no separate change acknowledgment message, so an ORDCHG is answered by an ORDRSP — the difference most likely to catch out someone porting a mapping from X12.

POC*1*QD*60**EA*12.50**VP*WIDGET-001~     the buyer wants 60, not 100
ACK*IA*60*EA*068*20260926~                 the seller agrees
POC*2*DI*40**EA*4.15**VP*BRKT-050~         the buyer drops line 2
ACK*IR*0*EA~
REF*ZZ**Line deleted at the buyer's request~

A change cannot unmake what has already happened. A quantity cannot go below what shipped, a shipped line cannot be deleted, an order that shipped cannot be cancelled, and an order that has been invoiced cannot be changed at all. A refused line comes back IR with the reason, and the order keeps what it had — reporting the order's state instead would tell the buyer its request succeeded.

Give yourself a window. A change is only meaningful before the goods leave, and with every delay at zero the order is invoiced before the POST returns, so every change would be refused. That is correct behaviour, not a limitation to work around:

mock-edi --despatch-delay 3600000 --invoice-delay 3600000

Delays postpone the work, not merely the posting. A despatch that is not due yet has not been packed, so a change arriving in the meantime affects it — which is the whole point, and why GET /_mock/scheduled shows work promised but not done, separately from /_mock/outbox, which shows documents that already exist.

Acknowledgments, both ways

The mock sends a 997 for everything it receives. It also reads one for everything it sends, which is what makes the most expensive EDI failure testable: nobody acknowledged my invoice.

curl "http://127.0.0.1:8080/_mock/unacknowledged?older-than=60"
[
  {"code": "810", "kind": "invoice", "reference": "4500000042",
   "group_control": "4", "control": "0004", "partner": "ACME", "at": "..."}
]

Send a 997 back and the document it names is marked with the verdict:

{"acknowledged": [
  {"code": "810", "control": "0004", "matched": true, "status": "rejected",
   "note": "BIG at segment 2: Segment has data element errors; element 4: Invalid code value ('BADPO')"}
]}

The two dialects address what they are acknowledging differently, and both are matched properly. X12 names a transaction set inside a functional group — AK102 quotes GS06, AK202 quotes ST02, and both are needed because ST02 is only unique within its group. EDIFACT names a message inside an interchange, with UCI01 quoting UNB's control reference and UCM01 quoting UNH01.

An acknowledgment naming something the mock never sent comes back "matched": false rather than being silently dropped — it is real and common, and usually evidence of the bug you are looking for.

Set a partner to no-ack and nothing is ever acknowledged, so /_mock/unacknowledged keeps filling up. That is the point.

Trading over a directory

Not all EDI is AS2. A great deal of it is still a folder: the partner writes a file into it, you pick the file up; you write a file back, they pick it up.

mock-edi --drop-dir ./edi/in --pickup-dir ./edi/out

Anything dropped in ./edi/in goes through the same pipeline a POST does, and the answers are written into ./edi/out as <partner>-<code>-<control>.edi — written to a temporary name and renamed, so nothing watching the directory ever sees a half-written file.

Two things every directory integration meets are handled rather than left to bite. A file still being written is not read: anything modified within --drop-settle-ms is left for the next pass, and .tmp, .part and dotfiles are never read at all. A file that has been read is not read again: it is moved into processed/, or into failed/ if it could not be read — moved rather than deleted, because a mock that eats the evidence is no use when a test fails.

The poller runs every --drop-interval-ms, but it is not the only way in:

curl -X POST http://127.0.0.1:8080/_mock/drop/scan

scans once and returns what it found, so a test never has to wait for a poll interval — the same reason /_mock/advance exists.

Validation

Every inbound document is checked against the dictionary, and the findings become a real 997 or CONTRL — AK3/AK4 with X12 error codes, UCS/UCD with EDIFACT ones. Ask for the findings as prose instead:

curl -X POST --data-binary @broken.edi http://127.0.0.1:8080/_mock/validate
{
  "clean": false,
  "groupCode": "R",
  "explain": [
    "850/0001: rejected",
    "  BEG at segment 2: ZZ is not a code BEG01 accepts (00, 01, 04, 05, 06, 07, ...)",
    "  BEG at segment 2: BEG05 is not a valid date: '2026-09-24'",
    "  PO1 at segment 3 in the PO1 loop: PO102 must be a number, got 'ten'",
    "  SE01 counts 99 segments, the message holds 6 (4)"
  ]
}

Severity is the mock's own policy, and it is stated rather than implied. A fatal finding rejects the transaction set — an unknown set, a missing mandatory segment or element, a control number that does not match its trailer, a segment count that does not add up. Everything else is accepted with errors noted: an invalid code, a length violation, a malformed date, a segment the set does not define. A partner set to strict rejects on either.

Two limits, stated plainly: loop membership and repetition counts are checked but loop sequence is not, and conditional requirements ("if PO104 is present then PO103 must be") are not modelled. Both would need a rule language to express, and the mock would rather leave them out than pretend.

Layout

mockedi/schema.py        elements, segments, loops, transaction sets  (add shapes here)
mockedi/envelope.py      the shape both dialects share, and delimiter handling
mockedi/x12.py           reading and writing ASC X12 interchanges
mockedi/edifact.py       reading and writing UN/EDIFACT interchanges
mockedi/validate.py      checking a document against the dictionary
mockedi/ack.py           turning findings into a 997 or a CONTRL
mockedi/reconcile.py     reading an acknowledgment for something we sent
mockedi/transactions.py  business documents in, business documents out
mockedi/documents.py     what the seller decides, and the shipment and invoice
mockedi/partners.py      who we trade with, and how each one misbehaves
mockedi/pipeline.py      the choreography: an order in, four documents back
mockedi/delivery.py      posting to a partner that has somewhere to receive
mockedi/as2.py           AS2 headers, the MIC, and the MDN
mockedi/drop.py          trading over a directory rather than over HTTP
mockedi/db.py            SQLite: schema, number ranges, demo data
mockedi/server.py        HTTP: AS2, /edi, and the control plane

docs/ARCHITECTURE.md explains how they fit together; docs/FILES.md is an index of every file in the repository.

Tests

python3 -m unittest discover -s tests -v

228 tests, every one of them talking to a real mock over real HTTP. Nothing is stubbed. The most valuable one is in tests/test_dictionary.py: every document the mock generates is validated against the same dictionary it validates yours with, so the day someone adds a segment to a writer and forgets the definition, the suite says so.

Extending it

Add a segment or a transaction set in schema.py and it is parsed, validated and published in /_mock/dictionary without touching anything else. Add a behaviour in documents.decide(). Add an endpoint in server.py. CONTRIBUTING.md says where each kind of change goes and what a good pull request carries.

See also

mock-sap — the same idea for SAP: OData V2 and V4, BAPI/RFC and IDoc shapes over SQLite, also with zero dependencies. An IDoc ORDERS05 and an X12 850 are the same business document, so the two mocks make a reasonable pair of ends for testing a middleware layer. examples/po_bridge.py is one, and mock-sap's examples/invoice_check.py is another.

Release files for mock-edi 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mock-edi 0.2.0
File Size Uploaded
mock_edi-0.2.0.tar.gz 148.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mock-edi 0.2.0
File Interpreter ABI Platform
mock_edi-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 253.9 kB

Release files / mock_edi-0.2.0.tar.gz

Download URL mock_edi-0.2.0.tar.gz
Size 148.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c011a35b09eb42f5f2dfbd4dccb66232fa0c95b3aa430fbcad523fe6aa7aa79e
BLAKE2b-256 checksum
How to use checksums
ff0334f059f336dc726618e531a6518e8001174e9dc97cf4d26b590042a96631
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / mock_edi-0.2.0-py3-none-any.whl

Download URL mock_edi-0.2.0-py3-none-any.whl
Size 105.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0124a326a856cf27f79bc0b7855b283721d0ceea5b34b870e78b66f734c478b2
BLAKE2b-256 checksum
How to use checksums
046a9f64b455246bc0d4066f5945c6306fff24c621b89a4a1c86e8f3f93ec814
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page