NodeRailsCRYPTO PAYMENT INFRASTRUCTURE
DocumentationAPI Reference
Dashboard

Invoices

Invoices let you bill specific customers with line items, tax, and a one-click payment link. The customer receives an email with a pay button that opens the hosted checkout.

Full flow

An invoice follows this lifecycle: DRAFTOPENPAID. You can also VOID an unpaid invoice at any time.

Step 1: Create an invoice

Create invoicetypescript
const invoice = await noderails.invoices.create({
  customerAccountId: 'customer-id',
  currency: 'USD',
  dueDate: '2026-04-01T00:00:00Z',
  memo: 'March consulting services',
  items: [
    { description: 'Strategy consulting (10 hrs)', amount: '1500.00', quantity: 1 },
    { description: 'Implementation support', amount: '750.00', quantity: 1 },
  ],
});

console.log(invoice.id);     // Invoice ID
console.log(invoice.status); // "DRAFT"

Step 2: Open the invoice

Transition the invoice from DRAFT to OPEN. Once open, the invoice is finalized and can be paid.

Open invoicetypescript
await noderails.invoices.open(invoice.id);

Step 3: Send to customer

Send the invoice via email. The customer receives an email with the invoice details and a pay button that opens the hosted checkout.

Send invoicetypescript
const result = await noderails.invoices.send(invoice.id);
console.log(result.sent); // true
💡

One step

You can call open and send separately, or just call send which will automatically open a draft invoice before sending.

Step 4: Check status

Check invoice statustypescript
const invoice = await noderails.invoices.retrieve(invoice.id);
console.log(invoice.status); // "DRAFT" | "OPEN" | "PAID" | "VOID"

// Once paid, the full payment intent is included automatically
if (invoice.paymentIntent) {
  console.log(invoice.paymentIntent.status);              // "CAPTURED"
  console.log(invoice.paymentIntent.captureTxHash);       // "0x..." (on-chain tx hash)
  console.log(invoice.paymentIntent.cryptoAmount);        // "2250000000" (in token's smallest unit)
  console.log(invoice.paymentIntent.authorizationChainId); // 8453 (chain used)
  console.log(invoice.paymentIntent.authorizationTokenKey); // "USDC-8453" (token used)
}
💡

No extra calls needed

When you retrieve an invoice, the full paymentIntent object is automatically included once the customer has paid. You don't need to make a separate call to paymentIntents.retrieve().

List invoices

List and filtertypescript
// List all open invoices
const invoices = await noderails.invoices.list({ status: 'OPEN' });

for (const inv of invoices.data) {
  console.log(inv.id, inv.status, inv.amount);
}

Void an invoice

Void an unpaid invoice. This marks it as cancelled and prevents the customer from paying.

Void invoicetypescript
await noderails.invoices.void('invoice-id');

Webhooks

EventDescription
invoice.createdInvoice was created
invoice.sentInvoice was emailed to customer
invoice.paidCustomer paid the invoice
invoice.voidedInvoice was voided

Methods reference

MethodDescription
create(params)Create a new invoice
retrieve(id)Retrieve an invoice by ID
list(params?)List invoices with optional filters
open(id)Transition from DRAFT to OPEN
send(id)Send the invoice via email
void(id)Void an unpaid invoice

TypeScript types

Type importstypescript
import type {
  Invoice,
  InvoiceCreateParams,
  InvoiceListParams,
} from '@noderails/sdk';

Response body reference

All responses are wrapped in { success: true, data: ... }. The fields below describe what's inside data.

create() response

Invoice (create)

idstringUnique invoice ID (UUID)
appIdstringYour app ID
customerAccountIdstringCustomer being billed
subscriptionIdstring | nullLinked subscription, if auto-generated
paymentIntentIdnullAlways null at creation (set when paid)
invoiceNumberstringSequential number, e.g. "INV-00001"
statusstring"DRAFT" at creation
subtotalstringPre-tax total (Decimal as string)
taxAmountstringTax portion (Decimal as string)
totalstringFinal total including tax
currencystringCurrency code, default "USD"
taxRateIdstring | nullApplied tax rate ID
dueDatestring | nullISO 8601 due date
paidAtnullSet when invoice is paid
voidedAtnullSet when invoice is voided
periodStartstring | nullBilling period start (subscriptions)
periodEndstring | nullBilling period end (subscriptions)
allowedChainsstring | number[]"ALL" or array of chain IDs
allowedTokensstring | string[]"ALL" or array of token keys
memostring | nullNote to customer
metadataobjectYour metadata key-value pairs
createdAtstringISO 8601 creation timestamp
updatedAtstringISO 8601 last update timestamp
itemsInvoiceItem[]Line items (see below)
customerAccountCustomerAccountFull customer object
taxRateTaxRate | nullFull tax rate object, if applied

InvoiceItem (nested in items[])

idstringItem ID (UUID)
invoiceIdstringParent invoice ID
productPlanIdstring | nullLinked product plan
productPlanPriceIdstring | nullLinked price
taxRateIdstring | nullItem-level tax rate
descriptionstringItem description
amountstringItem amount (Decimal as string)
currencystringItem currency
quantitynumberItem quantity
taxAmountstringTax for this item
createdAtstringISO 8601 timestamp

retrieve() response

Returns all fields from create() above, plus these additional nested objects:

Additional fields on retrieve

appAppFull app object (id, name, environment, etc.)
paymentIntentPaymentIntent | nullFull payment intent with nested transactions[], once paid
items[].taxRateTaxRate | nullTax rate on each individual item
💡

retrieve() includes transactions

The paymentIntent on retrieve also includes its transactions[] array, so you can see all on-chain tx hashes, statuses, and block numbers.

list() response

Same as retrieve() but without the app relation. Includes items (with nested taxRate),customerAccount, taxRate, and paymentIntent (with transactions).

Paginated response shapejson
{
  "success": true,
  "data": [ /* Invoice[] */ ],
  "pagination": {
    "total": 42,
    "page": 1,
    "pageSize": 20,
    "totalPages": 3
  }
}

open() and void() response

Both return the invoice with items[] only (no customer, no tax rate, no payment intent). The status will be "OPEN" or "VOID" respectively.

send() response

Returns a simple confirmation:

Send responsejson
{ "success": true, "data": { "sent": true } }