Send an invoice

Send an invoice — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

This guide takes you from a bare customer record to a paid, reconciled invoice. It's runnable against the sandbox — swap your test keys in and follow along. Use invoices when you want VINR to host a payment page, email the customer, and chase payment for you, rather than collecting card details inline.

OverviewAsk

your server              VINR                    customer
    │  create customer     │                        │
    │─────────────────────►│                        │
    │  add line items      │                        │
    │─────────────────────►│                        │
    │  finalize → send     │   hosted invoice email  │
    │─────────────────────►│───────────────────────►│ pays
    │  invoice.paid (webhook)                        │
    │◄─────────────────────│◄───────────────────────│
    │  mark fulfilled / reconcile                    │

An invoice starts as a draft you can edit freely. Finalizing locks the line items and issues a hosted invoice URL; VINR then emails the customer and collects payment. You fulfil and reconcile on the invoice.paid webhook.

Create a customerAsk

Invoices belong to a customer. Store the returned cust_ id alongside your own user record so you can bill them again later.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const customer = await vinr.customers.create({
  email: 'ops@acme.example',
  name: 'Acme GmbH',
  metadata: { accountId: 'acct_8842' },
});
// customer.id === 'cust_...'

The customer's email is where VINR sends the hosted invoice and any payment reminders. Make sure it's verified before you finalize.

Build invoice line itemsAsk

Create a draft invoice, then attach one or more line items. Amounts are integers in minor units — 120000 is EUR 1,200.00. You can add ad-hoc items or reference an existing price.

const invoice = await vinr.invoices.create({
  customer: customer.id,
  currency: 'EUR',
  collectionMethod: 'send_invoice',   // email the customer (vs. charge_automatically)
  daysUntilDue: 14,
  metadata: { projectId: 'proj_2291' },
});

await vinr.invoices.addLineItem(invoice.id, {
  description: 'Implementation — May 2026',
  quantity: 1,
  unitAmount: 120000,                 // €1,200.00
});

await vinr.invoices.addLineItem(invoice.id, {
  description: 'Support hours',
  quantity: 8,
  unitAmount: 9500,                   // €95.00 each
});

While the invoice is in draft, you can add, update, or remove line items as often as you like. Totals, tax, and any discounts recompute automatically on finalize.

Finalize and sendAsk

Finalizing freezes the line items, assigns an invoice number, and generates the hosted invoice URL. With collectionMethod: 'send_invoice', calling send emails the customer a link to that page.

const finalized = await vinr.invoices.finalize(invoice.id);
// finalized.status === 'open'
// finalized.hostedInvoiceUrl → VINR-hosted, pays by card/wallet/local method

await vinr.invoices.send(finalized.id);   // delivers the email

After finalize, line items are immutable. To correct a sent invoice, void it and issue a new one — never edit amounts out of band.

Collect paymentAsk

The customer pays on the hosted page, which supports cards, wallets, and any local methods you've enabled. Each invoice generates an underlying pay_ you can inspect, but you don't need to build a checkout yourself.

Fulfil from the webhook so it happens exactly once, even if the customer never returns to your site.

export async function POST(req: Request) {
  const event = vinr.webhooks.verify(
    await req.text(),
    req.headers.get('x-vinr-signature'),
  );

  switch (event.type) {
    case 'invoice.paid':
      await markProjectPaid(event.data.metadata.projectId);   // idempotent!
      break;
    case 'invoice.payment_failed':
      await alertAccountManager(event.data.id);
      break;
  }
  return new Response('OK', { status: 200 });
}

ReconcileAsk

To check status server-side or build your own dashboard, retrieve the invoice. The amountPaid and amountRemaining fields (minor units) tell you exactly where it stands.

const inv = await vinr.invoices.retrieve(invoice.id);
console.log(inv.status, inv.amountPaid, inv.amountRemaining);
StatusMeaning
draftEditable; not yet issued
openFinalized and awaiting payment
paidSettled in full
voidCancelled before payment
uncollectibleWritten off after dunning

Paid invoices feed into your payouts and settlements like any other charge.

Test itAsk

Finalize and send a sandbox invoice, then open the hostedInvoiceUrl and pay with a test card:

CardResult
4242 4242 4242 4242Success → invoice.paid
4000 0000 0000 0002Declined → invoice.payment_failed
4000 0000 0000 32203D Secure challenge

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page