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:
| Object | What it does |
|---|---|
window.webln | Send and receive Lightning payments through the user's wallet |
window.nostr | Get the user's public key, sign Nostr events, encrypt/decrypt messages |
window.fedi | Generate/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:
| Field | Type | Description |
|---|---|---|
amount | number | Requested amount in sats |
defaultMemo | string | Optional invoice description |
minimumAmount | number | Optional minimum amount the user can set |
maximumAmount | number | Optional maximum amount the user can set |
Passing a plain number or string is shorthand for { amount: value }.
Response:
| Field | Type | Description |
|---|---|---|
paymentRequest | string | The BOLT11 invoice |
rHash | string | The 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:
| Field | Type | Description |
|---|---|---|
paymentRequest | string | A valid BOLT11 invoice |
Response:
| Field | Type | Description |
|---|---|---|
preimage | string | The 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:
| Field | Type | Description |
|---|---|---|
node.alias | string | The user's display name |
node.pubkey | string | Currently 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 messagesverifyMessage(signature, message)— Verifying signed messageskeysend(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):
| Field | Type | Description |
|---|---|---|
kind | number | Event kind (1 = text note, etc.) |
created_at | number | Unix timestamp in seconds |
tags | string[][] | Array of tag arrays |
content | string | Event content |
Response (SignedNostrEvent):
Returns the original event fields plus:
| Field | Type | Description |
|---|---|---|
id | string | Event ID (SHA256 hash) |
pubkey | string | Signer's hex public key |
sig | string | Schnorr 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.
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
- Your mini app calls an API method (e.g.
window.webln.makeInvoice()) - Fedi checks whether the user has already granted your app that permission
- If not, Fedi shows a permission dialog identifying your mini app and the permission being requested
- The user taps Allow or Deny, optionally checking "Remember my choice"
- If allowed, the method proceeds (and may show an additional confirmation overlay for the specific action)
- 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
| Permission | Methods | Description |
|---|---|---|
manageCommunities | createCommunity, editCommunity, joinCommunity, listCreatedCommunities, refreshCommunities, setSelectedCommunity, selectPublicChats, previewMatrixRoom | Create, edit, and manage Fedi communities |
manageInstalledMiniApps | getInstalledMiniApps, installMiniApp | List and install mini apps on the user's device |
navigation | navigateHome | Navigate 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:
| Method | What 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:
- The Promise rejects with a standard JavaScript
Error— your app receives this like any other rejected Promise - 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:
| Scenario | Error |
|---|---|
| User denies permission | "Permission denied: <permissionName>" |
| User rejects a confirmation overlay | Promise 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 method | UnsupportedMethodError with method name |
| No Nostr public key | "Failed to get Nostr pubkey" |
| Recovery in progress | Operations 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
- Build and deploy your web app to a URL (or use a tunnel like ngrok for local development)
- In Fedi, open the Mini Apps browser
- Enter your URL in the address bar
- Your app loads with
window.webln,window.nostr, andwindow.fediinjected
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.