Help Center Developer Docs Invoicing from your system

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 402 after that.
  • POST /v1/payment-links has no rate limit applied today. Do not depend on that. Handle 429 and 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