StartSDK

SDK

@getera/sdk — every Era model and action, typed, in Node, Bun, Deno, edge runtimes and the browser.

@getera/sdk is the Era API as a typed client. Every resource is a property (era.orders, era.leads, era.customers), every action is a method on it (era.orders.cancelOrder(id)), and every input and model has a TypeScript type — all generated from the same OpenAPI document the API serves, so a new action in Era is a new method the next time the SDK is generated. It has no dependencies and runs anywhere fetch does.

Install

npm install @getera/sdk
# or: bun add @getera/sdk · pnpm add @getera/sdk · yarn add @getera/sdk

Create a client

import { createEra } from "@getera/sdk"

// On a server, with an API key (Settings → Advanced → API keys):
const era = createEra({ apiKey: process.env.ERA_API_KEY })

// …or for someone who signed in with OAuth (an access token, or a function that returns a fresh one):
const eraFor = (token: string) => createEra({ accessToken: token })
OptionDefaultWhat it does
apiKeyERA_API_KEY from the environmentAn era_… key. Server-side only.
accessToken—An OAuth token (erat_…) or () => string | Promise<string>. Wins over apiKey.
baseUrlERA_BASE_URL, else https://app.getera.ioPoint at another Era, e.g. http://localhost:3001 in development.
maxRetries2Retries on 429, 5xx and network errors — for reads, and for writes that carry an idempotency key.
fetchglobal fetchYour own fetch (tests, proxies).
userAgent—Sent where the runtime allows it.

Where it runs

Node 18+, Bun and Deno need nothing extra. Keep the key in an environment variable.

import { createEra } from "@getera/sdk"
const era = createEra() // reads ERA_API_KEY and ERA_BASE_URL
console.log((await era.me()).user.name)

Cloudflare Workers, Vercel Edge and similar: pass the key from the platform's secrets.

import { createEra } from "@getera/sdk"

export default {
  async fetch(_req: Request, env: { ERA_API_KEY: string }) {
    const era = createEra({ apiKey: env.ERA_API_KEY })
    const { data } = await era.orders.list({ status: "unfulfilled", limit: 10 })
    return Response.json(data)
  },
}

Never ship an API key to a browser. In a browser use the access token of the person who is signed in (from your own OAuth flow, see Authentication) — it can do only what they can.

import { createEra } from "@getera/sdk"
const era = createEra({ accessToken: () => session.getFreshToken() })

For a public storefront (catalogue, cart, checkout, forms, bookings for visitors) use @getera/storefront with your site's publishable id instead.

Read

// One page, filtered by any column. Operators: [gte] [lte] [in]; null matches empty.
const page = await era.orders.list({ status: "unfulfilled", "created_at[gte]": "2026-10-01", order: "-created_at", limit: 50 })
page.data        // SalesOrder[]
page.next_cursor // pass as { cursor } for the next page, null at the end

// Every row, page by page:
for await (const order of era.orders.listAll({ charge_status: "none" })) {
  console.log(order.number, order.status)
}

// One record (throws EraError 404 when it is not there or not yours to see):
const order = await era.orders.get("ORDER_ID")

// Only some columns, or a text search:
await era.customers.list({ fields: "id,name,email", q: "ada" })

Write

Every write runs an action — the same one the button in the app runs — and returns { ok, result, events }.

// REST-style aliases, where the module has a plain create / update / delete:
const created = await era.customers.create({ name: "Ada Lovelace", email: "[email protected]" })
await era.customers.update("CUSTOMER_ID", { notes: "Prefers email" })

// An action on one record: (id, input?)
await era.orders.cancelOrder("ORDER_ID", { reason: "Customer asked" })
await era.orders.fulfilOrder("ORDER_ID", { lines: [{ orderLineId: "LINE_ID", warehouseId: "WAREHOUSE_ID", quantity: 1 }] })

// An action on the collection: (input)
await era.leads.captureLead({ customerId: "CUSTOMER_ID", message: "Wants the yearly plan" })

// Any action by its full name — typed by its input:
await era.actions.run("crm.capture_lead", { customerId: "CUSTOMER_ID" })

The method name is the action's verb in camelCase (cancel_order → cancelOrder) and the resource is the camelCase of its URL name (order_lines → era.orderLines). The reference shows the exact SDK call for every action.

Check the input

The exact fields of each action — fulfilOrder's lines above is an example — are in its section of the reference and in its TypeScript type (RetailFulfilOrderInput). Your editor completes them.

Safe retries

Give a write an idempotency key and the SDK may retry it safely; Era replays the first result for 24 hours instead of doing the work twice.

await era.orders.cancelOrder("ORDER_ID", { reason: "Duplicate" }, { idempotencyKey: crypto.randomUUID() })

Every call also takes { signal } to cancel it with an AbortController.

Errors

Every non-2xx answer throws EraError:

import { EraError } from "@getera/sdk"

try {
  await era.leads.captureLead({ customerId: "00000000-0000-0000-0000-000000000000" })
} catch (e) {
  if (!(e instanceof EraError)) throw e
  e.status    // 409
  e.code      // "customer_not_found" — the precondition's code
  e.problems  // for 422: [{ field, message }]
  e.requestId // quote it when you contact support
}

code is stable: unauthorized, invalid_token, forbidden, not_found, invalid_input, rate_limited, idempotency_in_progress, idempotency_conflict, network_error, or the action's own precondition code.

Who am I, what may I do, and memory

const me = await era.me()          // { user, workspace, credential: { kind, scopes }, modules: { crm: ["read","write"], … } }
const actions = await era.actions.list() // every action this credential may call, with its input schema and refusals
const { results } = await era.memory.recall("what did Ada ask for last time?", { limit: 5 }) // needs the AI module

Types

Every model is exported under its table's name in PascalCase (sales_order → SalesOrder, lead → Lead), every action input after its action (crm.capture_lead → CrmCaptureLeadInput), and ActionName is the union of all action names.

import type { SalesOrder, CrmCaptureLeadInput, ActionName } from "@getera/sdk"

Example: a customer and a lead

create-lead.ts
import { createEra, EraError } from "@getera/sdk"

const era = createEra({ apiKey: process.env.ERA_API_KEY })

const customer = await era.customers.create(
  { name: "Ada Lovelace", email: `ada+${Date.now()}@example.com` },
  { idempotencyKey: crypto.randomUUID() },
)
const customerId = (customer.result as { id: string }).id
const lead = await era.leads.captureLead({ customerId, message: "Wants the yearly plan" })
console.log("lead", (lead.result as { id: string }).id, "events", lead.events.join(", "))
ERA_API_KEY=era_… bun create-lead.ts