Docs

The typed exceptions

A refusal from any door on this origin is an RFC 7807 application/problem+json document whose type is one of the EPCIS standard's own epcisException:* identifiers. The type and status are the contract; title and detail are worded for humans and may be rephrased between releases. Match on type, never on prose.

The taxonomy the shipped surfaces emit

typestatusmeaningremediation
epcisException:ValidationException400malformed JSON, a document the pinned schema rejects, or a failed translationfix the payload; the per-path errors array names every violation
epcisException:QueryParameterException400a SimpleEventQuery the pinned query-schema rejectsfix the named parameter; CBV values are bare words (shipping, not a URN)
epcisException:SecurityException401a keyed door reached without a keysend Authorization: Bearer <capture-scoped key>; ask for one at /get-a-key/
epcisException:NoSuchResourceException404an eventID that is not in the caller's scope — absent, never redactednothing to retry; an out-of-scope event and a nonexistent one answer identically
epcisException:ResourceAlreadyExistsException400, nested per eventa capture that collides with a stored event of the same eventID but different content — the job refuses whole as a 400 ValidationException envelope ("no partial acceptance") and the collision arrives per event inside its errors[]a byte-identical replay is a benign ack; different bytes under the same identity are refused whole — submit a correction as a new event carrying an errorDeclaration
epcisException:CaptureLimitExceededException413payload exceeds the file-size limitsplit the document
epcisException:UnsupportedMediaTypeException415the right payload at the wrong doorthe detail names the right one: XML goes to /translate, JSON to /validate and /hash
epcisException:ImplementationException501a capability this door does not serve: the resolve and subscribe MCP toolsresolve: identity resolution is homed at id.org.ai; subscribe: use the query tool — this deployment serves pull queries only

The CLI mirrors the same discipline on stderr with four stable code strings — USAGE, PARSE, NOT_FOUND, INTERNAL — documented on the CLI reference.

Real refusals, cut from real runs

415 — the wrong door names the right one. JSON posted to the XML door:

$ curl -s -X POST https://epcis.dev/translate \
    -H 'Content-Type: application/json' -d '{"not":"xml"}'
{
  "type": "epcisException:UnsupportedMediaTypeException",
  "title": "Unsupported Media Type",
  "status": 415,
  "detail": "the translation door accepts application/xml or
    text/xml (got \"application/json\"); POST JSON to /validate
    or /hash"
}

400 — malformed bytes are a ValidationException, not a 500.

$ curl -s -X POST https://epcis.dev/validate \
    -H 'Content-Type: application/json' -d '{"broken'
{
  "type": "epcisException:ValidationException",
  "title": "malformed JSON",
  "status": 400,
  "detail": "SyntaxError: Unterminated string in JSON at
    position 8 (line 1 column 9)"
}

401 — the keyed ceremony, stated by the door itself. On the apex MCP door a keyed tool refuses as a tool-level error (isError: true) carrying the same problem document — the HTTP envelope stays 200 because in MCP a refusal is a result, not a transport failure:

$ curl -s -X POST https://epcis.dev/mcp \
    -H 'Content-Type: application/json' -d '{
      "jsonrpc":"2.0","id":1,"method":"tools/call",
      "params":{"name":"capture","arguments":{"document":{}}}}'
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "structuredContent": {
      "type": "epcisException:SecurityException",
      "title": "Unauthorised request",
      "status": 401,
      "detail": "/capture is a keyed door: send Authorization:
        Bearer <capture-scoped key>. Ask for one at /get-a-key/;
        we email you when it is ready."
    },
    "isError": true
  }
}

501 — the capability this door does not serve, refused typed. The apex resolve tool answers the same problem-document grammar — cut live from POST https://epcis.dev/mcp on 2026-07-31:

$ curl -sS -X POST https://epcis.dev/mcp \
    -H 'Content-Type: application/json' -d '{
      "jsonrpc":"2.0","id":4,"method":"tools/call",
      "params":{"name":"resolve",
        "arguments":{"reference":"urn:epc:id:sgtin:0614141.107346.2021"}}}'
{
  "jsonrpc": "2.0", "id": 4,
  "result": {
    "structuredContent": {
      "type": "epcisException:ImplementationException",
      "title": "resolve is not served on this door",
      "status": 501,
      "detail": "identity resolution is homed at id.org.ai — one
        resolver, one home; this door answers a typed refusal,
        never a fabricated resolution."
    },
    "isError": true
  }
}

400 with the collision nested — the capture rejection envelope. The capture door never answers a partial acceptance: a job that collides refuses whole as a ValidationException problem document, and ResourceAlreadyExistsException names each colliding event inside errors[]. Executed 2026-07-31 against epcis.dev@0.1.0 — the same document captured twice with different bytes under one eventID, over the stdio MCP door:

{
  "structuredContent": {
    "type": "epcisException:ValidationException",
    "title": "capture job rejected — no partial acceptance",
    "status": 400,
    "detail": "every EPCIS event in the payload is rejected;
      see errors[]",
    "captureID": "074b4c8a-4ebe-4561-8ca5-3859611f7f1c",
    "errors": [
      {
        "type": "epcisException:ResourceAlreadyExistsException",
        "title": "eventID urn:uuid:2f24f00d-0000-4000-8000-
          000000000001 was already captured with different
          content; events are append-only — submit a correction
          as a new event carrying an errorDeclaration",
        "status": 400,
        "instance": "/epcisBody/eventList/0/eventID"
      }
    ]
  },
  "isError": true
}

Match the envelope on type, then walk errors[] and match each entry on its own type — the collision is per event, the refusal is whole.

(The detail and long title strings above are wrapped for width; type, status and every other byte are verbatim from the real responses. Each full result also carries the same document as a content text block.)

A refusal is one of three honest answer shapes — the other two, the result and the truthful empty, are on the refusal grammar.