> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Your first EDI 850 → 855 → 856 → 810 cycle

> Onboard a trading partner, receive an inbound 850 purchase order, acknowledge it with a 997 and an 855, ship it with an 856, invoice it with an 810, and see where errors and retries surface.

One retail order, four documents. The retailer sends an **850** purchase order; you answer with a
**997** (the envelope was readable) and an **855** (you accept the order), ship against it with an
**856** advance ship notice, and bill it with an **810** invoice. Each outbound document is then
tracked until the retailer's own 997 names it. This guide runs that cycle against
`https://edi.stateset.com` with one illustrative partner, `acme-retail`, and one PO, `4500012345`.

It assumes you know what the gateway is — if not, read the [overview](/stateset-edi/overview) and
the [quickstart](/stateset-edi/quickstart). The focus here is the order of calls, what each response
tells you, and what to do when one does not say what you expected.

## Before you start

* A tenant API key, sent as `Authorization: Bearer <key>` on every request (the admin token picks a
  tenant with `x-tenant-id`, default `default`).
* Your ISA qualifier and ID (`ZZ` / `STATESET` below), the retailer's, and their X12 version.

JSON endpoints wrap results as `{ "success": true, "data": ... }`. The outbound document endpoints
do **not** return JSON: the body is the wire-ready X12 interchange, `Content-Type: application/edi-x12`.

<Steps>
  <Step title="Register the trading partner">
    A partner is one `TradingPartner` object, keyed by the `id` in the URL. Only the four ISA fields
    are required.

    ```bash theme={null}
    curl --request PUT "https://edi.stateset.com/v1/partners/acme-retail" \
      --header "Authorization: Bearer $STATESET_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "id": "acme-retail",
        "name": "Acme Retail (illustrative)",
        "partner_isa_qualifier": "ZZ",
        "partner_isa_id": "ACMERETAIL",
        "our_isa_qualifier": "ZZ",
        "our_isa_id": "STATESET",
        "version": "004010",
        "usage": "T",
        "ack_sla_hours": 24
      }'
    ```

    `partner_isa_qualifier` + `partner_isa_id` is how inbound documents are routed: the gateway reads
    the ISA sender of whatever arrives and looks up the matching partner. `ack_sla_hours` is the clock
    the reconciliation monitor runs for this partner's 997s, overriding the global `EDI_ACK_SLA_HOURS`.

    Before the retailer sends anything real, `POST /v1/partners/acme-retail/certification/run-standard`
    runs the prebuilt retail-baseline pack against this envelope and reports `passed` per case.

    <Note>
      An empty `validation` block means this partner's implementation guide accepts everything — right
      for a first cycle; tighten it once the retailer's spec is in hand ([Configuration](/stateset-edi/configuration)).
      Transport is configuration too: moving this partner to AS2, SFTP or a VAN changes nothing below
      except who carries the bytes ([Transports](/stateset-edi/transports)).
    </Note>
  </Step>

  <Step title="Receive the 850 and return the 997">
    The interchange below is adapted from `examples/po.edi` in the server repository (PO number and
    date changed). Save it as `po-4500012345.edi`:

    ```text theme={null}
    ISA*00*          *00*          *ZZ*ACMERETAIL     *ZZ*STATESET       *260610*0930*U*00401*000000101*0*T*>~
    GS*PO*ACMERETAIL*STATESET*20260610*0930*101*X*004010~
    ST*850*0001~
    BEG*00*SA*4500012345**20260610~
    PO1*1*12*EA*24.5*PE*SK*SHOE-RED-10~
    CTT*1~
    SE*5*0001~
    GE*1*101~
    IEA*1*000000101~
    ```

    `BEG*00*SA` is an original, stand-alone order; `PO1` is line 1, twelve each at 24.50, by seller SKU
    (`SK`). To dry-run it first, post the same body to `POST /v1/edi/validate/acme-retail`: the
    `ValidationReport` says `valid: true` or names the `structural_error` / per-document `reason`, with
    no side effects. Then submit it for real — `POST /v1/edi/inbound` auto-detects the syntax and routes
    by ISA sender (`POST /v1/edi/inbound/{partner_id}` is the route a partner-scoped key may call).

    ```bash theme={null}
    curl --request POST "https://edi.stateset.com/v1/edi/inbound" \
      --header "Authorization: Bearer $STATESET_API_KEY" \
      --header "Content-Type: application/edi-x12" \
      --data-binary @po-4500012345.edi
    ```

    ```json Response (abridged) theme={null}
    {
      "success": true,
      "data": {
        "partner_id": "acme-retail",
        "interchange_control_number": "000000101",
        "status": "processed",
        "documents": [{ "transaction_set": "850", "control_number": "0001", "status": "accepted",
                        "reference": "4500012345", "parsed": { "po_number": "4500012345", "...": "..." } }],
        "events_queued": 1,
        "ack_997": "ISA*00*...~GS*FA*STATESET*ACMERETAIL*...~ST*997*0001~AK1*PO*101~AK2*850*0001~AK5*A~AK9*A*1*1*1~SE*6*0001~GE*1*...~IEA*1*...~"
      }
    }
    ```

    Three things happened. The PO was parsed into a `PurchaseOrder850` and stored in the document
    ledger under the reference `4500012345` — the PO number is the handle every later document and trace
    uses. An `edi.purchase_order.received` event was queued in the durable outbox for the StateSet
    sequencer (`events_queued: 1`). And a **997** was generated, acknowledging group `101`
    (`AK1*PO*101`) and accepting transaction `0001`.

    Over the HTTP API, returning that 997 to the retailer is your job — it is the `ack_997` string. On
    the file and SFTP transports the worker writes the `.997.edi` beside the inbound file for you.

    <Warning>
      Send the same file twice and the second call is `409 Conflict`: the gateway dedups on (partner,
      interchange control number), so retailer resends and your own retries never create a second order.
      Seen-index retention is covered in [Configuration](/stateset-edi/configuration#retention).
    </Warning>
  </Step>

  <Step title="Acknowledge the order with an 855">
    The 997 said "we could read it". The 855 says "we will fill it", line by line. `po_number`,
    `ack_date` and at least one line are required. `ack_type` is BAK02 (`AC` accept, `AD` accept with
    detail, `RJ` reject); each line's `status` is ACK01 (`IA` accepted, `IB` backordered, `IQ` quantity
    changed, `IR` rejected).

    ```bash theme={null}
    curl --request POST "https://edi.stateset.com/v1/edi/outbound/855/acme-retail" \
      --header "Authorization: Bearer $STATESET_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "po_number": "4500012345",
        "ack_date": "2026-06-10",
        "ack_type": "AD",
        "lines": [
          { "line_number": "1", "quantity": "12", "uom": "EA", "unit_price": "24.50",
            "status": "IA", "ids": [{ "qualifier": "SK", "value": "SHOE-RED-10" }] }
        ]
      }'
    ```

    The response body is the enveloped X12 855 — `ST*855*0001`, the first this partner has been sent.
    Behind it the gateway applied the partner's `overlay` (if any), minted ISA/GS/ST control numbers
    from the per-partner sequence, wrote a `sent` ledger record, queued the outbound event, recorded
    the 855 as **pending** in acknowledgment reconciliation, and handed it to AS2, AS1 or the outbound
    directory if one is configured. Over plain HTTP, delivering the body is up to you.

    Partial acceptance is still one 855: mark the short line `IQ`, or `IB` with `status_quantity` for
    the backordered portion. A declined order is `ack_type: "RJ"`.
  </Step>

  <Step title="Ship it with an 856">
    `shipment_id` (BSN02), `ship_date` and `orders[].po_number` are required. Use `orders[].cartons` —
    each carton with its SSCC-18 and contents — for the S→O→P→I hierarchy retailers expect;
    `orders[].items` is for shipments with no carton breakdown.

    ```bash theme={null}
    curl --request POST "https://edi.stateset.com/v1/edi/outbound/856/acme-retail" \
      --header "Authorization: Bearer $STATESET_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "shipment_id": "SHIP-4500012345-1",
        "ship_date": "2026-06-12",
        "carrier_scac": "FDEG", "tracking_number": "794611223344",
        "orders": [{
          "po_number": "4500012345",
          "cartons": [
            { "sscc": "000123456000000018", "items": [{ "sku": "SHOE-RED-10", "sku_qualifier": "SK", "quantity": "6", "uom": "EA" }] },
            { "sscc": "000123456000000025", "items": [{ "sku": "SHOE-RED-10", "sku_qualifier": "SK", "quantity": "6", "uom": "EA" }] }
          ]
        }]
      }'
    ```

    Each carton becomes an `HL*…*P` loop with `MAN*GM` carrying its SSCC, so the barcode on the physical
    label and the ASN agree — mint those labels from the same payload with
    `POST /v1/labels/856/acme-retail/cartons`. The ASN is the document retailers charge back on most:
    send it when the truck leaves, and make sure the SSCCs here are the ones actually printed.
  </Step>

  <Step title="Invoice it with an 810">
    `invoice_number`, `invoice_date`, `po_number`, `lines` and `total_amount` are required. Freight,
    handling and discounts go in `allowances` as explicit SAC segments, not folded into unit prices.

    ```bash theme={null}
    curl --request POST "https://edi.stateset.com/v1/edi/outbound/810/acme-retail" \
      --header "Authorization: Bearer $STATESET_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "invoice_number": "INV-2026-00417",
        "invoice_date": "2026-06-12",
        "po_number": "4500012345",
        "lines": [
          { "line_number": "1", "quantity": "12", "uom": "EA", "unit_price": "24.50",
            "ids": [{ "qualifier": "SK", "value": "SHOE-RED-10" }] }
        ],
        "allowances": [{ "indicator": "C", "code": "D240", "amount": "15.00", "description": "Freight" }],
        "total_amount": "309.00"
      }'
    ```

    Money fields are strings and stay exact; `total_amount` lands in `TDS` with two implied decimals.
    Twelve × 24.50 plus 15.00 freight is 309.00 — the gateway does not compute the total, and a total
    that does not foot is the classic short-pay trigger.
  </Step>

  <Step title="Close the loop: the retailer's 997, reconciliation, and the trace">
    Every outbound document is **pending** until the retailer's 997 names it.
    `GET /v1/reconciliation?outstanding=true` now shows three `OutboundAck` rows for `acme-retail` —
    sets `855`, `856`, `810`, each with its ST `control_number` and `status: "pending"` — and
    `outstanding: 3` on the envelope.

    The retailer's 997 arrives through the same inbound door as their 850. This one accepts the 855
    (adapted from the reconciliation test in `tests/http_api.rs`):

    ```text theme={null}
    ISA*00*          *00*          *ZZ*ACMERETAIL     *ZZ*STATESET       *260614*0930*U*00401*000000950*0*T*>~
    GS*FA*ACMERETAIL*STATESET*20260614*0930*950*X*004010~
    ST*997*0001~
    AK1*PR*1~
    AK2*855*0001~
    AK5*A~
    AK9*A*1*1*1~
    SE*6*0001~
    GE*1*950~
    IEA*1*000000950~
    ```

    Post it to `POST /v1/edi/inbound` exactly as you posted the 850. FA groups are never themselves
    acknowledged (`ack_997` is `null`) and queue no event; the gateway matches `AK2*855*0001` against
    the pending 855 by set + ST control number and maps `AK5` to a status — `A` → `accepted`, `E` →
    `accepted_with_errors`, `P` → `partially_accepted`, `R` → `rejected`. `outstanding` drops to 2.

    Finally, trace the chain from the PO's point of view, and push the queued events to the StateSet
    sequencer now instead of waiting for the `EDI_FLUSH_SECONDS` worker:

    ```bash theme={null}
    curl "https://edi.stateset.com/v1/lifecycle/4500012345" \
      --header "Authorization: Bearer $STATESET_API_KEY"

    curl --request POST "https://edi.stateset.com/v1/outbox/flush" \
      --header "Authorization: Bearer $STATESET_API_KEY"
    ```

    The trace lists every event carrying that reference, oldest first. `GET /v1/documents` is the same
    ledger paged newest-first across partners — each `id` opens the `raw` wire, `parsed` payload,
    generated `acknowledgment` and any `validation_issues` — and `GET /v1/evidence/4500012345` packages
    the chain as a compliance bundle when a chargeback needs answering.
  </Step>
</Steps>

## Where errors surface

| Layer                | Symptom                                                                                                                  | Meaning                                                                                                                           | Recovery                                                                        |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Routing              | `404` on `POST /v1/edi/inbound`                                                                                          | No partner's ISA qualifier/ID matches the sender                                                                                  | Fix the partner record, resubmit                                                |
| Dedup                | `409 Conflict`                                                                                                           | Same partner + interchange control number already processed                                                                       | Nothing — the original stands; compare in `GET /v1/documents`                   |
| Envelope             | `422`, or `200` with `status: "interchange_rejected"` and `ack_ta1` set                                                  | Unparseable, or readable but invalid ISA/GS/GE/IEA (counts, control mismatch)                                                     | Return the TA1; the partner resends                                             |
| Implementation guide | `200`, `status: "processed"`, document `status: "rejected"` with `error`; `ack_997` contains `AK5*R`; `events_queued: 0` | The partner violated its own guide (disallowed set, missing segment, bad element)                                                 | Send the 997 back; or fix the guide and `POST /v1/documents/{id}/reprocess`     |
| Outbound build       | `422` from an outbound endpoint                                                                                          | Payload fails validation (an 855 with no lines) or a value contains one of the partner's delimiters — X12 has no escape mechanism | Correct the payload                                                             |
| AS2 delivery         | Delivery `failed` in `GET /v1/deliveries`, critical alert                                                                | The worker retried with exponential backoff (1m, 2m, 4m, ... capped at 30m) and hit the attempt cap of 5                          | `POST /v1/deliveries/{id}/redeliver` once the partner endpoint is back          |
| Sequencer            | Events in `GET /v1/deadletter`                                                                                           | The StateSet sequencer permanently rejected an event after its retries                                                            | `POST /v1/deadletter/replay` — safe to repeat; the sequencer dedups by event ID |
| Acknowledgment SLA   | Rows in `GET /v1/reconciliation?overdue=true`                                                                            | No 997 from the retailer within `ack_sla_hours`                                                                                   | Chase the retailer; monitor alerts fire as the overdue set grows                |

Reprocess and replay are idempotent: event IDs derive from (partner, ISA13, GS06, ST02), so re-deriving
events from the same wire collapses in the outbox and at the sequencer.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The 850 came back rejected with 'not configured to send'">
    The partner's `validation.accepted_inbound` does not include `850`. That list is an allow-list:
    once non-empty, every set not in it is rejected in the 997 with `AK5*R`. Add `850` (and `860` if
    the retailer sends changes) with another `PUT /v1/partners/acme-retail`, then `POST
            /v1/documents/{id}/reprocess` the stored document — it re-validates against the current guide and
    issues a fresh 997.
  </Accordion>

  <Accordion title="My 855 / 856 / 810 has been pending for days">
    Two problems look the same here. On AS2, check `GET /v1/deliveries` first — a `failed` or
    long-`pending` delivery means the retailer never received the document, so no 997 is coming;
    redeliver it. If it is `delivered` (or the file demonstrably landed), the retailer is simply late,
    and `?overdue=true` will say so once `ack_sla_hours` has elapsed.
  </Accordion>

  <Accordion title="The retailer's 997 arrived but reconciliation still shows pending">
    Matching is by (partner, transaction set, ST control number) — the `AK2` pair. Open the inbound 997
    with `GET /v1/documents/{id}` and compare its `AK2*855*0001` with the pending `OutboundAck`'s
    `control_number`. Padded or truncated control numbers, or a 997 sent under a different ISA sender
    than the partner record, will not match.
  </Accordion>
</AccordionGroup>

## What you built

| Piece                         | What it is                                                                            | Where to go deeper                                                                                                                                                                                     |
| ----------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Trading partner `acme-retail` | The envelope contract, ack type and SLA, routed by ISA sender                         | [Upsert partner](/api-reference/edi/partners/v1-partners-by-update)                                                                                                                                    |
| Inbound 850 → 997             | A parsed PO in the ledger, an event in the outbox, an acknowledgment for the retailer | [Inbound](/api-reference/edi/inbound/v1-edi-inbound-create)                                                                                                                                            |
| Outbound 855, 856, 810        | Three enveloped X12 documents, each tracked as pending until the retailer's 997       | [855](/api-reference/edi/outbound/v1-edi-outbound-855-by-create) · [856](/api-reference/edi/outbound/v1-edi-outbound-856-by-create) · [810](/api-reference/edi/outbound/v1-edi-outbound-810-by-create) |
| Reconciliation and trace      | Acknowledgment status per document, and one timeline per PO                           | [Reconciliation](/api-reference/edi/operations/v1-reconciliation-list) · [Lifecycle](/api-reference/edi/operations/v1-lifecycle-by-get)                                                                |

Every call above is also an MCP tool with the same handler behind it; the server's
`edi_demo_order_to_invoice_flow` tool runs this cycle in one shot — see the [EDI MCP server](/stateset-edi/mcp-server).

## Next steps

<CardGroup cols={2}>
  <Card title="Move the partner to AS2 or SFTP" icon="network-wired" href="/stateset-edi/transports">
    Add an `as2` or SFTP configuration to the same partner record; the pipeline above does not change.
  </Card>

  <Card title="Label the cartons" icon="barcode" href="/api-reference/edi/outbound/v1-labels-856-by-cartons-create">
    One GS1-128 / SSCC-18 ZPL label per carton, minted from the same 856 payload.
  </Card>

  <Card title="Operate it" icon="gauge" href="/stateset-edi/operations">
    Business reconciliation, evidence bundles, deductions, monitoring and alerting.
  </Card>

  <Card title="Every transaction set" icon="table-list" href="/stateset-edi/transaction-sets">
    860 changes, 865 change acks, 940/945 warehouse flows, 820 remittance, and the EDIFACT pair.
  </Card>
</CardGroup>
