Docs/Workflows/Invoice Payment

Invoice Payment

Create an invoice, let moat send the customer a payment link, and collect asynchronously. Use when billing is decoupled from real-time checkout — services, wholesale, B2B.

Invoices are how you bill when the customer is not sitting in front of a checkout page. You create the invoice, moat emails a payment link, the customer pays on their own time, and you get a webhook when the payment lands.

Overview

  1. Server: create an invoice with line items, terms, and a due date.
  2. moat: emails the customer a branded payment page link.
  3. Customer: opens the link, pays on moat's hosted page.
  4. moat: fires an invoice.paid webhook. Your server marks the invoice paid.

Step 1 — Create the invoice

const resp = await fetch("https://sandbox.fluidpay.com/api/invoice", {
  method: "POST",
  headers: {
    "Authorization": process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    customer: {
      first_name: "Jane",
      last_name: "Doe",
      email: "jane@example.com",
      company: "Acme Inc."
    },
    invoice_number: "INV-2026-042",
    due_date: "2026-05-15",
    line_items: [
      { description: "Consulting — April",       quantity: 8, unit_amount: 15000 },
      { description: "Travel reimbursement",     quantity: 1, unit_amount: 42500 }
    ],
    tax_amount: 0,
    memo: "Thanks for your business.",
    terms: "Net 30",
    send_email: true
  })
});
const invoice = (await resp.json()).data;
// invoice.id          -> store against your invoice record
// invoice.hosted_url  -> public payment page URL (also emailed)

With send_email: true, moat emails the customer immediately. If you want to send the link yourself (through your own transactional email, Slack, etc.), omit send_email and use invoice.hosted_url directly.

Step 2 — The customer pays

The customer clicks the link, lands on a moat-hosted page branded with your logo and colors, selects card or ACH, pays. Invoice status transitions through sent → viewed → paid.

Step 3 — Handle the paid webhook

Subscribe to invoice.paid. On receipt, verify the signature (see Webhooks) and reconcile with your local record:

app.post("/webhooks/moat", express.raw({ type: "application/json" }), async (req, res) => {
  const signature = req.headers["x-signature"];
  const timestamp = req.headers["x-signature-timestamp"];
  if (!verifyWebhook(req.body, timestamp, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(400).end();
  }

  const event = JSON.parse(req.body.toString());

  if (event.event === "invoice.paid") {
    await db.invoice.update({
      where: { external_id: event.data.id },
      data: {
        status: "paid",
        paid_at: event.created_at,
        transaction_id: event.data.transaction_id
      }
    });
  }

  res.status(200).end();
});

Reminders and dunning

If due_date is set and the invoice has not been paid by then, moat transitions the invoice to overdue and emails reminders on a schedule configured in Settings → Invoices → Reminders. Default reminder schedule: at due date, 3 days after, 7 days after, 14 days after. You can disable or customize.

Partial payments

For B2B use cases where invoices might be paid in installments, set allow_partial_payments: true on create. The hosted payment page then lets the customer enter any amount up to the balance. The invoice transitions to partial after the first partial payment and to paid when the balance reaches zero. Each partial produces its own transaction.

Voiding an invoice

If the invoice is wrong or no longer applicable, call POST /api/invoice/{id}/void. The link stops working; the customer sees a "This invoice has been cancelled" page. Voiding is permissible in any state except paid. If the invoice has been partially paid, void prevents further payment but does not refund the partial — issue a refund on the transaction separately if needed.

Re-sending

If the customer misplaces the email, call POST /api/invoice/{id}/send to resend. You can do this any number of times until paid.

See also