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
| type | status | meaning | remediation |
|---|---|---|---|
| epcisException:ValidationException | 400 | malformed JSON, a document the pinned schema rejects, or a failed translation | fix the payload; the per-path errors array names every violation |
| epcisException:QueryParameterException | 400 | a SimpleEventQuery the pinned query-schema rejects | fix the named parameter; CBV values are bare words (shipping, not a URN) |
| epcisException:SecurityException | 401 | a keyed door reached without a key | send Authorization: Bearer <capture-scoped key>; keys are provisioned from the /get-a-key/ list, in order |
| epcisException:NoSuchResourceException | 404 | an eventID that is not in the caller's scope — absent, never redacted | nothing to retry; an out-of-scope event and a nonexistent one answer identically |
| epcisException:ResourceAlreadyExistsException | 409 | a capture that collides with a stored event of the same eventID but different content | a byte-identical replay is a benign ack; different bytes under the same identity are refused whole |
| epcisException:CaptureLimitExceededException | 413 | payload exceeds the file-size limit | split the document |
| epcisException:UnsupportedMediaTypeException | 415 | the right payload at the wrong door | the 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.