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/sdkCreate 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 })| Option | Default | What it does |
|---|---|---|
apiKey | ERA_API_KEY from the environment | An era_… key. Server-side only. |
accessToken | — | An OAuth token (erat_…) or () => string | Promise<string>. Wins over apiKey. |
baseUrl | ERA_BASE_URL, else https://app.getera.io | Point at another Era, e.g. http://localhost:3001 in development. |
maxRetries | 2 | Retries on 429, 5xx and network errors — for reads, and for writes that carry an idempotency key. |
fetch | global fetch | Your 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 moduleTypes
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
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