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>; keys are provisioned from the /get-a-key/ list, in order
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:ResourceAlreadyExistsException409a capture that collides with a stored event of the same eventID but different contenta byte-identical replay is a benign ack; different bytes under the same identity are refused whole
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

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

Three real refusals, cut live

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>. Keys are provisioned from
        the /get-a-key/ list, in order — one email when yours
        is ready."
    },
    "isError": true
  }
}

(The detail strings above are wrapped for width; type, status and every other byte are verbatim from the live responses. The 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.

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.