Quickstart: Pay per API request
Charge per request with sats. This guide shows how to issue a BOLT11 invoice via the LNTPay SDK and confirm payment using the charge.paid webhook.
Payment flow overview
End-to-end pay-per-use flow using Lightning invoices and LNTPay webhooks.
User
↓
Your API
↓ Create charge (Lightning invoice)
LNTPay
↓ Lightning invoice (bolt11)
Lightning Wallet
↓ Payment settled
LNTPay
↓ charge.paid webhook
Your API
↓
Unlock API response1) Prerequisites
- Node.js >= 18
- LNTPay account (Portal)
- API key (create in Portal) with scopes:
charges:write,webhooks:read - Webhook endpoint + secret (create in Portal)
Environment variables: - LNTPAY_API_KEY - LNTPAY_WEBHOOK_SECRET - LNTPAY_BASE_URL (optional, default: https://api.lntpay.com/v1)
Direct links: - Portal: https://app.lntpay.com/login - Webhooks screen: https://app.lntpay.com/webhooks - API Reference: https://api.lntpay.com/docs
2) Install dependencies
Node.js >= 18 required.
npm init -y
npm install express body-parser @lntpay/sdk3) Example: pay-per-request API (CommonJS)
The webhook must be registered before express.json() because signature verification requires the raw request body.
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const LNTPay = require("@lntpay/sdk").default;
const app = express();
const port = process.env.PORT || 3001;
const client = new LNTPay({
apiKey: process.env.LNTPAY_API_KEY,
baseUrl: process.env.LNTPAY_BASE_URL || "https://api.lntpay.com/v1"
});
// In-memory store for the demo (production: use DB/cache)
const paidCharges = new Set();
const pendingCharges = new Set();
// Webhook MUST be registered BEFORE express.json()
// because signature verification requires the raw request body.
app.post(
"/webhooks/lntpay",
bodyParser.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-lntpay-signature"];
if (!signature) {
return res.status(400).send("Missing x-lntpay-signature header");
}
try {
const event = LNTPay.webhooks.constructEvent(
req.body,
String(signature),
process.env.LNTPAY_WEBHOOK_SECRET
);
if (event.type === "charge.paid") {
// Adjust these fields to match LNTPay webhook payload if needed
const chargeId =
(event.data && (event.data.charge_id || event.data.id)) ||
event.charge_id;
if (chargeId) {
paidCharges.add(String(chargeId));
pendingCharges.remove(String(chargeId));
}
}
return res.status(200).send("ok");
} catch (err) {
return res.status(400).send("Invalid signature");
}
}
);
// JSON parser for the rest of the app
app.use(express.json());
// Example protected resource (pay-per-request)
app.get("/api/data", async (req, res) => {
const chargeId = req.query.charge_id;
if (chargeId && paidCharges.has(String(chargeId))) {
return res.json({ ok: true, data: "Here is your paid content." });
}
if (chargeId && pendingCharges.has(String(chargeId))) {
const pCharge = await client.charges.retrieve(String(chargeId));
return res.status(200).json({
ok: true,
status: pCharge.status,
reason: "payment_pending",
charge_id: pCharge.charge_id,
bolt11: pCharge.bolt11,
next: `/api/data?charge_id=${pCharge.charge_id}`
});
}
// Not paid yet: create a new charge (BOLT11)
try {
const charge = await client.charges.create({
amount_sats: 10,
description: "Pay-per-request demo",
metadata: { feature: "pay_per_request" }
});
pendingCharges.add(String(charge.charge_id));
return res.status(200).json({
ok: true,
reason: "payment_required",
charge_id: charge.charge_id,
bolt11: charge.bolt11,
next: `/api/data?charge_id=${charge.charge_id}`
});
} catch (err) {
return res.status(500).json({ ok: false, error: "failed_to_create_charge" });
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});4) Webhook notes (important)
- Use the raw request body.
- Signature header:
x-lntpay-signature. - Signature is HMAC SHA-256 (hex) of the raw body using your webhook secret.
- Respond with HTTP 2xx quickly.
5) User flow (end-to-end)
1) Client calls your /api/data 2) API creates a charge (BOLT11) 3) Client pays the invoice 4) LNTPay sends charge.paid webhook 5) Client retries with charge_id and receives the response
6) Troubleshooting
- Webhook not arriving: check public URL, secret, and portal logs
- Invalid signature: ensure raw body and correct secret
Links: - Webhooks docs: https://docs.lntpay.com/webhooks - API Reference: https://api.lntpay.com/docs - Portal webhooks: https://app.lntpay.com/webhooks
Next steps
- Go to Portal: https://app.lntpay.com/login
- Explore Webhooks: https://docs.lntpay.com/webhooks
- See API Reference: https://api.lntpay.com/docs
- Install SDK: https://www.npmjs.com/package/@lntpay/sdk