API routes

Learn about HTTP endpoints under your extension's namespace.

API routes let external services (or your own UI) talk to your extension.

File location

Files at src/api/<path>.ts auto-register as HTTP handlers at /<path> — mounted under your extension’s namespace. The HTTP verb is the export name (GET, POST, PUT, PATCH, DELETE).

FileURL
src/api/tax-summary.ts/tax-summary
src/api/sync/customers.ts/sync/customers
src/api/webhooks/stripe.ts/webhooks/stripe

A minimal route

// src/api/tax-summary.ts
declare const TaxRecord: any;

export const GET = async function (ctx: {
  searchParams: Record<string, string>;
}) {
  const { start, end } = ctx.searchParams;

  if (!start || !end) {
    return {
      __api_response: true,
      status: 400,
      body: { error: "Missing required query parameters: start, end (YYYY-MM-DD)" },
    };
  }

  const records = TaxRecord.query({
    where: { _created_at: { gte: start, lte: end } },
  });

  return { period: { start, end }, records };
};

The handler signature

The runtime injects a context with the parsed request:

{
  searchParams: Record<string, string | string[]>;
  params: Record<string, string | string[]>;   // path parameters where supported
  body: any;
  rawBody: string;
  headers: Record<string, string>;
  context: { tenant: { id: string }, extension: { key: string } };
}

Returning responses

Plain object → 200 JSON

return { ok: true, count: 42 };

Explicit response object

return {
  __api_response: true,
  status: 400,
  body: { error: "Missing param" },
};

The api helper

The same shape, written more naturally:

import { api } from "@backfill-io/sdk";

export const POST = api(async (request) => {
  if (!request.body?.email) {
    return api.badRequest("Email required");
  }
  return api.json({ ok: true });
});

api exposes:

api(handler)                                // wraps a handler with type inference
api.json(data, { status })                  // 200 (or status) JSON
api.badRequest(message, data?)              // 400
api.notFound(message)                       // 404
api.forbidden(message)                      // 403
api.unauthorized(message)                   // 401
api.error(message, { status })              // generic

Auth modes

Routes declare an auth mode via an exported config:

export const config = { auth: "api_key" };

For plain extensions, two modes are accepted:

ModeEffect
api_keyDefault. The platform requires a workspace API token in Authorization: Bearer <token>.
publicNo auth. The handler is responsible for any verification (use sparingly).

A third mode (stripe_signature, etc.) is used by connector webhook routes, gated to connectors.

Method exports

A single route file can declare multiple verbs:

export const config = { auth: "api_key" };

export const GET = async (ctx) => { ... };
export const POST = api(async (req) => { ... });

At least one of GET, POST, PUT, PATCH, DELETE must be exported, or deploy fails:

route must export at least one HTTP method handler (e.g. export const POST = api(...))