What it is
A Lightning Address looks like an email address and works like one for payments: anyone
can type bob@example.com into a wallet and pay you, without QR codes,
invoices pasted over chat, or a new invoice for every payment.
Under the hood it is just HTTPS. A wallet turns bob@example.com into a
request to https://example.com/.well-known/lnurlp/bob, reads the payment
parameters it gets back, and then asks the same server for a bolt11 invoice of the
amount the payer chose. This library is that server, written in PHP.
Spec-compliant
Implements LNURL-pay (LUD-06), the format wallets already speak.
Multi-user
One deployment serves many usernames, each mapped to its own invoice backend and API key.
Backend-agnostic
LNbits ships today; other backends are an interface implementation away.
Small and typed
PHP 8.3+, Psalm errorLevel 1 and PHPStan level max on every commit.
Why self-host it
- The address is yours. It lives on your domain, so no provider can rename, rate-limit, or discontinue it.
- Payments go straight to your node. The library asks your wallet backend for invoices; nobody custodies the sats on the way.
- No new stack. If you already run PHP, this is one route and two config files, not another service to operate.
- Multiple identities, one deployment. Family, team, or project addresses can share a domain while each keeps its own backend.
Anatomy of an address
Nothing about a Lightning Address is new protocol — it is a naming convention on top of plain HTTPS. The two halves of the address tell a wallet exactly which URL to open.
How a payment happens
Two HTTP round trips, then a normal Lightning payment. Step through it:
Steps 1, 2, 3 and 6 are what this library answers. Step 4 and 5 happen against your own node — the sats in step 7 travel from the payer straight to your wallet, never through the library.
What travels on the wire
The pay parameters (step 2), straight from a running instance:
{
"callback": "https://example.com",
"maxSendable": 10000000000,
"minSendable": 100000,
"metadata": "[[\"text/plain\",\"Pay to bob@example.com\"], …]",
"tag": "payRequest",
"commentAllowed": false
}
And the invoice (step 6), with the bolt11 string under pr as LUD-06 requires:
{
"pr": "lnbc20n1p…",
"status": "OK",
"successAction": { "tag": "message", "message": "Payment received!" },
"routes": [],
"disposable": false,
"error": null
}
Quick start
Requires PHP 8.3 or newer and an LNbits wallet API key.
composer require php-lightning/lnaddress
cp lightning-config.dist.php lightning-config.php # settings
cp backends.dist.json backends.json # per-user invoice backends
composer serve # http://localhost:8080
Settings — lightning-config.php
use PhpLightning\Config\LightningConfig;
return (new LightningConfig())
->setDomain('yourdomain.com')
->setReceiver('default-receiver')
->setDescriptionTemplate('Pay to %s') // %s = the lightning address
->setSuccessMessage('Thanks for the payment!')
->setSendableRange(min: 100_000, max: 10_000_000_000) // millisats
->setCallbackUrl('https://yourdomain.com')
->addBackendsFile(getcwd() . '/backends.json');
Backends — backends.json
{
"bob": { "type": "lnbits", "api_key": "abc...123", "api_endpoint": "http://localhost:5000" },
"alice": { "type": "lnbits", "api_key": "def...456", "api_endpoint": "http://localhost:5000" }
}
Then point /.well-known/lnurlp/{username} at the app's
/{username} route, over HTTPS, and the address is live.
Under the hood
Built on the Gacela framework as a small modular monolith. Each module exposes a facade; the domain layer is pure PHP that knows nothing about HTTP, which is what makes swapping the invoice backend — or calling the library directly instead of over HTTP — straightforward.
public/index.php → Router → InvoiceRoutesPlugin
├─ CorsMiddleware wallets call cross-origin
├─ InvoiceExceptionHandler errors → LNURL error object
└─ InvoiceController
└─ InvoiceFacade
├─ CallbackUrl → pay parameters
└─ InvoiceGenerator → bolt11 invoice
└─ LnbitsBackendInvoice → your node
Using it as a library, without the HTTP layer:
use Gacela\Framework\Gacela;
use PhpLightning\Invoice\InvoiceFacade;
Gacela::bootstrap(__DIR__);
$facade = new InvoiceFacade();
$payParams = $facade->getCallbackUrl('bob');
$invoice = $facade->generateInvoice('bob', 2_000); // millisats
Documentation
| Guide | Contents |
|---|---|
| Getting started | Install, configure, run locally, deploy behind a real domain |
| Configuration | Every setter, its default, and the backends file format |
| HTTP API | Routes, payloads, CORS, LNURL error objects, nginx rewrite |
| Architecture | Modules, layers, request flow, adding a backend |
| Development | Composer scripts, tests, static analysis, releasing |
FAQ
Does the library hold my money?
No. It asks your configured backend for an invoice and returns it. The payment settles between the payer's wallet and your node.
Do I need LNbits?
LNbits is the backend implemented today. Adding another means a case in the
BackendType enum and an implementation of
BackendInvoiceInterface — the rest of the code depends on the interface,
not on LNbits.
Can one domain serve several people?
Yes. Every entry in backends.json is a username with its own endpoint and
API key, so bob@ and alice@ can point at different wallets.
Does it work in a browser wallet?
Yes — every response carries permissive CORS headers and OPTIONS
preflights are answered directly.
What is not implemented?
LNURL comments (LUD-12): commentAllowed is always false.