← All posts
Spec CloakAPI · 2026-04-10 · 7 min read

OpenReceipt sub-tags: file-ext, streaming, router-decision

The signed receipt envelope was designed to be minimal by default: the core payload records what the gateway did — model, provider, token counts, chain hash — without embedding anything application-specific. Three new optional sub-tags extend that coverage for receipt consumers who need more context about input types, streaming behaviour, and routing decisions, without breaking any existing verifier.

Updated July 2026 This post describes the sub-tag mechanism as it shipped on the legacy v1 gateway-signed envelope. The concept is unchanged, but the current wire format is the client-signed OpenReceipt v3 envelope, where registered sub-tags live inside the 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:

Implementation note If you are building a receipt consumer that processes sub-tags, always use a presence check rather than a default assumption. Check "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.