30817:nip-fe-nostr-over-http

NIP-FE: Nostr over HTTP

Vitor Pamplona

published
2026-09-27

Relay Commands over HTTP

A relay MAY answer single client commands over plain HTTP, beside its websocket. A request carries one command frame, exactly as a client would send it on the websocket, and gets back that command's answer, exactly as the relay would send it there. Nothing stays open after it: no live subscription, no connection state, no session to resume.

This serves clients that want an answer rather than a connection — scripts, serverless functions, crawlers, a page that renders one query — and any relay instance behind a load balancer can answer any request. A relay can serve it by feeding the body to its websocket handler as if it had arrived on a new socket.

Requests

A request is a POST to the relay's HTTP URL: the relay URL with ws:// read as http:// and wss:// as https://, host and path unchanged. Its body is one NIP-01 client frame: REQ, COUNT (NIP-45) or EVENT.

POST https://relay.example
["REQ","q1",{"kinds":[1],"limit":2}]

The subscription id is the client's to pick, as on the websocket. Relays SHOULD NOT require a Content-Type, so a browser can send these without a CORS preflight when it carries no Authorization. A request with Content-Type: application/nostr+json+rpc is a NIP-86 call, not a command.

A relay that serves this NIP lists FE in its NIP-11 supported_nips.

Answers

The response body is application/x-ndjson: one relay-to-client frame per line, exactly as the relay would send it on the websocket.

["EVENT","q1",{"id":"…","kind":1,…}]
["EVENT","q1",{"id":"…","kind":1,…}]
["EOSE","q1"]
command answer ends on
REQ EOSE, or CLOSED
COUNT COUNT, or CLOSED
EVENT OK

The answer ends on the frame in the table, or on a NOTICE, which means the command never ran. The relay then ends the response. A body that ends on anything else was cut off, and clients MUST treat what they got as incomplete: a REQ with its tail missing looks exactly like a short result.

A REQ is its websocket self with the live part removed: stored events up to EOSE, then the end of the response. Relays SHOULD write each frame as it is produced (chunked transfer encoding), so a client can act on the first event before the last one is found; clients MAY read the body incrementally.

Status

The status is decided by the first frame of the answer.

first frame status
EVENT, EOSE, COUNT, OK with true 200
OK with false and a duplicate: reason 200
a refusal (CLOSED, OK with false) prefixed auth-required: 401, with WWW-Authenticate: Nostr
… prefixed restricted: or blocked: 403
… prefixed rate-limited: 429, with Retry-After
… prefixed error: 500
… with any other prefix, or a NOTICE 400

A refusal is the whole body, one line. The relay may also refuse a request before running it, with one NOTICE line: 400 for a body that is not one REQ, COUNT or EVENT frame, 413 for one longer than its max_message_length (NIP-11), 429 (this client) or 503 (everyone) with Retry-After when it is too busy to take the request, and 503 when no first frame came within its own time limit.

Once a 200 has gone out the status cannot change, so a failure after that — the store failing mid-answer, the relay's time limit passing, a reader too slow to keep up — is a last CLOSED line in the body.

Authentication

Where a websocket client would send a NIP-42 AUTH, an HTTP client sends a NIP-98 Authorization: Nostr <base64 event> header with the request. The event's u tag is the relay's HTTP URL, its method tag is POST, and its payload tag is REQUIRED: the hex sha256 of the body. The relay treats the pubkey as authenticated for that one command, exactly as if it had sent a NIP-42 AUTH on the socket, and as nothing more.

Relays MUST reject a token whose payload does not match the body, and one whose created_at is more than 60 seconds away from their own clock. Within that window a token MAY be used again for the same body: relays need not remember the tokens they accepted, since sending the same body again only repeats a read or re-sends an event the relay already has, and a client can retry a request that failed on the network without signing a new token.

A relay reachable at more than one address (a .onion beside its clearnet name) accepts a u at any of them. An Authorization header in any other scheme (a proxy's Basic, an API gateway's Bearer) is not addressed to the relay and MUST be ignored rather than refused.

Browsers, proxies, compression

Relays SHOULD answer CORS preflights for the relay URL from any origin, allowing POST and the Authorization and Content-Type headers. No cookies are involved; credentials are the per-request NIP-98 token.

A relay that compresses a streamed answer SHOULD flush the compressor each time it flushes frames (a gzip sync flush), or compression holds back the lines streaming exists to deliver. A relay behind a buffering reverse proxy SHOULD disable its buffering for these responses (X-Accel-Buffering: no for nginx).

Discussion

Connect a key to comment.