LNURL Protocol
LNURL utilities implementing the Lightning URL protocol for Lightning Network payments and withdrawals. Covers LUD-01 (bech32 encode/decode), LUD-03 (withdraw requests), LUD-06 (pay requests), LUD-09 (success actions), LUD-10 (AES encrypted success actions), LUD-12 (comments), LUD-17 (scheme prefixes), LUD-18 (payer identity), LUD-20 (long description metadata), and LUD-21 (payment verification).
Import
import { lnurl } from 'nostr-core'
// or import individual functions
import {
encodeLnurl,
decodeLnurl,
isLnurl,
resolveUrl,
fetchPayRequest,
requestInvoice,
fetchWithdrawRequest,
submitWithdrawRequest,
parseSuccessAction,
decryptAesSuccessAction,
parseLnurlMetadata,
verifyPayment,
} from 'nostr-core'LnurlError Class
class LnurlError extends Error {
code: string
constructor(message: string, code?: string)
}All LNURL functions throw LnurlError on failure. The code field defaults to 'LNURL_ERROR'.
Types
SuccessAction
type SuccessAction =
| { tag: 'message'; message: string }
| { tag: 'url'; description: string; url: string }
| { tag: 'aes'; description: string; ciphertext: string; iv: string }| Variant | Description |
|---|---|
message | LUD-09: Display a plaintext message to the user |
url | LUD-09: Display a description and link |
aes | LUD-10: AES-encrypted payload, decrypted with the payment preimage |
PayerDataSpec
type PayerDataSpec = {
name?: { mandatory: boolean }
pubkey?: { mandatory: boolean }
identifier?: { mandatory: boolean }
email?: { mandatory: boolean }
auth?: { mandatory: boolean; k1: string }
}LUD-18: Describes which payer identity fields the service requires or accepts.
PayerData
type PayerData = {
name?: string
pubkey?: string
identifier?: string
email?: string
auth?: { key: string; k1: string; sig: string }
}LUD-18: Payer identity data sent by the wallet to the service.
LnurlMetadata
type LnurlMetadata = {
plainText: string
longDesc?: string
image?: { type: string; data: string }
entries: [string, string][]
}| Field | Type | Description |
|---|---|---|
plainText | string | Short description from text/plain entry |
longDesc | string (optional) | LUD-20: Long description from text/long-desc entry |
image | object (optional) | First image entry with its MIME type and base64 data |
entries | [string, string][] | Raw array of all [mime-type, content] tuples |
PayRequestResponse
type PayRequestResponse = {
tag: 'payRequest'
callback: string
minSendable: number
maxSendable: number
metadata: string
commentAllowed?: number
payerData?: PayerDataSpec
allowsNostr?: boolean
nostrPubkey?: string
}LUD-06: First response from a pay request LNURL endpoint.
PayRequestCallbackResponse
type PayRequestCallbackResponse = {
pr: string
routes: unknown[]
successAction?: SuccessAction
verify?: string
}| Field | Type | Description |
|---|---|---|
pr | string | BOLT-11 payment request (invoice) |
routes | unknown[] | Optional route hints |
successAction | SuccessAction (optional) | LUD-09: Action to perform after successful payment |
verify | string (optional) | LUD-21: URL to poll for payment verification |
WithdrawRequestResponse
type WithdrawRequestResponse = {
tag: 'withdrawRequest'
callback: string
k1: string
defaultDescription: string
minWithdrawable: number
maxWithdrawable: number
}LUD-03: First response from a withdraw request LNURL endpoint.
VerifyResponse
type VerifyResponse = {
settled: boolean
preimage: string | null
pr: string
}LUD-21: Response from a payment verification endpoint.
RequestInvoiceOptions
type RequestInvoiceOptions = {
comment?: string
payerData?: PayerData
nostr?: string
}| Field | Type | Description |
|---|---|---|
comment | string (optional) | LUD-12: Comment to attach to the payment |
payerData | PayerData (optional) | LUD-18: Payer identity data |
nostr | string (optional) | Stringified nostr zap request event |
lnurl.encodeLnurl
function encodeLnurl(url: string): stringEncodes a URL as a bech32 LNURL string (LUD-01).
const encoded = lnurl.encodeLnurl('https://service.com/api?q=3fc3645b')
// 'lnurl1dp68gurn8ghj7...'lnurl.decodeLnurl
function decodeLnurl(encoded: string): stringDecodes a bech32 LNURL string back to the original URL (LUD-01). Throws LnurlError if the prefix is not lnurl.
const url = lnurl.decodeLnurl('lnurl1dp68gurn8ghj7...')
// 'https://service.com/api?q=3fc3645b'lnurl.isLnurl
function isLnurl(str: string): booleanChecks if a string is a valid bech32-encoded LNURL (LUD-01).
lnurl.isLnurl('lnurl1dp68gurn8ghj7...') // true
lnurl.isLnurl('not-an-lnurl') // falselnurl.resolveUrl
function resolveUrl(input: string): { url: string; tag?: string }Resolves an LNURL to a plain URL (LUD-17). Accepts bech32-encoded LNURLs, scheme-prefixed URLs (lnurlp://, lnurlw://, lnurlc://, keyauth://), or plain https:// URLs as passthrough.
// Bech32 LNURL
const { url } = lnurl.resolveUrl('lnurl1dp68gurn8ghj7...')
// { url: 'https://service.com/api?q=3fc3645b' }
// LUD-17 scheme prefix
const result = lnurl.resolveUrl('lnurlp://service.com/.well-known/lnurlp/alice')
// { url: 'https://service.com/.well-known/lnurlp/alice', tag: 'payRequest' }
// Plain URL passthrough
const plain = lnurl.resolveUrl('https://service.com/lnurl')
// { url: 'https://service.com/lnurl' }lnurl.parseLnurlMetadata
function parseLnurlMetadata(metadata: string): LnurlMetadataParses the LNURL metadata JSON string into structured data (LUD-06/20). Metadata is a JSON-encoded array of [mime-type, content] tuples.
const meta = lnurl.parseLnurlMetadata(payRequest.metadata)
console.log(meta.plainText) // 'Pay to alice@service.com'
console.log(meta.longDesc) // Long description (LUD-20), if present
console.log(meta.image) // { type: 'image/png', data: 'base64...' }
console.log(meta.entries) // [['text/plain', 'Pay to alice@service.com'], ...]lnurl.fetchPayRequest
function fetchPayRequest(input: string): Promise<PayRequestResponse>Fetches a pay request from an LNURL endpoint (LUD-06). Accepts bech32 LNURL, scheme-prefixed URL, or plain URL.
const payRequest = await lnurl.fetchPayRequest('lnurl1dp68gurn8ghj7...')
console.log(payRequest.minSendable) // 1000 (msats)
console.log(payRequest.maxSendable) // 1000000000 (msats)
console.log(payRequest.commentAllowed) // 144 (LUD-12)
console.log(payRequest.payerData) // PayerDataSpec (LUD-18)
console.log(payRequest.allowsNostr) // true (for zaps)lnurl.requestInvoice
function requestInvoice(
payRequest: PayRequestResponse,
amountMsats: number,
opts?: RequestInvoiceOptions,
): Promise<PayRequestCallbackResponse>Requests an invoice from a pay request callback (LUD-06). Supports comments (LUD-12), payer data (LUD-18), and nostr zap requests. Throws if the amount is outside the allowed range.
const payRequest = await lnurl.fetchPayRequest('lnurl1dp68gurn8ghj7...')
// Simple invoice request
const { pr, successAction } = await lnurl.requestInvoice(payRequest, 10000)
// With comment (LUD-12) and payer data (LUD-18)
const response = await lnurl.requestInvoice(payRequest, 50000, {
comment: 'Great work!',
payerData: {
name: 'Alice',
email: 'alice@example.com',
},
})
console.log(response.pr) // BOLT-11 invoice string
console.log(response.successAction) // SuccessAction, if any
console.log(response.verify) // LUD-21 verify URL, if anylnurl.fetchWithdrawRequest
function fetchWithdrawRequest(input: string): Promise<WithdrawRequestResponse>Fetches a withdraw request from an LNURL endpoint (LUD-03).
const withdraw = await lnurl.fetchWithdrawRequest('lnurl1dp68gurn8ghj7...')
console.log(withdraw.minWithdrawable) // 1000 (msats)
console.log(withdraw.maxWithdrawable) // 500000 (msats)
console.log(withdraw.k1) // server-provided unique identifierlnurl.submitWithdrawRequest
function submitWithdrawRequest(
withdrawRequest: WithdrawRequestResponse,
invoice: string,
): Promise<void>Submits a withdraw request with a BOLT-11 invoice (LUD-03). The service will pay the provided invoice.
const withdraw = await lnurl.fetchWithdrawRequest('lnurl1dp68gurn8ghj7...')
const invoice = 'lnbc10u1p...' // your BOLT-11 invoice
await lnurl.submitWithdrawRequest(withdraw, invoice)lnurl.parseSuccessAction
function parseSuccessAction(raw: unknown): SuccessActionParses and validates a success action object (LUD-09). Returns a typed SuccessAction or throws on invalid input.
const action = lnurl.parseSuccessAction(callbackResponse.successAction)
switch (action.tag) {
case 'message':
console.log(action.message)
break
case 'url':
console.log(action.description, action.url)
break
case 'aes':
// Decrypt with preimage (see decryptAesSuccessAction)
break
}lnurl.decryptAesSuccessAction
function decryptAesSuccessAction(
action: Extract<SuccessAction, { tag: 'aes' }>,
preimageHex: string,
): Promise<string>Decrypts an AES-encrypted success action using the payment preimage (LUD-10). Uses AES-256-CBC with PKCS5 padding via the Web Crypto API. The preimage must be 32 bytes (64 hex characters).
const response = await lnurl.requestInvoice(payRequest, 10000)
// After payment is complete and you have the preimage:
if (response.successAction?.tag === 'aes') {
const plaintext = await lnurl.decryptAesSuccessAction(
response.successAction,
'abcdef0123456789...', // 64-char hex preimage
)
console.log(plaintext) // decrypted message from the service
}lnurl.verifyPayment
function verifyPayment(verifyUrl: string): Promise<VerifyResponse>Polls a verify URL to check if a payment has been settled (LUD-21).
const response = await lnurl.requestInvoice(payRequest, 10000)
// After paying the invoice, check settlement status
if (response.verify) {
const status = await lnurl.verifyPayment(response.verify)
console.log(status.settled) // true if payment is confirmed
console.log(status.preimage) // payment preimage hex string
console.log(status.pr) // the original invoice
}How It Works
- LUD-01 defines bech32 encoding for LNURL strings, making URLs shareable as QR codes
- LUD-03 allows services to push sats to a user's wallet by providing an invoice to a withdraw endpoint
- LUD-06 is the core pay request flow: fetch service parameters, then request an invoice for a specific amount
- LUD-09/10 define success actions the wallet should perform after payment (show message, open URL, or decrypt AES content)
- LUD-12 allows attaching a text comment to a payment, bounded by
commentAllowedlength - LUD-17 introduces human-readable scheme prefixes (
lnurlp://,lnurlw://) as alternatives to bech32 - LUD-18 enables the payer to optionally share identity information (name, email, pubkey) with the service
- LUD-20 extends metadata with a
text/long-descentry for richer service descriptions - LUD-21 provides a polling endpoint to verify whether a payment has settled