Easy Labs
BillingGuides

Send an invoice

Issue an ad-hoc invoice and email it to a customer.

Goal

Create an invoice from line items, send it to a customer's email, and (optionally) attach the PDF. Use this for one-off billing — quotes, professional services, milestone payments — or when you want the customer to pay through a hosted page rather than charging a saved instrument. For recurring billing of a fixed plan, use subscriptions instead.

Prerequisites

  • A sandbox or production API key
  • @easylabs/node installed
  • A to_email for the recipient (and optionally a buyer_id linking to a Customer)

Implementation

1. Create the invoice

Items carry description, quantity, and unit_price in the smallest currency unit. The invoice starts in DRAFT; pass collection_method to control how it gets paid once finalized.

import { createClient } from "@easylabs/node";

const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });

const { data: invoice } = await easy.createInvoice({
  to_email: "ada@example.com",
  to_company_name: "Lovelace Labs",
  buyer_id: "cus_01HXXXXXXXXXXX",
  currency: "USD",
  collection_method: "send_invoice", // customer pays via hosted page
  due_date: "2026-06-01",
  items: [
    { description: "Q2 retainer", quantity: 1, unit_price: 500_000 }, // $5,000.00
    { description: "Add-on workshop", quantity: 2, unit_price: 75_000, manual_tax_rate: 8.875 },
  ],
  notes: "Thanks for working with us — net 30.",
  auto_reminders: true,
  include_payment_page_link: true,
});

2. Send the email

sendInvoice finalizes the invoice (DRAFT → OPEN), delivers the email, and returns the updated record. Pass attach_pdf: true to embed the PDF.

await easy.sendInvoice(invoice.id, {
  attach_pdf: true,
  cc_recipients: ["billing@lovelacelabs.example"],
});

To schedule the send for a future time, pass scheduled_send_at instead of calling immediately, or set it on createInvoice and skip the manual sendInvoice step.

3. Charge automatically (optional)

If you'd rather collect against the customer's default payment instrument the moment the invoice opens, pass collection_method: "charge_automatically" on create and call payInvoice (or let the platform pull on due_date):

await easy.payInvoice(invoice.id, {
  // omit instrument_id to use the customer's default
  idempotency_key: `invoice-${invoice.id}-attempt-1`,
});

4. React to status

Listen for invoice.finalized, invoice.paid, invoice.payment_failed, invoice.voided, and invoice.marked_uncollectible. Send manual nudges with remindInvoice(invoice.id) between auto-reminders, or voidInvoice(invoice.id) to cancel an open one.

Tradeoffs

  • send_invoice vs. charge_automatically — pick send_invoice when the customer expects to pay manually (typical for B2B). Pick charge_automatically only when you have an instrument on file and the customer has agreed to be billed.
  • Recurring invoices (is_recurring: true + recurrence_interval) are simpler than subscriptions but lack proration, item-level mutations, and the dunning lifecycle. Choose subscriptions when the relationship is open-ended.
  • Taxmanual_tax_rate on each item is the lowest-friction option. For multi-jurisdiction tax, set tax_rate_id on prices and let the platform compute totals.

On this page