Embed zaps on your website

This guide shows you how to add a Nostr zap button to your own website so visitors can send you zaps. Everything runs in the browser.

Step 0 - Make sure you can receive zaps

Before you embed anything, your Nostr profile must be “zap-ready”. If the zap button cannot find a Lightning address / LNURL in your profile metadata, it can’t generate an invoice.

What you need: a Lightning address (often stored as lud16) or an LNURL (often stored as lud06) in your Nostr profile metadata (kind 0). Most Nostr clients provide a field like “Lightning address” or “Zap address”.
🧠

Mental model (quick)

  • Zap button → looks up your profile, finds your zap endpoint, requests an invoice, then lets the user pay (WebLN or QR).
  • Viewer button → reads zap receipts from relays and shows them in a dialog.
🧩

Step 1 - Include the bundle

Add this near the end of your page (before </body>). We recommend using the full URL so it works anywhere.

<script src="https://zap.nostr.buzz/dist/nostr-zap.js"></script>
🔘

Step 2 - Add a Zap button

Add a button with data-npub to target a profile. Optionally provide data-relays and data-theme.

<button
  data-npub="npub1..."
  data-relays="wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol"
  data-theme="dark">
  ⚡️ Send a zap
</button>
👀

Step 3 - Add a “View zaps” button

The library auto-wires any button[data-nzv-id] on click and opens a dialog. Use it to show zaps for a profile, a note, or an addressable event.

<button
  data-nzv-id="npub1..."
  data-relay-urls="wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol"
  data-title=""
  data-zap-color-mode="true">
  👀 View zaps
</button>

🧷 Viewer attributes

🎨

Theming

You can force a theme per button: data-theme="light" or data-theme="dark". If you don’t set it, the dialogs follow prefers-color-scheme.

<button data-npub="npub1..." data-theme="light">🌞 Light zap</button>
<button data-npub="npub1..." data-theme="dark">🌙 Dark zap</button>

<button
  data-nzv-id="npub1..."
  data-relay-urls="wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol"
  data-theme="dark">
  🌙 View zaps
</button>
🧰

JavaScript API

After you include the bundle, the library is available as window.nostrZap. In most cases you don’t need to call anything manually.

🧾 Legacy zap buttons

Older embeds remain compatible. Common attributes: data-npub, data-note-id, and data-relays.

// Low-level API object:
// window.nostrZap.nostrZap.init({ npub, noteId?, naddr?, relays?, buttonColor?, anon? })

// Convenience helpers:
await window.nostrZap.zapInit({
  npub: "npub1...",
  relays: "wss://relay.damus.io,wss://nos.lol",
});

window.nostrZap.zapInitTargets();

🔒 Zapwall (paywalled content)

Zapwall lets you gate content per-item and unlock it after a successful zap. It supports two modes: blur mode (easy, not secret) and encrypted payload mode (no plaintext shipped in HTML).

Important security note: With a single static HTML file and no external key delivery, you cannot automatically give a secret only to payers. Encrypted mode is secure only if the decryption key is provided via an external secure channel (server, Nostr DM bot, manual delivery, etc.).

1) Quick start (blur mode)

In blur mode, the content is present in HTML but visually gated until a zap succeeds. This is good for UX, but it is not “secret” (view-source can still show it).

<section
  data-zapwall
  data-zapwall-id="post-123"
  data-title="Premium post"
  data-amount="21"
  data-npub="npub1..."
  data-relays="wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol">

  <div data-zapwall-content>
    <h2>Premium content</h2>
    <p>This becomes visible after a successful zap.</p>
  </div>
</section>

2) Secure mode (encrypted payload, no plaintext in HTML)

In encrypted mode, you do not ship the plaintext in the DOM. Instead you ship an encrypted payload and decrypt it only after: (a) the zap is successful, and (b) a decryption key is provided.

Payload format (embedded as JSON):
  • v: version (currently 1)
  • alg: AES-256-GCM
  • iv: base64 IV (12+ bytes)
  • ct: base64 ciphertext
  • format: html or text
  • keyHint (optional): a human-friendly hint shown in the key prompt
<section
  data-zapwall
  data-zapwall-id="post-locked-1"
  data-title="Premium post"
  data-amount="21"
  data-npub="npub1..."
  data-relays="wss://relay.damus.io,wss://relay.primal.net,wss://nos.lol"
  data-zapwall-key-label="Enter your key">

  <!-- empty container (plaintext is NOT present in HTML) -->
  <div data-zapwall-content></div>

  <!-- encrypted payload JSON -->
  <script type="application/json" data-zapwall-payload>
    {"v":1,"alg":"AES-256-GCM","iv":"...","ct":"...","format":"html","keyHint":"provided after payment"}
  </script>
</section>

3) Providing the decryption key

For encrypted mode, the library needs a 32-byte key (AES-256). You can provide it in either: 64-hex or base64.

Recommended approach: implement a key provider function. The provider can check whatever you want (zap receipt, DM, etc.) and return the key.

// Option A: global provider
window.nostrZapZapwallKeyProvider = async ({ wall, zap }) => {
  // wall.key / wall.amountSats / wall.noteId / wall.naddr / wall.npub ...
  // zap.invoice / zap.amountSats / zap.preimage (wallet-dependent) ...
  // Return 32-byte key as 64-hex or base64.
  return null;
};

// Option B: API (if you import the module)
// window.nostrZap.setZapWallKeyProvider(async ({ wall, zap }) => "...key...");

4) Encrypting content offline

This repo includes an offline helper script to generate the encrypted payload. It outputs payload + a random key. You must deliver the key to paying users via a secure channel.

# Example (run locally)
node scripts/zapwall-encrypt.mjs ./premium.html --format html --key-hint "Delivered after payment"

# Output JSON contains:
# - payload: { v, alg, iv, ct, format, keyHint? }
# - key: 64-hex AES key (keep this secret!)
📡

Relays (recommended)

Your buttons should use multiple relays. For best results, use a mix of reliable read+write relays.

Zap flow: uses data-relays (string).
Viewer: uses data-relay-urls (comma-separated list).
🛠️

Troubleshooting