Skip to main content
BillingInvoices

Invoices.

Invoices are the canonical billing artifact in OpenSettle. Sequential numbers (INV-2026-0001), line items, due dates, optional subscription anchor. Every invoice has a stable hosted URL, a saveable PDF, and a non-custodial pay flow your customer can complete with any wallet on any supported chain.

Create an invoice

Four SDKs, one shape. The customer ID points at an existing customer in your workspace (or pass the email to create one inline). Totals derive from lineItems; unitAmountMinor is in cents (USDC and USDT are USD-pegged, so 100 = $1.00).

create-invoice.ts
import { OpenSettle } from "@opensettle/sdk";

const os = new OpenSettle({
  apiKey: process.env.OPENSETTLE_KEY!,
  workspaceId: process.env.OPENSETTLE_WORKSPACE!,
});

const invoice = await os.invoices.create({
  customerId: "cus_9fX0a2E1",
  chain: "base",
  token: "USDC",
  currency: "USD",
  lineItems: [
    { description: "Implementation, April 2026", quantity: 1, unitAmountMinor: 480_000 },
    { description: "Hosting credits", quantity: 200, unitAmountMinor: 50 },
  ],
  dueInDays: 14,
  memo: "Thanks for working with us.",
});

await os.invoices.send(invoice.id);
create_invoice.py
import os
from opensettle import OpenSettle

client = OpenSettle(
    api_key=os.environ["OPENSETTLE_API_KEY"],
    workspace_id=os.environ["OPENSETTLE_WORKSPACE_ID"],
)

invoice = client.invoices.create(
    customer_id="cus_9fX0a2E1",
    chain="base",
    token="USDC",
    currency="USD",
    line_items=[
        {"description": "Implementation, April 2026", "quantity": 1, "unit_amount_minor": 480_000},
        {"description": "Hosting credits", "quantity": 200, "unit_amount_minor": 50},
    ],
    due_in_days=14,
    memo="Thanks for working with us.",
)

client.invoices.send(invoice["id"])
create_invoice.go
client := opensettle.NewClient(
  os.Getenv("OPENSETTLE_API_KEY"),
  opensettle.WithWorkspace(os.Getenv("OPENSETTLE_WORKSPACE_ID")),
)

invoice, err := client.Invoices.Create(ctx, &opensettle.CreateInvoiceRequest{
  CustomerID: "cus_9fX0a2E1",
  Chain:      "base",
  Token:      "USDC",
  Currency:   "USD",
  LineItems: []opensettle.LineItem{
    {Description: "Implementation, April 2026", Quantity: 1, UnitAmountMinor: 480_000},
    {Description: "Hosting credits", Quantity: 200, UnitAmountMinor: 50},
  },
  DueInDays: 14,
  Memo:      "Thanks for working with us.",
})

_, _ = client.Invoices.Send(ctx, invoice.ID)
create_invoice.rs
use opensettle::{Client, CreateInvoiceRequest, LineItem};

let client = Client::new(
    std::env::var("OPENSETTLE_API_KEY")?,
    std::env::var("OPENSETTLE_WORKSPACE_ID")?,
);

let invoice = client
    .invoices()
    .create(CreateInvoiceRequest {
        customer_id: "cus_9fX0a2E1".into(),
        chain: "base".into(),
        token: "USDC".into(),
        currency: "USD".into(),
        line_items: vec![
            LineItem { description: "Implementation, April 2026".into(), quantity: 1, unit_amount_minor: 480_000 },
            LineItem { description: "Hosting credits".into(), quantity: 200, unit_amount_minor: 50 },
        ],
        due_in_days: Some(14),
        memo: Some("Thanks for working with us.".into()),
        ..Default::default()
    })
    .await?;

client.invoices().send(&invoice.id).await?;

Send + remind

send emails the customer with the hosted pay link and flips the invoice from draft to open. remind re-emails without changing status — useful for ad-hoc follow-ups outside the automated dunning cadence.

send-and-remind.ts
// Initial send — flips status, emails the customer
await os.invoices.send(invoice.id);

// 7 days later, no payment yet — send a reminder
await os.invoices.remind(invoice.id);

Lifecycle

Invoices move through the states below. Transitions emit invoice.* webhooks (subscribe at Settings → Webhooks); terminal states are paid and void.

  • draftcreated but not yet sent. Editable. Not visible to the customer.
  • opensent. Pay-link is live. Reminders fire on the cadence you set.
  • paidon-chain settlement confirmed. Receipt emailed automatically.
  • past_duedue date passed without payment. Still payable.
  • voidcanceled. Pay-link returns 410. Audit trail preserved.

Webhook handler

Listen for invoice.paid to fulfil, ship, or unlock access. The payload includes the on-chain tx_hash and confirmation block so you can verify against your own chain reader if you want belt-and-braces. Signature verification is one line with the SDK; rejecting unverified bodies prevents replay attacks.

webhook.ts
import { verifyWebhook, WebhookVerificationError } from "@opensettle/sdk";

export async function POST(req: Request) {
  const rawBody = await req.text();

  try {
    const { data: event } = verifyWebhook<{ type: string; data: any }>({
      rawBody,                                  // exact bytes — not parsed JSON
      signatureHeader: req.headers.get("x-opensettle-signature"),
      secret: process.env.OPENSETTLE_WEBHOOK_SECRET!,
    });

    if (event.type === "invoice.paid") {
      const inv = event.data;
      await unlockAccess({ customerId: inv.customerId, invoiceId: inv.id });
    }
    return new Response("ok");
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response("invalid signature", { status: 400 });
    }
    throw err;
  }
}

Hosted invoice page

After send, the customer gets an email with a link to https://opensettle.io/i/<token> — your branded invoice with line items, a status pill, and a Download PDF button that uses the browser's native print dialog to produce a clean, attachment-ready PDF. The PDF carries a small Generated by opensettle.iofooter (print-only — not visible on the web page) so the recipient's accountant can trace the source. The link is unguessable (192-bit token) and rate-limited per IP.

Payment terms

Set the window with dueInDays on create — a count of days from now until dueAt. Default is 14, range 0–365. Set 0 for due-on-receipt; pick 7, 30, or 60 to mirror Net-7 / Net-30 / Net-60. Early-payment discounts and late-fee policies aren't live yet — track them on the roadmap.

Reconciliation export

Pass format=csv to the list endpoint to get an accountant-ready export. The CSV carries:

  • Invoice fields: id, number, status, amount_minor, currency, chain, token, due_at, issued_at, paid_at, memo
  • Customer identity: customer_name, customer_email
  • On-chain settlement (paid invoices): payment_tx_hash, payment_amount_usd, payment_confirmed_at

Cells starting with =, +, -, or @ are prefixed with a single quote so Excel / Sheets / Numbers render them as text rather than evaluating as formulas. Scope to a fiscal period with ?from=2026-04-01&to=2026-04-30.

bash
# All invoices (most-recent 5000)
curl -H "Authorization: Bearer $OPENSETTLE_API_KEY" \
  "https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/invoices?format=csv" \
  > invoices.csv

# Q2 2026 invoices only
curl -H "Authorization: Bearer $OPENSETTLE_API_KEY" \
  "https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/invoices?format=csv&from=2026-04-01&to=2026-06-30" \
  > invoices-q2-2026.csv

Or click Export CSV on /app/invoices — the popover surfaces presets for current month, last month, this quarter, year-to-date, and custom date range.

See Reconciliation for accounting-system mappings.