
Research
/Security News
16 Malicious Firefox Extensions Steal Cryptocurrency Wallet Credentials
Socket found 16 malicious Firefox extensions designed to steal crypto wallet recovery phrases and private keys using cloned Rabby and OKX interfaces.
@timbrix/sdk
Advanced tools
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.
npm install @timbrix/sdk
# or
pnpm add @timbrix/sdk
# or
yarn add @timbrix/sdk
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_..." })
// The organization is resolved from the key itself — no organizationId needed.
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) // the folio fiscal assigned by the SAT via the PAC
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.
The SDK supports two auth strategies, mutually exclusive (bearerToken takes precedence if both are set):
import { Timbrix } from "@timbrix/sdk"
// API key — server-to-server integrations. Sent as X-API-Key.
// The organization is resolved from the key, so most resource methods
// accept an optional organizationId instead of a required one.
const client = new Timbrix({ apiKey: "sk_live_..." })
// Bearer token — acting as a logged-in Timbrix user (e.g. from OAuth).
// Sent as Authorization: Bearer <token>. organizationId is required on
// resource methods that need it.
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?).
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.
// Stamp (timbrar) a new CFDI — type: "I" (Ingreso, default), "E" (Egreso), or "T" (Traslado)
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 }],
})
// Preview a PDF without stamping (no quota consumed, nothing persisted)
const pdfPreview = await client.invoices.previewPdf({ ...sameShapeAsCreate })
// List, paginated and filterable
const { data, total } = await client.invoices.list({
page: 1,
limit: 20,
status: "vigente",
dateFrom: "2026-01-01",
dateTo: "2026-01-31",
})
// Get / download
const one = await client.invoices.get("folio-fiscal-uuid")
const xml = await client.invoices.getXml("folio-fiscal-uuid") // Blob
const pdf = await client.invoices.getPdf("folio-fiscal-uuid") // Blob
// Cancel per SAT rules (may return cancellationStatus: "pendiente" if the
// receptor's approval is required — Timbrix can't confirm that in real time)
await client.invoices.cancel("folio-fiscal-uuid", { motivo: "02" })
// Email the CFDI (XML + PDF) to the receptor
await client.invoices.sendEmail("folio-fiscal-uuid", {
to: "cliente@example.com",
})
// Current-month CFDI usage against the plan's quota
const usage = await client.invoices.usage()
// Fiscal report summary + CSV/XML/PDF bulk export (capped at 500 invoices/request)
const summary = await client.invoices.reportSummary({ dateFrom: "2026-01-01" })
const csv = await client.invoices.exportCsv({ status: "vigente" }) // Blob
const xmlZip = await client.invoices.exportXmlZip() // Blob
const pdfZip = await client.invoices.exportPdfZip() // Blob
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")
await client.products.list({ description: "widget" }) // filters are optional
await client.products.get("product-id")
await client.products.create({
description: "Widget",
productKey: 84111506, // SAT c_ClaveProdServ code
price: 100,
})
await client.products.update("product-id", { price: 150 })
await client.products.delete("product-id")
// Bulk import from a CSV file (Blob/File) — partial success, one error per bad row
const result = await client.products.importCsv(csvBlob, "products.csv")
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" })
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")
// CSD (owner only) — Buffer/Uint8Array in Node, Blob/File in the browser
await client.organizations.uploadCertificates("acme-corp", {
certificate: await readFile("certificate.cer"),
privateKey: await readFile("private.key"),
password: "your_certificate_password",
})
await client.organizations.deleteCertificates("acme-corp")
// Members & invites
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")
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")
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) // only returned once, at creation
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() // validates the currently-authenticated key
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")
// Client credentials flow
const token = await client.oauth.generateToken({
clientId: app.clientId,
clientSecret: "...",
scopes: ["read:invoices"],
})
// Authorization code flow
await client.oauth.exchangeCode({
code: "auth-code",
clientId: app.clientId,
clientSecret: "...",
redirectUri: "https://example.com/callback",
})
await client.oauth.refreshToken({ refreshToken: token.refresh_token! })
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")
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) // 404
console.error(error.code) // "NOT_FOUND"
console.error(error.message) // "Organization not found"
console.error(error.suggestion) // e.g. "Check the organization id" (when the API provides one)
}
}
Rate-limit errors (statusCode === 429) get a (retry after Ns) hint appended to message automatically when the API sends a Retry-After header.
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"
MIT
FAQs
TypeScript SDK for Timbrix API
The npm package @timbrix/sdk receives a total of 594 weekly downloads. As such, @timbrix/sdk popularity was classified as not popular.
We found that @timbrix/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Socket found 16 malicious Firefox extensions designed to steal crypto wallet recovery phrases and private keys using cloned Rabby and OKX interfaces.

Product
Socket now scans VS Code extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.

Research
/Security News
Socket uncovered two malicious VS Code themes in a GlassWorm-linked cluster with thousands of installs across VS Code Marketplace and Open VSX.