Documentation

Build on Backfill — TypeScript SDK, the HTTP API, and the runtime extensions live inside.

Backfill is the accounting platform you can program. Drop a TypeScript file into your integration project and it runs inside Backfill — in transaction with the data it touches, with typed access to every entity, and with the same primitives we use ourselves.

The SDK has two flavors:

  • ExtensionsdefineExtension(...). Hooks, jobs, custom entities, dashboard pages, UI tabs, action buttons. Anything you bolt onto Backfill’s existing surface lives here.
  • ConnectorsdefineConnector(...). Third-party data sources: Stripe, Shopify, your bank, your warehouse. Streams, signed webhooks, the canonical ingest pipeline.

You can mix freely. A connector that pulls Stripe charges is just an extension with extra connector-shaped manifest entries for data ingress.

What you can build

Here’s an extension hook that adds a sales-tax line to every invoice — but skips it for tax-exempt customers, by loading the customer record and checking a flag:

// src/hooks/invoice/before-save.ts
import { Customer, Settings, Log } from "@backfill-io/sdk";
import type { HookContext } from "@backfill-io/sdk";

export default async function (ctx: HookContext) {
  // Look up the customer the invoice points at.
  const customer = ctx.entity.customer_id
    ? Customer.get(ctx.entity.customer_id)
    : null;

  // Tax-exempt? Leave the invoice alone.
  if (customer?.tax_exempt) {
    Log.info("Skipping tax — customer is exempt", {
      customerId: customer.id,
      exemptionRef: customer.tax_exempt_ref,
    });
    return null;
  }

  const rate = Settings.get("taxRate") ?? 8.25;
  const subtotal = ctx.entity.line_items
    .reduce((s, li) => s + Number(li.amount), 0);
  const tax = Math.round(subtotal * rate / 100 * 100) / 100;

  return {
    line_items: [
      ...ctx.entity.line_items,
      { description: "Sales tax", amount: tax, quantity: 1 },
    ],
    total_amount: subtotal + tax,
  };
}

Customer is the typed entity class — same shape as Entity but pre-bound, so Customer.get(id) reads as well as Entity.get("Customer", id) and gives you autocomplete on the returned record. Every standard entity (Invoice, Payment, Account, …) and every custom entity you declare is exported from @backfill-io/sdk the same way. The handler returns the changes to merge into the invoice — here, just the new line items and total.

No registration. No manifest entry. The folder name says the entity, the filename says the event, the default export is the handler. From there it scales:

  • Validate, transform, or block any writebefore-save returns the changes you want, throws to reject. Runs in the same transaction.
  • Compute taxes, fees, and FX while a draft is being editedrecalculate hooks contribute components to the total in real time.
  • Pull data from anywhere into clean accounting records — Stripe, Shopify, your bank, a custom warehouse — and ingest.canonical(...) lands it as canonical entities with idempotency, signed webhook ingestion, and tenant binding handled for you.
  • Stand up dashboard pages — reconciliation views, close checklists, custom reports — written in JSX with the same components Backfill itself uses.
  • Define your own typed records — custom entities with field validation, typed CRUD, and the option to surface as a panel on built-in entities like Invoice or Payment.
  • Run on a schedule — drop a file in src/jobs/, export a cron string and a run function. Nightly reconciliation, weekly reports, hourly polls.
  • Add tabs, panels, and action buttons to existing entity pages — declared once, rendered everywhere the entity surfaces.

The runtime is sandboxed, permissions are explicit in the manifest, and backfill deploy --observe lets you ship a new version that runs against live traffic without committing writes — promote when you’re satisfied.

Where to go next