Invoicing from your system
You already have billing software and you want it to collect payment through Brykto.
Read this first
Do not email an API-created link
Payment links created through POST /v1/payment-links expire 30 minutes after creation. An invoice emailed on Monday is dead long before anyone opens it.
Only links created in the dashboard get 7 days. The API deliberately issues short-lived links because they are meant to be created at the moment someone is ready to pay.
So the pattern is: email a link to your own billing page, and create the Brykto payment when the customer clicks Pay there.
Flow
Your system → Emails the invoice, linking to your own /invoices/{id} page
Customer → Opens your invoice page, whenever they get round to it
Customer → Clicks "Pay now"
Your server → POST /v1/payment-links (created right now, fresh 30 minutes)
Customer → Redirected to the Brykto pay page
Customer → Pays from their wallet
Brykto → Sends the order.paid webhook
Your server → Marks the invoice paid
The customer never sees the 30 minute limit, because the clock only starts when they are already paying.
Step 1 - Create the payment when they click Pay
import requests
from flask import redirect, abort
BASE = "https://bryktopay.com"
API_KEY = os.environ["BRYKTO_API_KEY"]
@app.post("/invoices/<invoice_id>/pay")
def pay_invoice(invoice_id):
invoice = db.get_invoice(invoice_id)
if invoice is None:
abort(404)
if invoice.status == "paid":
return redirect(f"/invoices/{invoice_id}")
response = requests.post(
f"{BASE}/v1/payment-links",
headers={"X-API-Key": API_KEY},
json={
"amount": float(invoice.total),
"currency": invoice.currency,
"description": f"Invoice {invoice.number}",
"order_id": invoice.number,
"customer_name": invoice.customer_name,
"customer_email": invoice.customer_email,
},
timeout=10,
)
response.raise_for_status()
payment = response.json()
db.attach_payment(invoice_id, payment["request_id"])
return redirect(f"{BASE}/pay/{payment['request_id']}")
const BASE = "https://bryktopay.com";
const API_KEY = process.env.BRYKTO_API_KEY;
router.post("/invoices/:invoiceId/pay", async (req, res) => {
const invoice = await db.getInvoice(req.params.invoiceId);
if (!invoice) return res.status(404).send("Not found");
if (invoice.status === "paid") {
return res.redirect(`/invoices/${invoice.id}`);
}
const response = await fetch(`${BASE}/v1/payment-links`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: Number(invoice.total),
currency: invoice.currency,
description: `Invoice ${invoice.number}`,
order_id: invoice.number,
customer_name: invoice.customerName,
customer_email: invoice.customerEmail,
}),
});
if (!response.ok) throw new Error(await response.text());
const payment = await response.json();
await db.attachPayment(invoice.id, payment.request_id);
res.redirect(`${BASE}/pay/${payment.request_id}`);
});
Setting order_id to your invoice number is what lets the webhook find the invoice again. It also lands in the merchant's CSV export, which makes reconciliation straightforward.
Step 2 - Mark the invoice paid from the webhook
@app.post("/webhook/brykto")
def brykto_webhook():
body = request.get_data()
if not verify(body,
request.headers.get("X-Brykto-Signature", ""),
request.headers.get("X-Brykto-Timestamp", "")):
abort(400)
event = request.get_json()
if event["event"] != "order.paid":
return "", 200
data = event["data"]
if db.already_handled(data["tx_hash"]):
return "", 200
invoice = db.get_invoice_by_number(data["order_id"])
if invoice is not None:
db.mark_invoice_paid(
invoice.id,
tx_hash=data["tx_hash"],
paid_at=data["paid_at"],
received=data["amount_received"],
)
return "", 200
router.post("/webhook/brykto",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = verify(
req.body,
req.headers["x-brykto-signature"],
req.headers["x-brykto-timestamp"],
);
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body);
if (event.event !== "order.paid") return res.status(200).end();
const data = event.data;
if (await db.alreadyHandled(data.tx_hash)) return res.status(200).end();
const invoice = await db.getInvoiceByNumber(data.order_id);
if (invoice) {
await db.markInvoicePaid(invoice.id, {
txHash: data.tx_hash,
paidAt: data.paid_at,
received: data.amount_received,
});
}
res.status(200).end();
});
amount and amount_received are different fields
amount is what you invoiced. amount_received is what actually arrived, read from the network. Store both if your books need to explain a difference.
Handling abandoned attempts
A customer who clicks Pay and then closes the tab leaves a payment that quietly expires after 30 minutes. Nothing needs cleaning up on your side, because you never marked the invoice paid.
If they come back and click Pay again, create a fresh payment. There is no limit on how many attempts an invoice can have, and only a completed one counts toward the merchant's monthly total.
Notes
- On the Free plan the account stops at 30 completed payments a calendar month, and the API returns
402after that. POST /v1/payment-linkshas no rate limit applied today. Do not depend on that. Handle429and back off.- There is no endpoint to look up a payment's status. The webhook is the only notification, so make your handler reliable rather than planning to poll.
Next steps
- Payment Links API - every request and response field
- Webhooks - retries, signature checking, and replay protection