ohttp.io
Documentation

Relay integration
in three steps.

Everything you need to route Oblivious HTTP traffic through ohttp.io, from key discovery to production hardening.


Quickstart

The OHTTP promise in one sentence: a relay knows who you are but never what you're doing; a gateway knows what you're doing but never who you are. The guarantee holds because the two roles are run by different companies, and an OHTTP exchange has three moving parts: your clients, the ohttp.io relay, and an Oblivious Gateway Resource (yours, or a partner's). The client never talks to the gateway directly, that's the whole point.

1. Create a relay endpoint

Sign up and register the gateway URI you want to relay to. You'll receive a dedicated relay URL:

your relay endpoint
https://relay.ohttp.io/<your-relay-id>

2. Publish the gateway key configuration

Clients need the gateway's HPKE key configuration before they can encapsulate anything. Gateways serve it as application/ohttp-keys; clients fetch it out of band or via the well-known URI.

key discovery
curl -H "Accept: application/ohttp-keys" \
  https://gateway.example.com/.well-known/ohttp-gateway

3. Encapsulate and POST

The client encodes its request as Binary HTTP (RFC 9292), encrypts it with HPKE (RFC 9180) to the gateway's public key, and POSTs the result to the relay. The relay forwards it and returns the encapsulated response.

encapsulated request
curl -X POST https://relay.ohttp.io/<your-relay-id> \
  -H "Content-Type: message/ohttp-req" \
  --data-binary @encapsulated-request.bin \
  -o encapsulated-response.bin
That's it. There is no relay-side SDK, no per-request authentication on the hot path, and no plaintext anywhere between client and gateway.

Relay endpoints

The relay surface is deliberately tiny, one resource, one method.

PropertyValue
MethodPOST only. Anything else receives 405.
Request typeContent-Type: message/ohttp-req - an HPKE-encapsulated Binary HTTP request.
Response typeContent-Type: message/ohttp-res - the gateway's encapsulated response, streamed back unmodified.
ProtocolsHTTPS only (RFC 9458 requires HTTPS on both hops). HTTP/1.1 and HTTP/2 on all plans; HTTP/3 (QUIC) from the Scaling tier up.
Errors400 malformed or empty content · 413 oversized · 429 rate limited · 502/504 gateway unreachable or timed out.
Rate limitsCommunity 2 req/s (no bursting) · Starter 25 req/s · Scaling 150 req/s · Pro 1,000 req/s. Paid plans can burst to 2× per region for short spikes. Enterprise rates are custom.
Max request sizeCommunity 1 MB · Starter 50 MB · Scaling 100 MB · Pro unlimited (fair use). Enterprise limits are custom. Bodies above the cap are rejected with 413.
Bandwidth / monthCommunity 50 GB · Starter 150 GB · Scaling 500 GB · Pro 4 TB. Enterprise custom. Overage billed at $0.05/GB.

Relays may reject obviously invalid requests (RFC 9458 §5), but a relay can never generate an encapsulated response itself, it has no keys. Error responses from the relay are always plain HTTP and never reveal anything about encapsulated content.

Key configuration

A gateway's key configuration tells clients how to encrypt. It is serialized as application/ohttp-keys and contains:

FieldSizePurpose
Key ID8 bitsIdentifies which gateway key the client selected.
KEM ID16 bitsKey encapsulation mechanism, e.g. DHKEM(X25519, HKDF-SHA256).
Public keyvariableThe gateway's HPKE public key.
Symmetric algorithmsvariableSupported KDF/AEAD pairs, e.g. HKDF-SHA256 + AES-128-GCM.

Clients must treat key configurations as integrity-protected and attributable to the gateway (RFC 9458 §6.1). Gateways should rotate keys regularly, OHTTP has no forward secrecy over the lifetime of a key configuration, so rotation bounds the exposure window.

Chunked OHTTP

Classic OHTTP buffers the entire request and response before encryption - fine for telemetry, painful for anything that streams. Chunked Oblivious HTTP (draft-ietf-ohai-chunked-ohttp) encrypts messages incrementally, so responses can flow as they're produced. ohttp.io accepts the chunked media types from the Starter tier up.

Media typeDirection
message/ohttp-chunked-reqClient → relay → gateway, streamed request chunks.
message/ohttp-chunked-resGateway → relay → client, streamed response chunks.

This is the right transport for:

  • Anonymous LLM inference - tokens stream to the user as they're generated, with no identity attached to the prompt.
  • Agent tool calls - autonomous agents can invoke external tools without revealing which principal they act for.
  • Large or long-lived responses - map tiles, dataset slices, media range requests.
Compatibility note. Clients negotiate chunked mode via the gateway's key configuration. Gateways that only support classic OHTTP continue to work unchanged, both media types traverse the same relay endpoint.

Privacy Pass

Rate limiting an anonymous service is a paradox: the usual tool is IP tracking, which is exactly what OHTTP exists to eliminate. Privacy Pass (RFC 9576–9578) resolves it with anonymous tokens, clients prove they're rate-limited participants without revealing who they are.

ohttp.io operates the issuance and redemption for you:

  • Issuance - clients obtain unlinkable tokens through an attestation flow you configure (device attestation, account-backed, or anonymous credits).
  • Redemption - tokens accompany encapsulated requests; the relay enforces per-token budgets without IPs, accounts, or CAPTCHAs.
  • Anti-abuse without surveillance - abuse is bounded by token economics, not by watching users.

Privacy Pass support is included from the Scaling tier up; full issuance, where ohttp.io operates the issuer for you, is part of the Pro tier. The standards involved: RFC 9576 (architecture), RFC 9577 (HTTP authentication scheme), RFC 9578 (issuance protocols).

Client libraries

ohttp.io interoperates with any RFC 9458-conformant implementation. Popular open-source options:

  • Rust - the ohttp crate, used in production by privacy infrastructure like Divvi Up.
  • Go - ohttp-go, plus relays like pog if you self-host for testing.
  • JavaScript / TypeScript - ohttp-js for browser and Node clients.
  • C++ - ohttp-gp for Chromium-derived networking stacks.

Configure the client with two URIs, the gateway's key config and your ohttp.io relay:

rust · janus-style configuration
let ohttp_config = OhttpConfig {
    key_configs: Url::parse("https://gateway.example.com/ohttp-keys")?,
    relay:       Url::parse("https://relay.ohttp.io/<your-relay-id>")?,
};
Testing without a gateway? Point the client at a gateway URI directly as the relay to exercise encapsulation end to end, but note this provides no unlinkability, since the client then talks to the gateway itself.

Gateway-in-a-box

RFC 9458 requires the relay and gateway to be run by different entities - so we can't run your gateway for you. What we can do is make running one yourself an afternoon's work instead of a quarter's:

  • Cloudflare Worker gateway template - a deployable Oblivious Gateway Resource with HPKE decapsulation, key configuration serving, and chunked support. Fork, point at your backend, deploy.
  • Terraform module - provisions the gateway, its key rotation schedule, and the DNS records (including RFC 9540 service-binding) in one apply.
  • Key-rotation runbooks - tested procedures for rotating HPKE key configurations without breaking in-flight clients, including overlap windows and rollback.
  • Auto provider compliance - on Scaling and Pro we continuously verify that your configured gateway resolves and routes on a network independent from ours. If a gateway is moved onto infrastructure we also run on (for example the same cloud or edge provider), we alert you, or refuse the mapping, because relay and gateway on one network silently erodes unlinkability.
  • Key-config consistency checker - verifies every client sees the same key configuration, as RFC 9458 §6.1 requires, across regions and over time.

Your gateway stays in your account, under your keys, operated by your team. The independence the protocol depends on is preserved, we just remove the excuse for not deploying it.

Relay guarantees

RFC 9458 §6.2 defines what a conformant relay must and must not do. These are our operating commitments, not aspirations:

  • No plaintext access. The relay handles HPKE ciphertext only. It cannot read methods, paths, headers, or bodies inside the encapsulation.
  • Zero logging. We retain nothing beyond billing records. Client IP addresses, TLS connection metadata, timing, and ciphertext bodies are discarded the moment a request completes.
  • No metadata injection. We never add Forwarded, Via, or any client-identifying fields when forwarding to the gateway.
  • Unknown fields dropped. Header fields the client adds beyond the required content type are removed before forwarding, per §6.2.
  • No collusion. ohttp.io is legally unconnected to every other relay and gateway operator, no shared ownership, partnerships, or data agreements.
  • One-to-one mapping. Each relay endpoint forwards to exactly one configured gateway, as the protocol requires.
  • GDPR, by architecture. We never see request payloads and retain nothing beyond billing records, so most processing obligations fall to you as controller. What little we do process is set out in our privacy policy.

What transiently passes through memory: your client's IP address, TLS connection metadata, timing, and ciphertext sizes, the half of the picture the gateway never sees. Unlinkability holds because relay and gateway are operated independently, and ohttp.io enforces that independence two ways: we are legally unconnected to other operators, and no customer may use us for both ends of a flow.

DNS discovery

Services can advertise OHTTP support with an HTTPS DNS resource record (RFC 9540). The ohttp SvcParamKey signals that a service is reachable via Oblivious HTTP:

dns · service binding
service.example.com. 7200 IN HTTPS 1 . ( alpn=h2 ohttp )

Clients resolving this record learn to fetch the key configuration from /.well-known/ohttp-gateway on the same origin, then route encapsulated requests through a pre-configured relay, yours at ohttp.io.

Migration guide

Moving to ohttp.io from another hosted relay, or from a self-hosted one - is a configuration change, not a code change. OHTTP is a standard; clients don't care which relay forwards their ciphertext.

From another hosted relay

  1. Create your ohttp.io relay endpoint pointing at your existing gateway URI.
  2. Update the relay URL in your client configuration (one string in most libraries).
  3. Run both relays in parallel during your deploy window, the gateway sees no difference.
  4. Decommission the old relay once client rollout completes.

From a self-hosted relay

  1. Same steps as above, plus you get to delete a service, an on-call rotation, and a TLS certificate from your life.
No gateway changes needed. Your gateway's key configuration, inner request handling, and response encapsulation are untouched. Only the relay URL clients use changes.

Frequently asked questions

Can ohttp.io read the requests it relays?

No. Requests are HPKE-encrypted to the gateway's keys before they reach us. We see ciphertext, client IP addresses, and timing, never methods, paths, headers, or bodies.

Why does the relay need to be independent from the gateway?

RFC 9458 §6 is explicit: if one entity runs both relay and gateway for the same traffic, it can combine client network identity with request content and the unlinkability property collapses. ohttp.io enforces this commercially, a customer may use us for one side of a flow, never both, and we are legally unconnected to every other relay and gateway operator.

What does ohttp.io log?

Nothing beyond billing records. Client IP addresses, connection metadata, timing, and ciphertext bodies exist only in memory for the duration of a request and are discarded immediately after. Aggregate byte and request counts for billing are the only records we keep.

Does OHTTP provide forward secrecy?

Not over the lifetime of a key configuration. If a gateway's private key is compromised, recorded ciphertext under that key can be recovered. Regular key rotation limits the window; TLS protects each hop in transit.

What is OHTTP good for?

Stateless, moderately sensitive requests: telemetry and crash reporting, DNS queries (Oblivious DoH), safe-browsing lookups, anonymous surveys, map tile fetches, anywhere linking a request to an identity would teach the server something about a user.

What does OHTTP not protect?

Content the client itself puts in the request. If the body carries a user ID or auth token, the gateway can correlate regardless of transport privacy. OHTTP hides who sent the request, not what was sent.

When should I use chunked OHTTP instead of classic?

Whenever the response benefits from streaming, LLM inference, agent tool calls, large payloads, or when request/response sizes are unknown upfront. Classic single-shot OHTTP remains ideal for small, stateless requests like telemetry pings and DNS queries.

How do you rate-limit without tracking IPs?

With Privacy Pass tokens (RFC 9576–9578). Clients present unlinkable anonymous tokens with their requests; we enforce per-token budgets. Abuse is bounded without IP logs, accounts, or CAPTCHAs.

Ready when you are

Spin up your relay endpoint.

Free tier included. Point your first client at ohttp.io in minutes.