OpenReceive for PHP
Accept Bitcoin & Stablecoin Payments With PHP
Mount the framework-free PSR-15 engine in a plain PHP front controller with your PDO connection and three host methods. Bring the PSR-7 implementation you already use; no framework or payment service is required.
Tested live · Oct 6, 2026 · 0.4.18
Copy agent directions — Paste into Claude Code, Codex or Cursor to add OpenReceive to your PHP app.
Why OpenReceive
- No OpenReceive account. There is no sign-up, no API key and no dashboard. Install the package, add a receive-only wallet code, and your server issues invoices itself.
- Open source, inside your server. MIT-licensed routes mount in the app you already run. OpenReceive never sees an order, a price or a customer, and never holds funds.
- Receive-only wallet permissions. The one credential is a receive-only NWC code. It can create invoices and read their status. It cannot spend, so a compromised server cannot drain the wallet.
- Stablecoins, optional. Connect a swap provider and customers can pay in USDT, USDC, ETH or SOL. Every payment still settles to your wallet in Bitcoin.
Try the checkout your customers will see
The live demo needs JavaScript: open the OpenReceive home page.
Set up
Requires PHP ≥ 8.2 (64-bit), ext-gmp.
1. Get a receive-only NWC code — get one here. It can issue invoices and watch for payment, and it can never spend from your wallet.
2. Get an LSC code for swaps (optional) — from a swap provider, only if customers should be able to pay in USDT, USDC, ETH or SOL.
3. Set the environment variables — NWC_URI (required) and
LSC_URI_PRIMARY / LSC_URI_BACKUP (optional) in the environment of
your PHP server process; see
Environment variables.
4. Install
composer require openreceive/openreceive nyholm/psr7 nyholm/psr7-server
5. Wire OpenReceive — quoted from the PHP quickstart.
<?php
// public/index.php — or wherever your front controller lives
declare(strict_types=1);
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use OpenReceive\Host;
use OpenReceive\PaymentSettlement;
use OpenReceive\Server\AuthorizeContext;
use OpenReceive\Server\Engine;
use OpenReceive\Server\Service;
use OpenReceive\Storage\PdoConnection;
use OpenReceive\Storage\SqlPaymentRepository;
require __DIR__ . '/../vendor/autoload.php';
$pdo = new PDO(getenv('DATABASE_DSN')); // the PDO your app already opens
$orders = new App\Orders($pdo); // YOUR order model — any name works
$host = new class($orders) implements Host {
public function __construct(private readonly App\Orders $orders) {}
// Your own access check: may this caller do this action to this reference?
// `$context->reference()` is your order id, sent back by the payer's
// browser — a claim, not proof — already validated as a non-empty string.
// `$context->request` is the PSR-7 ServerRequest: read your session or
// cookie from it. `$context->action` names the route (checkout.create, …).
public function authorize(AuthorizeContext $context): bool
{
$order = $this->orders->find($context->reference());
return $order !== null && $order->userId === App\Session::userId($context->request);
}
// The price for a reference from YOUR data. `value` is a decimal STRING,
// never a float and never a request parameter; `description` is what the
// payer is buying, rendered above the amount. null = nothing to pay (404).
public function amountFor(string $reference): ?array
{
$order = $this->orders->find($reference);
return $order === null ? null : [
'currency' => 'USD',
'value' => $order->total, // "12.00"
'description' => "{$order->lineCount} items",
];
}
// INSIDE the settlement transaction, once per reference. Write through
// `$settlement->connection` — that transaction — so your order flips in the
// same commit as the payment record. The WHERE clause is the lock: a second
// fulfillment path of yours updates zero rows. Database writes only here;
// emails and webhooks go after commit (implement Hosts\AfterPaid for that).
public function onPaid(PaymentSettlement $settlement): void
{
$settlement->connection->execute(
"UPDATE orders SET state = 'paid', paid_at = ? WHERE id = ? AND state = 'awaiting_payment'",
[$settlement->paidAt, $settlement->reference],
);
}
};
$engine = new Engine(
$host,
new SqlPaymentRepository(new PdoConnection($pdo)),
Service::fromEnvironment(), // NWC_URI (+ LSC_URI_*) from the environment; preflight runs here
prefix: '/openreceive',
);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (str_starts_with($path, '/openreceive')) {
$factory = new Psr17Factory();
$request = (new ServerRequestCreator($factory, $factory, $factory, $factory))->fromGlobals();
$response = $engine->psr15Handler()->handle($request);
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
foreach ($values as $value) header("{$name}: {$value}", false);
}
echo $response->getBody();
return;
}
// … your own routes
6. Render the checkout
<link rel="stylesheet" href="/openreceive/openreceive-checkout.css" />
<script type="module" src="/openreceive/openreceive-checkout.js"></script>
<openreceive-checkout
reference="<?= htmlspecialchars($order->id) ?>"
prefix="/openreceive"
></openreceive-checkout>
Full PHP quickstart · Example app
Practical questions
- Which currencies can customers pay with?
- Bitcoin over Lightning, always. With a swap provider configured, also USDT on Tron, Solana or Ethereum, USDC on Solana or Ethereum, ETH and SOL, each converted into the same Lightning payment. Automated swaps guide
- Where does the Bitcoin go?
- Into the wallet behind your NWC code. OpenReceive never holds funds: the invoice is minted in your wallet and the sats land there. Your server confirms settlement and runs your onPaid hook once per order. Payment storage guide
- What wallet do I need?
- Any wallet that issues receive-only NWC (Nostr Wallet Connect) codes, hosted or self-run. The code must be able to create invoices and read their status, and must not be able to spend. Get a receive-only NWC code
- What does it cost?
- OpenReceive is open source and free. Fees charged by your wallet provider, by a swap provider or by Lightning routing belong to those services and are outside OpenReceive's scope.
- What do I need to deploy?
- 64-bit PHP 8.2 or later with GMP, sodium, mbstring, JSON, PDO and your database driver; NWC_URI in the server environment; the payment-table DDL in your normal migration workflow; and the standalone checkout archive or an npm-built frontend. Plain PHP quickstart
Add OpenReceive to your PHP app
Copy agent directions · Full quickstart · Example repository · API reference · Guides