1 Overview
Many HTTP services are built for Nostr and deal with Nostr user accounts. A media host, an API gateway, or a paid feed needs to know who sends each request. Passwords and API keys would add a second identity next to the user's Nostr key. This NIP removes that second identity.
The client signs a small ephemeral event of kind 27235 that names the exact URL and HTTP method of the request. The client encodes the event as base64 and puts it in the Authorization header. The server decodes the event, verifies it, and knows the caller's pubkey. No account creation, no session, no cookie.
The event travels inside the HTTP request. It normally never touches a relay. Because the kind sits in the ephemeral range of NIP-01, relays would not store it anyway.
The kind number 27235 is a wink at RFC 7235, the IETF standard for HTTP authentication. Do not confuse this NIP with NIP-42: NIP-42 authenticates a client to a relay over WebSocket. NIP-98 authenticates a client to an HTTP server.
Nostr scheme.2 The Auth Event
The auth event is a normal NIP-01 event with kind 27235. The content SHOULD be empty. Two tags are mandatory. One tag is conditional:
| Tag | Required | Rule |
|---|---|---|
| u | MUST | The absolute URL of the request, including query parameters. |
| method | MUST | The HTTP request method, for example GET or POST. |
| payload | SHOULD, when the request has a body | The sha256 hash of the request body as lowercase hex: ["payload", "<sha256-hex>"]. Use it for POST, PUT, and PATCH requests. |
{
"id": "fe964e758903360f28d8424d092da8494ed207cba823110be3a57dfe4b578734",
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"content": "",
"kind": 27235,
"created_at": 1682327852,
"tags": [
["u", "https://api.snort.social/api/v1/n5sp/list"],
["method", "GET"]
],
"sig": "5ed9d8ec958bc854f997bdc24ac337d005af372324747efe4a00e24f4c30437f…"
}
3 Server Validation
The server MUST perform these checks, in order, to validate the event:
- The
kindMUST be27235. - The
created_attimestamp MUST be within a reasonable time window. The suggestion is 60 seconds. - The
utag MUST be exactly the same as the absolute request URL, including query parameters. - The
methodtag MUST be the same HTTP method used for the requested resource.
If one of the checks fails, the server SHOULD respond with a 401 Unauthorized status code. When the request carries a payload tag, the server MAY hash the request body and compare it to the tag value. This validates that the caller authorized this exact payload. The server MAY also perform additional implementation-specific checks.
The u match is exact, byte for byte. One reordered query parameter, a trailing slash, or an http instead of https makes the check fail. Clients: build the event from the final URL you send. Servers behind proxies: reconstruct the URL the client actually used.
The ordered check list in the source never says to verify the event id or the Schnorr sig. It is implied by NIP-01, but a literal reading lets a forged header pass. Servers must verify the signature before they trust the pubkey. The source also gives no rule for URL normalization (case, port, encoding) — implementations must agree on the exact string.
4 Request Flow
The event travels in the Authorization HTTP header. The header MUST use the Authorization scheme Nostr. The value MUST be the base64-encoded JSON of the kind 27235 event. The whole header is one line:
Authorization: Nostr eyJpZCI6ImZlOTY0ZTc1ODkwMzM2MGYyOGQ4NDI0ZDA5MmRhODQ5NGVkMjA3Y2JhODIz… (base64 of the full event JSON, shortened here for print)
5 Worked Example
A client uploads a report to a Nostr-aware API with POST. The request has a body, so the client adds a payload tag with the sha256 hash of the body. Then it signs, encodes, and sends:
{
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"content": "",
"kind": 27235,
"created_at": 1682328000,
"tags": [
["u", "https://api.example.com/v1/reports?draft=false"],
["method", "POST"],
["payload", "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"]
],
"id": <sha256 of the serialized event>,
"sig": <Schnorr signature>
}
-- client sends the request -- → POST /v1/reports?draft=false HTTP/1.1 → Host: api.example.com → Authorization: Nostr eyJwdWJrZXkiOiI2M2ZlNjMxOGRjNTg1ODNjZmUxNjgxMGY4NmRk… → Content-Type: application/json → → {"title":"weekly report"} -- server decodes, verifies sig, runs the 4 checks, hashes the body -- ← HTTP/1.1 200 OK ← {"status":"stored","owner":"63fe6318…"} -- 90 seconds later the client replays the same header -- ← HTTP/1.1 401 Unauthorized ← {"error":"auth event expired"}
A reference implementation exists for C# ASP.NET: the NostrAuth.cs AuthenticationHandler gist by the NIP's author.
6 Version History
NIP-98 arrived in one piece in April 2023 and has changed little since. Notable changes, newest first:
- 20232023-12Fixed the Authorization header example string.
- 20232023-11Repo-wide cleanups: JSON reformatting and removal of the NIP author lines.
- 20232023-09Spelling fixes and clarifications to the validation wording.
- 20232023-05Typo fixes; the references section was retitled.
- 20232023-04Added the
payloadtag: a sha256 hash of the request body for methods that carry one. - Origin2023-04-24Kieran published the NIP: kind 27235, the
uandmethodtags, and theNostrAuthorization scheme.
Original Proposal - Credits
| Author | Kieran |
| First commit | 2023-04-24 — “NIP-98” |
| Status | draft - optional |
| Source | 98.md on GitHub - full change history |