1 Overview
A Nostr web client needs a private key to sign events. Pasting a raw private key into every website is dangerous. One malicious or buggy site can steal the key and impersonate the user forever.
NIP-07 solves this problem. A browser extension (or, in principle, the browser itself) injects an object named window.nostr into every page. The page calls methods on this object instead of handling the key directly. The extension holds the key, asks the user to approve each request, and signs on the page's behalf.
This turns the extension into a small, trusted signer that many different Nostr web apps can share. The user installs one extension, approves it once per site, and every NIP-01 compatible web client can publish signed events through it.
A web page must check that window.nostr exists before using it. Not every browser has an extension installed, and not every browser will ever support this capability.
NIP-07 is one of three ways to keep a private key out of an app. Compare it with NIP-46 (a remote signer reached over the network) and NIP-55 (a signer app on the same Android device). All three follow the same idea: the app asks, the signer decides.
2 The window.nostr Object
An extension that implements NIP-07 must define window.nostr with two required methods. It may also define four optional encryption methods, grouped under nip04 and nip44 namespaces. Every method is asynchronous and returns a Promise.
async window.nostr.getPublicKey(): string // returns the user's public key, as lowercase hex async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an unsigned event, adds id, pubkey, and sig, and returns the full event
3 Required Methods
| Method | Returns | Rule |
|---|---|---|
| getPublicKey() | string (64-char hex) | Returns the public key of the account the extension currently has selected. |
| signEvent(event) | Event | Takes an unsigned event object. Adds id, pubkey, and sig following the NIP-01 rules, then returns the complete event. |
The event argument to signEvent is an unsigned event. The page supplies only the fields it controls. The extension computes the rest.
| Field | Type | Supplied by |
|---|---|---|
| created_at | number, unix seconds | the page |
| kind | number | the page |
| tags | array of arrays of strings | the page |
| content | string | the page |
| pubkey | hex string | the extension, on return |
| id | hex string | the extension, on return |
| sig | hex string | the extension, on return |
A page must never construct id, pubkey, or sig itself and pass them into signEvent. The extension owns those three fields. See NIP-01 for the full event format and the id-serialization rule.
4 Optional Encryption Methods
Aside from the two required methods, an extension may also implement encryption helpers. These let a page encrypt and decrypt content for direct messages without the page seeing the shared secret.
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv, as specified in nip-04 (deprecated) async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv, as specified in nip-04 (deprecated) async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext, as specified in nip-44 async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext, as specified in nip-44
| Method | Returns | Rule |
|---|---|---|
| nip04.encrypt(pubkey, plaintext) | string | Encrypts plaintext for pubkey using the NIP-04 scheme. Deprecated; kept only for legacy content. |
| nip04.decrypt(pubkey, ciphertext) | string | Decrypts a NIP-04 payload from pubkey. Deprecated; kept only for legacy content. |
| nip44.encrypt(pubkey, plaintext) | string | Encrypts plaintext for pubkey using the current NIP-44 versioned scheme. |
| nip44.decrypt(pubkey, ciphertext) | string | Decrypts a NIP-44 payload from pubkey. |
NIP-04 is deprecated in favor of NIP-17. A page should prefer nip44 for new encrypted content. Keep nip04 calls only to read old messages.
Older revisions of this NIP also defined window.nostr.getRelays(), which returned the user's relay read/write policy. It was removed from the spec in 2025 without a replacement method. A page that needs a user's relay list should read the kind:10002 event defined in NIP-65 instead of relying on an extension method.
5 The Trust Boundary
The whole point of NIP-07 is a boundary. The page and the extension run in separate JavaScript contexts. The page can call methods; it cannot read the extension's memory or storage. The private key stays inside the extension at all times.
Recommendation to Extension Authors
A page may check for window.nostr as soon as it loads. If the extension injects its script too late, the page finds nothing and assumes no signer is installed.
Chromium and Firefox extension authors should set "run_at": "document_end" in the extension manifest. This makes sure window.nostr is ready before the page finishes loading.
This NIP does not define an implementation. A list of known browser extensions and other signers is kept at aljazceru/awesome-nostr on GitHub.
6 Worked Example
The code below runs in a web page. It checks for the extension, reads the public key, builds an unsigned kind-1 note, and asks the extension to sign it. A rejected or failed request throws, so the page wraps the call in try/catch.
if (window.nostr) { const pubkey = await window.nostr.getPublicKey(); const unsignedEvent = { created_at: Math.floor(Date.now() / 1000), kind: 1, tags: [], content: "Signed in the browser. The key never left the extension." }; try { const signedEvent = await window.nostr.signEvent(unsignedEvent); // signedEvent now has id, pubkey, and sig - see NIP-01 relaySocket.send(JSON.stringify(["EVENT", signedEvent])); } catch (err) { // the user clicked Reject, or the extension is locked console.log("Signing request refused:", err); } } else { // no NIP-07 extension found - fall back to another signer, e.g. NIP-46 }
7 Version History
NIP-07 has changed mostly by adding and later trimming its optional methods. Notable changes, newest first:
- 20252025-02Removed
getRelays()from the required interface (#1779). No replacement method was defined; use NIP-65. - 20242024-07Added the recommendation for extension authors to load scripts with
"run_at": "document_end". - 20242024-02Added
nip44.encryptandnip44.decryptto the optional methods. - 20232023-12NIP-44 version 2 was published;
nip44calls were briefly added to this NIP, then reverted until the encryption spec stabilized. - 20232023-10Clarified the type of the unsigned event object passed into
signEvent. - 20232023-01Extensions were required to also add
idandpubkeywhen signing, not onlysig. - 20222022-12Added
nip04.encryptandnip04.decryptand marked all extra methods optional. - 2022-2023variousMany community extensions (Alby, nos2x-fox, Blockcore, Flamingo, Horse, and others) were added to the implementations list.
- Origin2022-05-06fiatjaf published NIP-07 with
getPublicKeyandsignEvent.
Original Proposal - Credits
| Author | fiatjaf |
| First commit | 2022-05-06 — “publish NIP-07: window.nostr.” |
| Status | draft - optional |
| Source | 07.md on GitHub - full change history |
8 See Also
- NIP-01 — the event format that
signEventproduces and the id/sig rules the extension follows. - NIP-04 — the deprecated encryption scheme behind
nip04.encrypt/nip04.decrypt. - NIP-44 — the current encryption scheme behind
nip44.encrypt/nip44.decrypt. - NIP-46 — a remote signer reached over the network, for when no browser extension is available.
- NIP-55 — the same idea as this NIP, but for Android apps instead of web pages.