claims object (spec §4.3) rather than a top-level payload.subtags. The v1 examples below still verify — v1/v2 receipts verify forever — but check the spec for the authoritative v3 sub-tag registry.
Why sub-tags exist
The core OpenReceipt payload is deliberately narrow. Keeping it narrow means every verifier — regardless of how it was built — can parse and verify any receipt. Extending the payload with required fields would break older verifiers that do not understand new fields.
Sub-tags solve this problem. They live in an optional subtags object within the payload. A verifier that does not understand sub-tags ignores the subtags key; the signature covers the entire payload including sub-tags, so the verification still succeeds. A verifier that does understand sub-tags can read them for additional context.
The sub-tags are included in the JCS-canonicalised payload before signing, so their contents are tamper-evident. If an attacker changes a sub-tag value, the signature check fails.
The three new sub-tags
file_ext
Type: string | null. The declared MIME type or file extension of the primary input to the request, when that input is a file or binary blob rather than a plain-text prompt.
This sub-tag is populated when the client sends a multipart or binary request and explicitly declares the input type in the X-CloakAPI-Input-Type request header. If the header is absent, file_ext is omitted from the sub-tags object entirely (not set to null — the key is absent).
Allowed values follow the pattern of RFC 6838 media type subtypes: "pdf", "docx", "png", "mp3", "csv", and so on. The gateway does not validate that the declared type matches the actual binary content — that is the client application's responsibility. The receipt records what was declared, not what was detected.
streaming
Type: boolean. Set to true if the upstream AI provider returned the response via server-sent events (SSE), false if it returned a complete JSON response body.
Streaming responses have implications for receipt completeness: when streaming, the gateway records the receipt at first-chunk time using the token counts from the provider's usage field in the final chunk. If the provider does not include a usage field in the final SSE chunk (some providers omit it for streaming responses), the receipt will show token_out: null in the core payload, and "streaming": true in sub-tags explains why.
This sub-tag is always present when the gateway can determine streaming status. On non-streaming requests, it is set to false, not omitted.
router_decision
Type: string | null. The identifier of the routing rule that selected the upstream provider for this request. CloakAPI's router arbitrates among multiple AI providers based on configurable rules — cost, latency, capability, geographic placement. When a rule fires, its identifier is recorded here.
Rule identifiers are strings defined in your routing configuration, for example "latency-eu-first", "cost-opt-gpt4o", or "capability-vision-required". If no custom routing rule fired and the default route was used, this field contains the string "default". If routing was bypassed (e.g. because the client specified a model explicitly and routing was disabled for that tenant), this field is omitted.
This sub-tag is useful for cost auditing: it lets you verify after the fact which routing rule was responsible for each upstream selection, and whether the routing configuration was behaving as expected during a given time window.
JSON position in the envelope
In the legacy v1 envelope, sub-tags live inside the payload object under the key subtags. Here is a complete v1 example with all three sub-tags present (the v3 equivalent nests these under claims):
{
"ver": "openreceipt/1",
"kid": "2026-Q1",
"payload": {
"req_id": "01HZ9QK4WMFVG3C8XSDQVT7R4P",
"tenant_id": "t_acme",
"ts": "2026-04-10T09:17:44.201Z",
"model": "gpt-4o",
"provider": "openai",
"token_in": 2048,
"token_out": null,
"token_cached": 512,
"latency_ms": 1203,
"prev_hash": "sha256:a3f7e2b1...(64 hex chars)...",
"subtags": {
"file_ext": "pdf",
"streaming": true,
"router_decision": "cost-opt-gpt4o"
}
},
"sig": "MEUCIQDx...base64url DER signature..."
}
The canonical position of subtags within payload is: after all core fields, as the last key. JCS sorts keys lexicographically, so in canonicalised form subtags appears after token_in, token_out, token_cached, and before nothing (it is last). Verifiers that reconstruct the canonical form from scratch will arrive at the same ordering automatically if they use any compliant JCS implementation.
Backwards compatibility
The backwards compatibility guarantee is straightforward:
- A verifier built against the original OpenReceipt spec (before sub-tags) will receive receipts where
subtagsmay or may not be present. If the verifier's JCS implementation processes the payload as an opaque JSON object, it will includesubtagsin the canonical form and the signature will verify correctly — the verifier does not need to know whatsubtagsmeans. - Receipts that do not include sub-tags (either because the feature is not enabled or because the request predates the feature) will simply have no
subtagskey. The absence of the key is not an error. Verifiers MUST NOT requiresubtagsto be present. - On the v1 envelope the
verfield stayed"openreceipt/1"— sub-tags were a backwards-compatible extension within v1, not a version bump. The later move to the client-signed"v":"v3"envelope (see the note at the top) was a deliberate breaking change for the two-party signing model, not for sub-tags; in v3 the same registered sub-tags live underclaims.
"subtags" in payload before accessing any sub-tag field, and check each sub-tag field individually, as individual sub-tags may be absent even when the subtags object is present.
Enabling sub-tags
Sub-tags are optional and off by default, to keep receipts minimal for consumers who do not need the additional fields. A producer includes a sub-tag when the underlying data is available for a request; enabling one affects subsequent receipts only and never rewrites historical ones.
If you are building an integration that depends on sub-tags, verify they are present before going to production — receipts without the expected sub-tags are not a bug, they indicate the producer did not emit that field.
Worked examples: receipts without sub-tags
A minimal legacy-v1 receipt with no sub-tags — still fully valid:
{
"ver": "openreceipt/1",
"kid": "2026-Q1",
"payload": {
"req_id": "01HZ9QK5BNZP2MK9RTDQWU8S6Q",
"tenant_id": "t_acme",
"ts": "2026-04-10T09:22:11.889Z",
"model": "claude-3-5-sonnet-20241022",
"provider": "anthropic",
"token_in": 310,
"token_out": 88,
"token_cached": 0,
"latency_ms": 541,
"prev_hash": "sha256:c4d8f1a2...(64 hex chars)..."
},
"sig": "MEQCIBzW...base64url DER signature..."
}
The Python reference verifier handles both forms identically. The only difference is what information is available to downstream receipt consumers. If your audit tooling reads router_decision to produce routing analytics, it will simply see no data for receipts where the field is absent — which is the correct behaviour for an optional field.