Skip to main content

Mini App Integration Guide

What is a Fedi Mini App?

A Fedi Mini App is any website that runs inside Fedi's built-in browser. When a user opens your site in Fedi, your app gains access to capabilities that aren't available in a normal browser:

  • Lightning payments — Your app can request payments from the user or ask them to create invoices, all through their Fedi wallet. No payment processors, no credit cards, no sign-up flows. The user taps "Approve" and the payment connects directly with their Fedi wallet.
  • Identity and signing — Your app can identify the user via their Nostr pubkey and request them to cryptographically sign data. This gives you verifiable identity without passwords or accounts.
  • User context — Your app can read the user's preferred currency and language so you can match their Fedi experience.

From the user's perspective, opening a mini app feels like opening a page in a browser — except the page can interact with their wallet and identity. Every sensitive action (paying, signing, generating ecash) shows a confirmation screen where the user reviews the details and chooses to approve or deny. The user is always in control.

You don't need to install anything special. A mini app is just a website. Any tech stack works — React, Vue, Svelte, plain HTML/JS. There are no SDKs to install and no libraries to import. Fedi makes the capabilities available automatically when your site loads inside the app.

Quick Start

When your page loads inside Fedi, three JavaScript objects are available on window that give your code access to the capabilities described above:

ObjectWhat it does
window.weblnSend and receive Lightning payments through the user's wallet
window.nostrGet the user's public key, sign Nostr events, encrypt/decrypt messages
window.fediGenerate/receive ecash, get user info, currency, language, and more

Here's what it looks like in code:

// Ask the user to create a Lightning invoice for 1000 sats
await window.webln.enable()
const { paymentRequest } = await window.webln.makeInvoice({ amount: 1000 })

// Get the user's Nostr public key and sign an event
const pubkey = await window.nostr.getPublicKey()
const signed = await window.nostr.signEvent(myEvent)

// Generate ecash from the user's balance
const { notes } = await window.fedi.generateEcash({ amount: 5000 })

Every call that involves money or signing will show the user a confirmation screen. If they approve, you get the result. If they deny, you get an error. The rest of this guide covers each API in detail, including how to handle approvals, denials, and errors.


WebLN — Lightning Payments

WebLN lets your mini app send and receive Lightning payments through the user's Fedi wallet. Fedi implements the WebLN specification.

Enabling WebLN

You should call enable() before using any other WebLN method. This is a no-op in Fedi but is required by the WebLN spec and ensures compatibility with other providers.

await window.webln.enable()

Receiving Payments — makeInvoice

Request that the user creates a Lightning invoice. Fedi shows a confirmation overlay where the user can review and approve the invoice.

const response = await window.webln.makeInvoice({
amount: 1000, // Amount in sats
defaultMemo: "Coffee" // Optional description
})

console.log(response.paymentRequest) // BOLT11 invoice string

Parameters:

makeInvoice accepts a string, number, or RequestInvoiceArgs object:

FieldTypeDescription
amountnumberRequested amount in sats
defaultMemostringOptional invoice description
minimumAmountnumberOptional minimum amount the user can set
maximumAmountnumberOptional maximum amount the user can set

Passing a plain number or string is shorthand for { amount: value }.

Response:

FieldTypeDescription
paymentRequeststringThe BOLT11 invoice
rHashstringThe payment hash

Sending Payments — sendPayment

Request that the user pays a BOLT11 invoice. Fedi decodes the invoice and shows a confirmation overlay with the amount and description.

await window.webln.enable()

const invoice = "lnbc10u1p..." // BOLT11 invoice string

const response = await window.webln.sendPayment(invoice)

console.log(response.preimage) // Payment preimage (proof of payment)

Parameters:

FieldTypeDescription
paymentRequeststringA valid BOLT11 invoice

Response:

FieldTypeDescription
preimagestringThe payment preimage (proof of payment)

Getting Node Info — getInfo

Returns basic information about the user's Lightning node.

await window.webln.enable()

const info = await window.webln.getInfo()

console.log(info.node.alias) // User's display name

Response:

FieldTypeDescription
node.aliasstringThe user's display name
node.pubkeystringCurrently returns empty string

Unsupported Methods

The following WebLN methods are defined but not currently supported in Fedi. Calling them will throw an UnsupportedMethodError:

  • signMessage(message) — Signing arbitrary messages
  • verifyMessage(signature, message) — Verifying signed messages
  • keysend(args) — Keysend payments

Full Example: Pay-per-Action

<button id="pay-btn">Pay 100 sats</button>
<p id="status"></p>

<script>
document.getElementById("pay-btn").addEventListener("click", async () => {
const status = document.getElementById("status")

try {
await window.webln.enable()

// Create an invoice on your server, then have the user pay it
const invoice = await fetch("/api/create-invoice", {
method: "POST",
body: JSON.stringify({ amount: 100, memo: "Premium content" }),
}).then((r) => r.json())

const { preimage } = await window.webln.sendPayment(invoice.bolt11)

status.textContent = "Payment successful!"
// Verify preimage on your server to unlock content
await fetch("/api/verify-payment", {
method: "POST",
body: JSON.stringify({ preimage }),
})
} catch (err) {
status.textContent = `Payment failed: ${err.message}`
}
})
</script>

Nostr (NIP-07) — Key Management and Event Signing

Fedi implements the NIP-07 browser extension interface on window.nostr. This lets your mini app request the user's Nostr public key and sign events without ever accessing the user's private key.

Getting the Public Key

const pubkey = await window.nostr.getPublicKey()

console.log(pubkey) // Hex-encoded public key

Returns the user's Nostr public key as a hex string.

Signing Events

Request that the user signs a Nostr event. Fedi shows a confirmation overlay where the user can review and approve the event before signing.

const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: "Hello from my Fedi mini app!",
}

const signedEvent = await window.nostr.signEvent(event)

console.log(signedEvent.id) // Event ID (hash)
console.log(signedEvent.pubkey) // Signer's public key
console.log(signedEvent.sig) // Schnorr signature

Input (UnsignedNostrEvent):

FieldTypeDescription
kindnumberEvent kind (1 = text note, etc.)
created_atnumberUnix timestamp in seconds
tagsstring[][]Array of tag arrays
contentstringEvent content

Response (SignedNostrEvent):

Returns the original event fields plus:

FieldTypeDescription
idstringEvent ID (SHA256 hash)
pubkeystringSigner's hex public key
sigstringSchnorr signature

Encryption and Decryption

Fedi supports both NIP-04 (legacy) and NIP-44 (modern) encryption.

NIP-44 is recommended for new integrations. NIP-04 is provided for backward compatibility with older Nostr clients and relays.

We strongly recommend you use NIP-44 since NIP-04 is NOT considered secure for production usage.

const recipientPubkey = "ab12cd34..."

// Encrypt
const ciphertext = await window.nostr.nip44.encrypt(recipientPubkey, "secret message")

// Decrypt
const plaintext = await window.nostr.nip44.decrypt(senderPubkey, ciphertext)

Full Example: Publish a Nostr Note

async function publishNote(content) {
const pubkey = await window.nostr.getPublicKey()

const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content,
}

const signedEvent = await window.nostr.signEvent(event)

// Publish to a relay
const relay = new WebSocket("wss://relay.damus.io")

relay.onopen = () => {
relay.send(JSON.stringify(["EVENT", signedEvent]))
}
}

Fedi API

The window.fedi object provides APIs for functionality specific to Fedi or Fedimint such as ecash, chat, & mini apps.

App Context

Get Currency and Language

const currency = await window.fedi.getCurrencyCode() // e.g. "USD", "EUR", "BTC"
const language = await window.fedi.getLanguageCode() // e.g. "en", "es"

Use these to localize your mini app to match the user's Fedi settings.


Permissions and User Approval

Every API available to mini apps is gated by a permission system. Before your mini app can use any WebLN, Nostr, or Fedi API, the user must grant your app permission to do so. When your app calls a method for the first time, Fedi shows a permission dialog where the user can Allow or Deny access, with an option to remember their choice for future calls.

This is the core contract between mini apps and users: the user is always in control.

Temporary Legacy Behavior

In current production builds, some APIs (WebLN, Nostr, basic Fedi methods) do not yet prompt for permission and are callable without explicit user authorization. This is deprecated behavior that will be removed in a future update. An upcoming Fedi release will require user permission for all API calls. You must build your mini app to handle permission denials for every API — do not assume any method will succeed without user approval.

How Permissions Work

  1. Your mini app calls an API method (e.g. window.webln.makeInvoice())
  2. Fedi checks whether the user has already granted your app that permission
  3. If not, Fedi shows a permission dialog identifying your mini app and the permission being requested
  4. The user taps Allow or Deny, optionally checking "Remember my choice"
  5. If allowed, the method proceeds (and may show an additional confirmation overlay for the specific action)
  6. If denied, the Promise rejects with an Error

If the user chose to remember their decision, subsequent calls skip the permission dialog — allowed methods proceed directly, denied methods reject immediately.

Permission Types

PermissionMethodsDescription
manageCommunitiescreateCommunity, editCommunity, joinCommunity, listCreatedCommunities, refreshCommunities, setSelectedCommunity, selectPublicChats, previewMatrixRoomCreate, edit, and manage Fedi communities
manageInstalledMiniAppsgetInstalledMiniApps, installMiniAppList and install mini apps on the user's device
navigationnavigateHomeNavigate the user out of the mini app browser

Additional permission types for WebLN, Nostr, ecash, and other APIs will be introduced as the permission system is extended to cover all injection methods.

User Confirmation Overlays

Separately from the permission system, certain methods also show a confirmation overlay where the user reviews and approves the specific action. Even after your app has been granted permission, the user still sees these overlays for each individual operation:

MethodWhat the user sees
webln.makeInvoice()Invoice creation screen — user reviews amount and memo
webln.sendPayment()Payment confirmation — user reviews decoded invoice details and amount
nostr.signEvent()Event signing screen — user reviews the event content before signing
fedi.generateEcash()Ecash generation screen — user reviews the amount to withdraw

This means sensitive operations have two points where the user can say no: the permission grant and the individual action confirmation. Your app must handle rejection at both stages.

What Happens When a User Denies Permission

When the user denies permission (or rejects a confirmation overlay), two things happen:

  1. The Promise rejects with a standard JavaScript Error — your app receives this like any other rejected Promise
  2. A toast notification appears in Fedi telling the user which permission is missing
try {
await window.webln.enable()
const { paymentRequest } = await window.webln.makeInvoice({ amount: 1000 })
// User approved — use the invoice
} catch (err) {
// User denied permission, rejected the overlay, or the operation failed
console.error(err.message)
// Recover your UI — re-enable buttons, clear loading states
}

If the user checked "Remember my choice" when denying, all future calls to methods under that permission will reject immediately without showing a dialog. The user can reset this in their Fedi settings.

Designing for Denial

Always assume the user might say no. Your app must handle denied permissions for every API call:

  • Recover the UI — Re-enable buttons, clear loading spinners, hide progress indicators
  • Explain what happened — Show a user-friendly message like "Payment permission is required to continue" rather than a raw error
  • Don't retry automatically — If a user denies an action, respect that decision. Let them try again manually
  • Degrade gracefully — If a core feature requires a permission the user denied, disable that feature and explain why rather than crashing or showing a blank screen
async function requestPayment(amount, memo) {
try {
await window.webln.enable()
const { paymentRequest } = await window.webln.makeInvoice({
amount,
defaultMemo: memo,
})
return { success: true, paymentRequest }
} catch (err) {
// Could be:
// - User denied permission for WebLN
// - User rejected the invoice confirmation overlay
// - No wallet federation joined
// - Recovery in progress
return { success: false, error: err.message }
}
}

// Usage
const result = await requestPayment(1000, "Coffee")
if (!result.success) {
showNotification(`Could not create invoice: ${result.error}`)
}

Detecting the Fedi Environment

If your app also runs outside of Fedi (e.g. in a regular browser), check for the injected APIs before calling them:

function isRunningInFedi() {
return typeof window.webln !== "undefined"
}

async function payInvoice(bolt11) {
if (window.webln) {
await window.webln.enable()
return window.webln.sendPayment(bolt11)
}
// Fallback: show QR code, redirect to wallet, etc.
showPaymentQR(bolt11)
}

For Nostr:

async function getNostrPubkey() {
if (window.nostr) {
return window.nostr.getPublicKey()
}
// Fallback: ask user to paste pubkey, use a different NIP-07 extension, etc.
return promptForPubkey()
}

Error Handling

All API methods return Promises that reject on failure. Common error scenarios:

ScenarioError
User denies permission"Permission denied: <permissionName>"
User rejects a confirmation overlayPromise rejects with an Error
WebLN not enabled"Provider must be enabled before use"
No wallet federation joined"Please join a wallet-enabled federation"
Invalid BOLT11 invoice"Failed to decode invoice"
Unsupported WebLN methodUnsupportedMethodError with method name
No Nostr public key"Failed to get Nostr pubkey"
Recovery in progressOperations that touch funds will show a recovery overlay
No Lightning gateways available"No available lightning gateways"
Missing authenticated member"No authenticated member"
Ecash receive failed"Failed to receive ecash"

Always wrap API calls in try/catch:

try {
await window.webln.enable()
const { preimage } = await window.webln.sendPayment(invoice)
// Handle success
} catch (err) {
// User rejected, or payment failed
console.error("Payment failed:", err.message)
}

Testing Your Mini App

In the Fedi App

  1. Build and deploy your web app to a URL (or use a tunnel like ngrok for local development)
  2. In Fedi, open the Mini Apps browser
  3. Enter your URL in the address bar
  4. Your app loads with window.webln, window.nostr, and window.fedi injected

Debug Mode

If you need it, Eruda (a mobile web console) is injected alongside the APIs, giving you a full developer console inside the webview.

Read this code to understand how to unlock Developer Settings and enable the Eruda debug tool.

Design Tips

  • Mobile-first: Your app runs in a mobile webview. Design for small screens.
  • Minimal navigation: Avoid complex routing — the user is already inside Fedi's browser with its own back button and address bar.
  • Handle rejections gracefully: Users can always cancel payment, signing, and ecash overlays. Re-enable buttons, clear loading spinners, and show a helpful message. Never leave the UI in a broken state after a rejection.
  • Don't retry automatically: If a user rejects an action, respect that decision. Prompt them to try again manually rather than immediately re-showing the overlay.