The launch journal

What a capture endpoint owes you.

31 July 2026 · the launch journal

Integrations against under-specified endpoints all die the same death: a payload that worked in the test environment fails in production, the error is a prose string nobody's code can branch on, and half the batch was stored before the failure — nobody is sure which half. The EPCIS 2.0 capture interface, as pinned in GS1's official OpenAPI description, is the antidote, because it specifies exactly which failure surfaces where. This post is that contract, door by door, with the bodies as the wire returns them.

Two doors, two atomicity stories

POST /events — synchronous single-event capture. One event in the body. The answer is 201 (validated, stored) or a 400 problem document (rejected, not stored) — in the response, immediately. There is no job to poll. Use it for interactive tools, low-rate producers, and anywhere you want the truth in the status code.

POST /capture — the capture-job door for documents. A full EPCISDocument in, 202 + a Location header out: /capture/{captureID}. The job document at that location carries the outcome — success, per-event errors[], timestamps — and GET /capture lists your jobs, paginated, scoped to your key. The advertised limits are discoverable before you send anything: OPTIONS /capture answers with the full header set — GS1-EPCIS-Version: 2.0.1, GS1-EPCIS-Capture-Limit: 1000 events per call, GS1-EPCIS-Capture-File-Size-Limit: 6291456 bytes. Exceed either and the answer is 413 with the typed exception epcisException:CaptureLimitExceededException, limits restated in the headers.

The spec leaves one semantics choice to the implementation, and this one is law here: no partial acceptance. The standard's GS1-Capture-Error-Behaviour header lets a server declare whether a failing document is rolled back or partially proceeds. This spine supports exactly rollback. Request proceed and you get a 400 refusal, not a silent downgrade:

HTTP/1.1 400
content-type: application/problem+json

{ "type": "epcisException:ValidationException",
  "title": "unsupported GS1-Capture-Error-Behaviour",
  "detail": "this spine never partially accepts a capture job; only \"rollback\" is supported (got \"proceed\")" }

Half-stored batches are how "which half?" enters your incident channel. A job is accepted whole or rejected whole, and the sender's retry logic gets to be one line.

The failure contract, verbatim

Every failure is an RFC 7807 application/problem+json body carrying the standard's own exception vocabulary in type. Here is a real rejection — the golden-corpus fixture with a missing action, pushed through the same pipeline the HTTP door runs:

{
  "accepted": false,
  "status": 400,
  "problem": {
    "type": "epcisException:ValidationException",
    "title": "capture job rejected — no partial acceptance",
    "status": 400,
    "detail": "every EPCIS event in the payload is rejected; see errors[]",
    "captureID": "0819a3c9-bf35-4d34-b891-71c74c78e361",
    "errors": [
      { "type": "epcisException:ValidationException",
        "title": "must have required property 'action'",
        "status": 400,
        "instance": "/epcisBody/eventList/0" }
    ]
  }
}

Note the three properties your sender can rely on. The type is machine-branchable — no regex over prose. The instance is a JSON Pointer to the exact failing path in your payload. And the rejected job is still persisted: GET /capture/{captureID} returns the identical errors afterward, so the failure survives for the postmortem instead of evaporating with the response. Determinism here is a tested property, not a habit.

Idempotency: retries are a non-event

Field reality is retries — offline buffers flushing, agents re-firing, batch loads replayed. The dedupe law rides on computed identity: every event's identity is the CBV 2.0 §8.9 hash of its own content. A duplicate eventID whose hash matches the stored row is a benign no-op, acknowledged without a second insert. The same eventID with a different hash — two claims wearing one name — is rejected. Both behaviors are pinned corpus fixtures (sequences/duplicate-benign-replay.json, sequences/duplicate-different-content.json). Your sender needs no sequence numbers, no client-side ledger of what was sent: replay the whole buffer, the store converges.

One more boundary worth knowing before you build: recordTime is server-stamped at the door, and caller-supplied values for trust-boundary fields are stripped, never trusted. What you assert is the observation; what the door stamps is the record of receiving it.

What to build into your sender

The failure table compiles directly into retry policy:

ResponseMeaningSender action
201 / 202 + Locationstored / job accepted wholedone; record captureID
400 ValidationExceptionpayload wrong, deterministicallyfix at instance path; do not retry as-is
409 on POST /eventssame eventID, different contentescalate — two claims, one name
413 CaptureLimitExceededExceptionover 1,000 events / 6 MBsplit the document; resend
415wrong media typesend application/json
network failure / timeoutunknown outcomeresend as-is — idempotency makes it safe

The last row is the one that pays rent. Because identity is content-derived, "unknown outcome" costs nothing: the retry either inserts or no-ops.

Try the pipeline before you point production at it

The identical pipeline — strip, validate against the sha256-pinned official schema, hash, dedupe, stamp, append — ships in the npm package and runs entirely on your machine:

$ npx epcis.dev capture your-document.json
accepted	captureID=142dcbee-06e8-48ea-8252-985e8ecb68cf	events=1
eventID	urn:uuid:7a000039-0001-4000-8000-000000000001

Same laws, same error bodies, exit codes 0/1/2 an agent can branch on. Validate a decade of payloads locally before the first production POST; the schema pins are on the conformance page and the local toolkit is on run it yourself.

When you are ready to point a WMS, an ERP, or a line at the hosted door, say what you will capture — capture keys are scoped to exactly that. The read side of the contract — queries, traces, and the pagination law — is the query interface's own entry in this journal, and the document layer those events join to (purchase orders, despatch advices) is /business-transactions/.

We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.