Receiving payments using LNURL-Pay and Lightning addresses
What is a Lightning address?
A Lightning address is a human-readable identifier formatted like an email address (e.g., user@domain.com) that can be used to receive Bitcoin payments over the Lightning Network. Behind the scenes, it uses the LNURL-Pay protocol to dynamically generate invoices when someone wants to send a payment to this address.
Configuring a custom domain
To use Lightning addresses with the Breez SDK, you first need to supply a domain. There are two options:
- Use a hosted LNURL server: You can have your custom domain configured to an LNURL server run by Breez.
- Self-hosted LNURL server: You can run your own LNURL server in a self-hosted environment.
In case you choose to point your domain to a hosted LNURL server, you will need to add a CNAME record in your domain's DNS settings.
Note:: If you're using Cloudflare, make sure the CNAME record is set to 'DNS only' (not 'Proxied').
Option 1: Using your domain without any subdomain
This points yourdomain.com directly to the LNURL server. Some DNS providers do not support this method. If yours doesn't support CNAME or ALIAS records for the root domain, you will need to configure your domain at the registrar level to use an external DNS provider (like Google Cloud DNS).
- Host/Name: @
- Type: CNAME (or ALIAS if available)
- Value/Target: breez.tips
Option 2: Using a subdomain This points a subdomain like pay.yourdomain.com to the LNURL server.
- Host/Name: pay (or your chosen prefix like payment, tip, donate)
- Type: CNAME
- Value/Target: breez.tips
Send us your domain name (e.g., yourdomain.com or pay.yourdomain.com).
We will verify and add it to our list of allowed domains.
Configuring Lightning addresses for users
Configure your domain in the SDK by passing the lnurl_domain parameter in the SDK configuration:
let mut config = default_config(Network::Mainnet);
config.api_key = Some("your-api-key".to_string());
config.lnurl_domain = Some("yourdomain.com".to_string());
var config = defaultConfig(network: Network.mainnet)
config.apiKey = "your-api-key"
config.lnurlDomain = "yourdomain.com"
val config = defaultConfig(Network.MAINNET)
config.apiKey = "your-api-key"
config.lnurlDomain = "yourdomain.com"
var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with
{
apiKey = "your-api-key",
lnurlDomain = "yourdomain.com"
};
const config = defaultConfig('mainnet')
config.apiKey = 'your-api-key'
config.lnurlDomain = 'yourdomain.com'
const config = defaultConfig(Network.Mainnet)
config.apiKey = 'your-api-key'
config.lnurlDomain = 'yourdomain.com'
final config = defaultConfig(network: Network.mainnet)
.copyWith(
apiKey: 'your-api-key',
lnurlDomain: 'yourdomain.com'
);
config = default_config(network=Network.MAINNET)
config.api_key = "your-api-key"
config.lnurl_domain = "yourdomain.com"
lnurlDomain := "yourdomain.com"
apiKey := "your-api-key"
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
config.ApiKey = &apiKey
config.LnurlDomain = &lnurlDomain
Managing Lightning addresses API docs
The SDK provides several functions to manage Lightning addresses:
Checking address availability
Before registering a Lightning address, you can check if the username is available. In your UI you can use a quick check mark to show the address is available before registering.
let request = CheckLightningAddressRequest { username };
let is_available = sdk.check_lightning_address_available(request).await?;
let request = CheckLightningAddressRequest(
username: username
)
let available = try await sdk.checkLightningAddressAvailable(req: request)
val request = CheckLightningAddressRequest(
username = username
)
val available = sdk.checkLightningAddressAvailable(request)
var request = new CheckLightningAddressRequest(username: username);
var isAvailable = await sdk.CheckLightningAddressAvailable(request);
const request = {
username
}
const available = await sdk.checkLightningAddressAvailable(request)
const request = {
username
}
const available = await sdk.checkLightningAddressAvailable(request)
final request = CheckLightningAddressRequest(
username: username,
);
final available = await sdk.checkLightningAddressAvailable(request: request);
request = CheckLightningAddressRequest(username=username)
is_available = await sdk.check_lightning_address_available(request)
request := breez_sdk_spark.CheckLightningAddressRequest{
Username: username,
}
isAvailable, err := sdk.CheckLightningAddressAvailable(request)
if err != nil {
return false, err
}
Registering a Lightning address
Once you've confirmed a username is available, you can register it by passing a username and a description. The username will be used in username@domain.com. The description will be included in lnurl metadata and as the invoice description, so this is what the sender will see. The description is optional, and will default to Pay to username@domain.com.
let request = RegisterLightningAddressRequest {
username,
description,
};
let address_info = sdk.register_lightning_address(request).await?;
let lightning_address = address_info.lightning_address;
let lnurl = address_info.lnurl;
let request = RegisterLightningAddressRequest(
username: username,
description: description
)
let addressInfo = try await sdk.registerLightningAddress(request: request)
let lightningAddress = addressInfo.lightningAddress
let lnurl = addressInfo.lnurl
val request = RegisterLightningAddressRequest(
username = username,
description = description
)
val addressInfo = sdk.registerLightningAddress(request)
val lightningAddress = addressInfo.lightningAddress
val lnurl = addressInfo.lnurl
var request = new RegisterLightningAddressRequest(
username: username,
description: description
);
var addressInfo = await sdk.RegisterLightningAddress(request);
var lightningAddress = addressInfo.lightningAddress;
var lnurl = addressInfo.lnurl;
const request = {
username,
description
}
const addressInfo = await sdk.registerLightningAddress(request)
const lightningAddress = addressInfo.lightningAddress
const lnurl = addressInfo.lnurl
const request = {
username,
description
}
const addressInfo = await sdk.registerLightningAddress(request)
const lightningAddress = addressInfo.lightningAddress
const lnurl = addressInfo.lnurl
final request = RegisterLightningAddressRequest(
username: username,
description: description,
);
final addressInfo = await sdk.registerLightningAddress(request: request);
final lightningAddress = addressInfo.lightningAddress;
final lnurl = addressInfo.lnurl;
request = RegisterLightningAddressRequest(
username=username,
description=description
)
address_info = await sdk.register_lightning_address(request)
lightning_address = address_info.lightning_address
lnurl = address_info.lnurl
request := breez_sdk_spark.RegisterLightningAddressRequest{
Username: username,
Description: &description,
}
addressInfo, err := sdk.RegisterLightningAddress(request)
if err != nil {
return nil, err
}
_ = addressInfo.LightningAddress
_ = addressInfo.Lnurl
Retrieving Lightning address information
You can retrieve information about the currently registered Lightning address.
let address_info_opt = sdk.get_lightning_address().await?;
if let Some(info) = address_info_opt {
let lightning_address = &info.lightning_address;
let username = &info.username;
let description = &info.description;
let lnurl = &info.lnurl;
}
if let addressInfo = try await sdk.getLightningAddress() {
let lightningAddress = addressInfo.lightningAddress
let username = addressInfo.username
let description = addressInfo.description
let lnurl = addressInfo.lnurl
}
val addressInfoOpt = sdk.getLightningAddress()
if (addressInfoOpt != null) {
val lightningAddress = addressInfoOpt.lightningAddress
val username = addressInfoOpt.username
val description = addressInfoOpt.description
val lnurl = addressInfoOpt.lnurl
}
var addressInfoOpt = await sdk.GetLightningAddress();
if (addressInfoOpt != null)
{
var lightningAddress = addressInfoOpt.lightningAddress;
var username = addressInfoOpt.username;
var description = addressInfoOpt.description;
var lnurl = addressInfoOpt.lnurl;
}
const addressInfoOpt = await sdk.getLightningAddress()
if (addressInfoOpt != null) {
const lightningAddress = addressInfoOpt.lightningAddress
const username = addressInfoOpt.username
const description = addressInfoOpt.description
const lnurl = addressInfoOpt.lnurl
}
const addressInfoOpt = await sdk.getLightningAddress()
if (addressInfoOpt != null) {
const lightningAddress = addressInfoOpt.lightningAddress
const username = addressInfoOpt.username
const description = addressInfoOpt.description
const lnurl = addressInfoOpt.lnurl
}
final addressInfoOpt = await sdk.getLightningAddress();
if (addressInfoOpt == null) {
throw Exception("No Lightning Address registered for this user.");
}
final lightningAddress = addressInfoOpt.lightningAddress;
final username = addressInfoOpt.username;
final description = addressInfoOpt.description;
final lnurl = addressInfoOpt.lnurl;
address_info_opt = await sdk.get_lightning_address()
if address_info_opt is not None:
lightning_address = address_info_opt.lightning_address
username = address_info_opt.username
description = address_info_opt.description
lnurl = address_info_opt.lnurl
addressInfoOpt, err := sdk.GetLightningAddress()
if err != nil {
return nil, err
}
if addressInfoOpt != nil {
_ = addressInfoOpt.LightningAddress
_ = addressInfoOpt.Username
_ = addressInfoOpt.Description
_ = addressInfoOpt.Lnurl
}
Deleting a Lightning address
When a user no longer wants to use the Lightning address, you can delete it.
sdk.delete_lightning_address().await?;
try await sdk.deleteLightningAddress()
sdk.deleteLightningAddress()
await sdk.DeleteLightningAddress();
await sdk.deleteLightningAddress()
await sdk.deleteLightningAddress()
await sdk.deleteLightningAddress();
await sdk.delete_lightning_address()
err := sdk.DeleteLightningAddress()
if err != nil {
return err
}
Accessing LNURL payment metadata
When receiving payments via LNURL-Pay or Lightning addresses, additional metadata may be included with the payment. This metadata is available on the received payment.
Sender comment
If the sender includes a comment with their payment (as defined in LUD-12), it will be available on the received payment. This is the message that the sender wrote when making the payment.
// Check if this is a lightning payment with LNURL receive metadata
if let Some(PaymentDetails::Lightning {
lnurl_receive_metadata: Some(metadata),
..
}) = payment.details
{
// Access the sender comment if present
if let Some(comment) = metadata.sender_comment {
println!("Sender comment: {}", comment);
}
}
// Check if this is a lightning payment with LNURL receive metadata
if case .lightning(let details) = payment.details {
// Access the sender comment if present
if let metadata = details.lnurlReceiveMetadata,
let comment = metadata.senderComment {
print("Sender comment: \(comment)")
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details is PaymentDetails.Lightning) {
val details = payment.details as PaymentDetails.Lightning
val metadata = details.lnurlReceiveMetadata
// Access the sender comment if present
metadata?.senderComment?.let { comment ->
println("Sender comment: $comment")
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details is PaymentDetails.Lightning lightningDetails)
{
var metadata = lightningDetails.lnurlReceiveMetadata;
// Access the sender comment if present
if (metadata?.senderComment != null)
{
Console.WriteLine($"Sender comment: {metadata.senderComment}");
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details?.type === 'lightning') {
const metadata = payment.details.lnurlReceiveMetadata
// Access the sender comment if present
if (metadata?.senderComment != null) {
console.log('Sender comment:', metadata.senderComment)
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details?.tag === PaymentDetails_Tags.Lightning) {
const metadata = payment.details.inner.lnurlReceiveMetadata
// Access the sender comment if present
if (metadata?.senderComment != null) {
console.log('Sender comment:', metadata.senderComment)
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details case PaymentDetails_Lightning lightningDetails) {
final metadata = lightningDetails.lnurlReceiveMetadata;
// Access the sender comment if present
final comment = metadata?.senderComment;
if (comment != null) {
print('Sender comment: $comment');
}
}
# Check if this is a lightning payment with LNURL receive metadata
if isinstance(payment.details, PaymentDetails.LIGHTNING):
metadata = payment.details.lnurl_receive_metadata
# Access the sender comment if present
if metadata is not None and metadata.sender_comment is not None:
print(f"Sender comment: {metadata.sender_comment}")
// Check if this is a lightning payment with LNURL receive metadata
if lightningDetails, ok := (*payment.Details).(breez_sdk_spark.PaymentDetailsLightning); ok {
metadata := lightningDetails.LnurlReceiveMetadata
// Access the sender comment if present
if metadata != nil && metadata.SenderComment != nil {
println("Sender comment:", *metadata.SenderComment)
}
}
Nostr Zap request
If the payment was sent as a Nostr Zap (as defined in NIP-57), the received payment will include the zap request event. It carries the signed Nostr event (kind 9734) used to create the zap, and will also include the zap receipt event (kind 9735) once that has been created and published.
// Check if this is a lightning payment with LNURL receive metadata
if let Some(PaymentDetails::Lightning {
lnurl_receive_metadata: Some(metadata),
..
}) = payment.details
{
// Access the Nostr zap request if present
if let Some(zap_request) = metadata.nostr_zap_request {
// The zap_request is a JSON string containing the Nostr event (kind 9734)
println!("Nostr zap request: {}", zap_request);
}
// Access the Nostr zap receipt if present
if let Some(zap_receipt) = metadata.nostr_zap_receipt {
// The zap_receipt is a JSON string containing the Nostr event (kind 9735)
println!("Nostr zap receipt: {}", zap_receipt);
}
}
// Check if this is a lightning payment with LNURL receive metadata
if case .lightning(let details) = payment.details {
if let metadata = details.lnurlReceiveMetadata {
// Access the Nostr zap request if present
if let zapRequest = metadata.nostrZapRequest {
// The zapRequest is a JSON string containing the Nostr event (kind 9734)
print("Nostr zap request: \(zapRequest)")
}
// Access the Nostr zap receipt if present
if let zapReceipt = metadata.nostrZapReceipt {
// The zapReceipt is a JSON string containing the Nostr event (kind 9735)
print("Nostr zap receipt: \(zapReceipt)")
}
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details is PaymentDetails.Lightning) {
val details = payment.details as PaymentDetails.Lightning
val metadata = details.lnurlReceiveMetadata
// Access the Nostr zap request if present
metadata?.nostrZapRequest?.let { zapRequest ->
// The zapRequest is a JSON string containing the Nostr event (kind 9734)
println("Nostr zap request: $zapRequest")
}
// Access the Nostr zap receipt if present
metadata?.nostrZapReceipt?.let { zapReceipt ->
// The zapReceipt is a JSON string containing the Nostr event (kind 9735)
println("Nostr zap receipt: $zapReceipt")
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details is PaymentDetails.Lightning lightningDetails)
{
var metadata = lightningDetails.lnurlReceiveMetadata;
if (metadata != null)
{
// Access the Nostr zap request if present
if (metadata.nostrZapRequest != null)
{
// The nostrZapRequest is a JSON string containing the Nostr event (kind 9734)
Console.WriteLine($"Nostr zap request: {metadata.nostrZapRequest}");
}
// Access the Nostr zap receipt if present
if (metadata.nostrZapReceipt != null)
{
// The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735)
Console.WriteLine($"Nostr zap receipt: {metadata.nostrZapReceipt}");
}
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details?.type === 'lightning') {
const metadata = payment.details.lnurlReceiveMetadata
// Access the Nostr zap request if present
if (metadata?.nostrZapRequest != null) {
// The nostrZapRequest is a JSON string containing the Nostr event (kind 9734)
console.log('Nostr zap request:', metadata.nostrZapRequest)
}
// Access the Nostr zap receipt if present
if (metadata?.nostrZapReceipt != null) {
// The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735)
console.log('Nostr zap receipt:', metadata.nostrZapReceipt)
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details?.tag === PaymentDetails_Tags.Lightning) {
const metadata = payment.details.inner.lnurlReceiveMetadata
// Access the Nostr zap request if present
if (metadata?.nostrZapRequest != null) {
// The nostrZapRequest is a JSON string containing the Nostr event (kind 9734)
console.log('Nostr zap request:', metadata.nostrZapRequest)
}
// Access the Nostr zap receipt if present
if (metadata?.nostrZapReceipt != null) {
// The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735)
console.log('Nostr zap receipt:', metadata.nostrZapReceipt)
}
}
// Check if this is a lightning payment with LNURL receive metadata
if (payment.details case PaymentDetails_Lightning lightningDetails) {
final metadata = lightningDetails.lnurlReceiveMetadata;
if (metadata != null) {
// Access the Nostr zap request if present
final zapRequest = metadata.nostrZapRequest;
if (zapRequest != null) {
// The zapRequest is a JSON string containing the Nostr event (kind 9734)
print('Nostr zap request: $zapRequest');
}
// Access the Nostr zap receipt if present
final zapReceipt = metadata.nostrZapReceipt;
if (zapReceipt != null) {
// The zapReceipt is a JSON string containing the Nostr event (kind 9735)
print('Nostr zap receipt: $zapReceipt');
}
}
}
# Check if this is a lightning payment with LNURL receive metadata
if isinstance(payment.details, PaymentDetails.LIGHTNING):
metadata = payment.details.lnurl_receive_metadata
if metadata is not None:
# Access the Nostr zap request if present
if metadata.nostr_zap_request is not None:
# The nostr_zap_request is a JSON string containing the Nostr event (kind 9734)
print(f"Nostr zap request: {metadata.nostr_zap_request}")
# Access the Nostr zap receipt if present
if metadata.nostr_zap_receipt is not None:
# The nostr_zap_receipt is a JSON string containing the Nostr event (kind 9735)
print(f"Nostr zap receipt: {metadata.nostr_zap_receipt}")
// Check if this is a lightning payment with LNURL receive metadata
if lightningDetails, ok := (*payment.Details).(breez_sdk_spark.PaymentDetailsLightning); ok {
metadata := lightningDetails.LnurlReceiveMetadata
if metadata != nil {
// Access the Nostr zap request if present
if metadata.NostrZapRequest != nil {
// The NostrZapRequest is a JSON string containing the Nostr event (kind 9734)
println("Nostr zap request:", *metadata.NostrZapRequest)
}
// Access the Nostr zap receipt if present
if metadata.NostrZapReceipt != nil {
// The NostrZapReceipt is a JSON string containing the Nostr event (kind 9735)
println("Nostr zap receipt:", *metadata.NostrZapReceipt)
}
}
}
Note: When used in private mode, the nostr zap receipt will be published by the SDK when online. When used in public mode, the zap receipt will be published by the LNURL server on your behalf.