@timbrix/sdk
TypeScript/JavaScript client for the Timbrix API — CFDI 4.0 electronic invoicing for Mexico. Fully typed (no any), works in Node 18+ and the browser, and normalizes every failed request into one typed error class.
Installation
npm install @timbrix/sdk
pnpm add @timbrix/sdk
yarn add @timbrix/sdk
Quickstart (< 5 minutes)
Sign up at app.timbrix.mx/register — no credit card required. Timbrix generates a sandbox API key automatically for your organization; copy it from Settings → API Keys. CFDI stamped with a sandbox key have no real fiscal validity (they use Timbrix's test PAC), so you can experiment freely.
import { Timbrix } from "@timbrix/sdk"
const client = new Timbrix({ apiKey: "sk_test_..." })
const customer = await client.customers.create({
legalName: "Cliente de Prueba SA de CV",
taxId: "DUM901231AB3",
taxSystem: "601",
email: "facturas@cliente-prueba.mx",
defaultInvoiceUse: "G01",
addressStreet: "Blvd. Atardecer",
addressExterior: "142",
addressNeighborhood: "Centro",
addressCity: "Huatabampo",
addressMunicipality: "Huatabampo",
addressZip: "86500",
addressState: "Sonora",
})
const product = await client.products.create({
description: "Servicio de prueba",
productKey: 84111506,
unitKey: "E48",
price: 100.0,
})
const invoice = await client.invoices.create({
type: "I",
series: "A",
folioNumber: "1",
date: new Date().toISOString(),
paymentForm: "01",
paymentMethod: "PUE",
use: "G03",
customerId: customer.id,
items: [{ productId: product.id, quantity: 1, amount: 100.0 }],
})
console.log(invoice.uuid)
Move to production by swapping the sandbox key for a live one (sk_live_...) — the base URL, request shapes, and response shapes are identical in both environments.
Authentication
The SDK supports two auth strategies, mutually exclusive (bearerToken takes precedence if both are set):
import { Timbrix } from "@timbrix/sdk"
const client = new Timbrix({ apiKey: "sk_live_..." })
const client = new Timbrix({ bearerToken: "eyJhbGciOi..." })
baseUrl defaults to http://localhost:3001 (for local development against the API); pass baseUrl: "https://api.timbrix.mx" for sandbox or production traffic — same URL for both, since the environment is determined by which API key you use.
Swap credentials on an existing client (e.g. after a token refresh in a CLI) with client.setAuth(bearerToken?, apiKey?).
Sandbox vs. production
There's no separate sandbox base URL — sk_test_... (or any key created while livemode is off) and sk_live_... keys hit the same https://api.timbrix.mx, and the API scopes every request to the environment the key belongs to. When authenticating with a bearer token instead, pass environment: "sandbox" | "production" on the methods that accept it (invoices.list, invoices.reportSummary, invoices.exportCsv, etc.) to filter explicitly — omitting it defaults to "production" for those report endpoints, since sandbox timbres carry no fiscal weight.
API Reference
Invoices (CFDI 4.0)
const invoice = await client.invoices.create({
series: "A",
folioNumber: "1",
date: new Date().toISOString(),
paymentForm: "01",
use: "G03",
customerId: "customer-id",
items: [{ productId: "product-id", quantity: 1, amount: 100 }],
})
const pdfPreview = await client.invoices.previewPdf({ ...sameShapeAsCreate })
const { data, total } = await client.invoices.list({
page: 1,
limit: 20,
status: "vigente",
dateFrom: "2026-01-01",
dateTo: "2026-01-31",
})
const one = await client.invoices.get("folio-fiscal-uuid")
const xml = await client.invoices.getXml("folio-fiscal-uuid")
const pdf = await client.invoices.getPdf("folio-fiscal-uuid")
await client.invoices.cancel("folio-fiscal-uuid", { motivo: "02" })
await client.invoices.sendEmail("folio-fiscal-uuid", {
to: "cliente@example.com",
})
const usage = await client.invoices.usage()
const summary = await client.invoices.reportSummary({ dateFrom: "2026-01-01" })
const csv = await client.invoices.exportCsv({ status: "vigente" })
const xmlZip = await client.invoices.exportXmlZip()
const pdfZip = await client.invoices.exportPdfZip()
Customers
await client.customers.list()
await client.customers.get("customer-id")
await client.customers.create({
legalName: "Acme SA de CV",
taxId: "XAXX010101000",
taxSystem: "601",
email: "facturas@acme.mx",
defaultInvoiceUse: "G03",
addressStreet: "Reforma",
addressExterior: "1",
addressNeighborhood: "Centro",
addressCity: "CDMX",
addressMunicipality: "Cuauhtemoc",
addressZip: "06000",
addressState: "CDMX",
})
await client.customers.update("customer-id", { legalName: "New name" })
await client.customers.delete("customer-id")
Products
await client.products.list({ description: "widget" })
await client.products.get("product-id")
await client.products.create({
description: "Widget",
productKey: 84111506,
price: 100,
})
await client.products.update("product-id", { price: 150 })
await client.products.delete("product-id")
const result = await client.products.importCsv(csvBlob, "products.csv")
SAT catalogs
await client.sat.regimenes({ tipoPersona: "fisica" })
await client.sat.usosCfdi({ tipoPersona: "moral", regimen: "601" })
await client.sat.searchClavesProdServ({ q: "arroz", limit: 10 })
await client.sat.searchClavesUnidad({ q: "kilo" })
Organizations
await client.organizations.create({ name: "Acme Corp", slug: "acme-corp" })
await client.organizations.get("acme-corp")
await client.organizations.update("acme-corp", { name: "New Name" })
await client.organizations.updateLegalData("acme-corp", {
legal_name: "Acme SA de CV",
})
await client.organizations.delete("acme-corp")
await client.organizations.members("acme-corp")
await client.organizations.invite("acme-corp", {
email: "a@b.com",
role: "member",
})
await client.organizations.removeMember("acme-corp", "member-id")
await client.organizations.updateMemberRole("acme-corp", "member-id", {
role: "admin",
})
await client.organizations.invites("acme-corp")
await client.organizations.cancelInvite("acme-corp", "invite-id")
Webhooks
await client.webhooks.list("organization-id")
await client.webhooks.get("organization-id", "webhook-id")
await client.webhooks.create("organization-id", {
url: "https://example.com/webhooks",
events: ["invoice.stamped", "invoice.cancelled"],
})
await client.webhooks.update("organization-id", "webhook-id", {
isActive: false,
})
await client.webhooks.delete("organization-id", "webhook-id")
await client.webhooks.deliveries("organization-id", "webhook-id")
await client.webhooks.test("organization-id", "webhook-id")
API Keys
await client.apiKeys.list("organization-id")
await client.apiKeys.get("organization-id", "key-id")
const created = await client.apiKeys.create("organization-id", {
name: "Production Key",
})
console.log(created.plainKey)
await client.apiKeys.update("organization-id", "key-id", { name: "Renamed" })
await client.apiKeys.delete("organization-id", "key-id")
await client.apiKeys.stats("organization-id")
await client.apiKeys.validate()
OAuth apps & tokens
const app = await client.oauth.createApp({
name: "My App",
scopes: ["read:invoices"],
})
await client.oauth.getApp(app.clientId)
await client.oauth.listApps("organization-id")
await client.oauth.updateApp(app.clientId, { name: "Renamed" })
await client.oauth.deleteApp(app.clientId)
await client.oauth.listTokens(app.clientId)
await client.oauth.revokeToken("token-id")
const token = await client.oauth.generateToken({
clientId: app.clientId,
clientSecret: "...",
scopes: ["read:invoices"],
})
await client.oauth.exchangeCode({
code: "auth-code",
clientId: app.clientId,
clientSecret: "...",
redirectUri: "https://example.com/callback",
})
await client.oauth.refreshToken({ refreshToken: token.refresh_token! })
Auth & Users
const { access_token } = await client.auth.login({
email: "a@b.com",
password: "...",
})
await client.auth.logout()
await client.users.me()
await client.users.getById("user-id")
await client.users.getByEmail("a@b.com")
await client.users.getOrganizations("user-id")
Error handling
Every failed request rejects with a TimbrixApiError — never ky's generic HTTPError. It carries the API's own message (validation arrays are joined into one string), the domain error code, an optional suggestion, and the HTTP status:
import { Timbrix, TimbrixApiError } from "@timbrix/sdk"
const client = new Timbrix({ apiKey: "sk_live_..." })
try {
await client.organizations.get("non-existent")
} catch (error) {
if (error instanceof TimbrixApiError) {
console.error(error.statusCode)
console.error(error.code)
console.error(error.message)
console.error(error.suggestion)
}
}
Rate-limit errors (statusCode === 429) get a (retry after Ns) hint appended to message automatically when the API sends a Retry-After header.
TypeScript support
Every request/response shape is exported, so you rarely need to write your own types:
import type {
CreateInvoiceInput,
Invoice,
Customer,
Product,
Webhook,
WebhookEvent,
ApiKey,
DomainErrorCode,
} from "@timbrix/sdk"
License
MIT