@timbro/invoices
v0.11.0
Published
Readme
@timbro/invoices
Create, retrieve, and download Dominican electronic invoices from your application, ERP, or POS. Invoice issuance and payment collection have separate lifecycles. Use @timbro/payments when you also need to collect money through Checkout.
npm install @timbro/invoicesUse a server-side key with the Invoices capability. The client defaults to https://api.timbro.do; a dedicated Invoice installation uses the same document calls with its own baseUrl and Source key. The credential selects the business and environment.
import { createTimbroInvoices, type InvoiceRequest } from "@timbro/invoices";
const apiKey = process.env.TIMBRO_API_KEY;
if (apiKey === undefined || apiKey.trim() === "") {
throw new Error("TIMBRO_API_KEY is required");
}
const timbro = createTimbroInvoices({ apiKey });
const request = {
invoiceReference: "sale-123",
type: "E32",
currency: "DOP",
incomeType: "01",
paymentTerms: { type: "immediate" },
items: [{
description: "Servicio",
quantity: "1",
unit: "unit",
kind: "service",
unitPrice: "1250.00",
tax: { kind: "itbis_18", included: true },
}],
} satisfies InvoiceRequest;
const invoice = await timbro.invoices.create(request);
const current = await timbro.invoices.retrieve(invoice.id);
if (current?.files.pdf.status === "ready") {
const pdf = await timbro.invoices.downloadPdf(current.id);
// Send pdf.bytes through your application's authorized download route.
}For a hosted Gateway deployment, the same configured origin and API key also expose Business setup and WebhookEndpoint operations. Use mode: "test" or "live" for environment-specific setup and key issuance; sending a TesteCF invoice is a separate test-only operation.
const business = await timbro.business.retrieve();
await timbro.business.configureInvoicing({
mode: "test",
idempotencyKey: "configure-invoicing-test-1",
});
const testInvoice = await timbro.business.issueTestInvoice();
const invoiceKey = await timbro.business.issueInvoiceKey({
mode: "test",
idempotencyKey: "issue-invoice-key-test-1",
});
const endpoint = await timbro.webhookEndpoints.create({
url: "https://merchant.example/webhooks/timbro",
enabledEvents: ["invoice.accepted", "invoice.rejected", "invoice.operatorRequired", "invoice.ready"],
idempotencyKey: "create-invoice-webhook-1",
});
const deliveries = await timbro.webhookEndpoints.listDeliveries({
webhookEndpointId: endpoint.id,
limit: 50,
});
if (deliveries.hasMore && deliveries.nextCursor !== null) {
const nextPage = await timbro.webhookEndpoints.listDeliveries({
webhookEndpointId: endpoint.id,
limit: 50,
cursor: deliveries.nextCursor,
});
}business.retrieve returns current capabilities, setup status, and requirements. Update only the business members being replaced with business.update; import a signing certificate with business.importSigningCertificate; and read the business again to confirm retained state. issueTestInvoice always uses the test environment. Invoice API keys are issued for one explicit mode and returned once. The Gateway enforces credential authority for each task.
Webhook endpoints support list, create, retrieve, delete, delivery listing, event replay, secret rotation, suspension, and reactivation. Lists return data, hasMore, and nextCursor; pass the cursor to the next page with the same filters. Delivery eventType remains a string so stored deliveries from earlier event contracts can still be read. Use a Gateway host for these hosted controls; native Source origins expose their Source operations separately.
Persist the request and its invoiceReference before submitting it. An equivalent retry returns the same retained Invoice ID and current state. Changed content under that reference returns invoice_reference_conflict. An optional idempotencyKey in the second argument binds a command independently of the reference. A timeout or lost response does not justify a new reference or a second invoice.
For ERP attribution, the optional source: {id, version} identifies the immutable accounting snapshot. When omitted, the reference supplies the source identity and the version is initial. Odoo keeps ownership of its posted accounting facts, prepared requests, and durable retry work.
Amounts are exact decimal strings. unitPrice is interpreted using the item's tax.included flag. Timbro groups items by tax kind and tax.included, and rounds each group's taxable base and ITBIS once from the exact quantity × unit price amounts; a legal tip is 10% of each tipped item's value without ITBIS. incomeType is explicit when supplied; otherwise the issuer's configured default must exist. A create request may supply expectedTotals to assert accounting agreement. A mismatch refuses admission without consuming a fiscal number.
The request is discriminated by type. E31 and E32 have sales fields, E41 has supplier and withholding fields, and E45 has buyer and government eligibility fields. They use the same methods and Invoice identity.
Before offering a document type, read whether the issuer can issue it now:
import { createTimbroClient } from "@timbro/invoices/admin";
const timbro = createTimbroClient({ baseUrl, authentication: { mode: "source_connection", credential } });
const readiness = await timbro.issuanceReadiness.retrieve();
for (const documentType of readiness.documentTypes) {
if (documentType.status === "actionRequired") {
console.warn(documentType.type, documentType.reasons);
}
}Each of the ten e-CF types is ready only when the issuer profile is complete, the signing certificate is valid now, and a numbering range is eligible. reasons lists every blocker in issuer, certificate, numbering order, and numbering.remaining and numbering.expiresOn describe the range Timbro assigns from next. Admission still decides each document. Pass hosted: { checkoutTenantId, environment } to read a hosted tenant with a first-party service credential; an ERP connected through the Checkout Gateway reads the same verdicts on GET /v1/business.
An optional preview evaluates the request without creating an Invoice, reserving a number, or producing files:
const preview = await timbro.invoices.preview(request);
if (preview.status === "invalid") {
for (const problem of preview.problems) {
console.error(problem.path, problem.message);
}
}status describes fiscal progress. processing and verifying retain the same identity while work continues. Acceptance, file readiness, and payment success are separate facts. buyerDeliveryReadiness becomes ready only once DGII has accepted the e-CF, with or without observations. latestEvent is null until a real notification has been retained. Do not use file availability or the latest notification timestamp as authority acceptance evidence.
A point-of-sale or ERP receipt prints three facts DGII requires on every printed e-CF: the QR code, the security code below it, and the signing time. Read them from fiscalIdentity.signature instead of deriving them from the XML; Timbro's PDF prints the same values.
const invoice = await timbro.invoices.retrieve(invoiceId);
if (invoice?.status === "accepted" || invoice?.status === "acceptedWithObservations") {
const signature = invoice.fiscalIdentity.signature;
if (signature !== null) {
printReceipt({
qrCode: signature.verificationUrl,
securityCode: signature.securityCode,
signedAt: signature.signedAt, // "2026-10-05T04:44:45-04:00", printed as 05-10-2026 04:44:45
});
}
}signature is null until the complete e-CF is signed. Print the buyer's copy only after DGII accepts the Invoice: DGII sequences the printed e-CF after its answer for every type, including a Type 32 below DOP 250,000.
Replace a retained Invoice after authority rejection by sending a new canonical request. The replacement keeps the predecessor's Source ID, uses a new Source version and invoiceReference, and returns a new Invoice whose replacesInvoiceId identifies the predecessor. A 422 admission refusal has no retained Invoice ID, so correct the request and call create; replace applies only to a retained, replaceable Invoice. Authority-rejected numbering gaps still require the existing administrator authorization.
const replacement = await timbro.invoices.replace(rejectedInvoice.id, {
...request,
invoiceReference: "sale-123-r1",
});downloadXml and downloadPdf return verified bytes, SHA-256, byte length, creation time, and Invoice identity. PDF downloads also verify the signed source, eNCF, type, layout, and page count. Both methods accept an optional signal; create, preview, retrieve, and list do too. A download refuses mismatched bytes or identity.
Recover by ID or retained reference:
const page = await timbro.invoices.list({ invoiceReference: request.invoiceReference });
const retained = page.data[0];
if (retained !== undefined) {
const current = await timbro.invoices.retrieve(retained.id);
}retrieve returns undefined only for invoice_not_found. Lists return one page with data, hasMore, and nextCursor. Pass the returned cursor to the next list call, with the same credential and filters. The default limit is 20, with a maximum of 100.
API refusals use InvoiceError, with code, status, requestId, problems, and isRetryable. Malformed input is validated against the generated contract before submission. Preserve the original request when retrying a transport failure; resolve permanent validation and conflict errors before submitting corrected work.
Verify webhook signatures over the exact request body, then retrieve current state:
import { isRecognizedWebhookEvent } from "@timbro/invoices";
const event = await timbro.webhooks.verify({
body: await httpRequest.text(),
headers: Object.fromEntries(httpRequest.headers),
secrets: [currentWebhookSecret, previousWebhookSecret],
});
if (!isRecognizedWebhookEvent(event)) {
return; // a newer server announced an event type this SDK version does not know
}
if (
event.type === "invoice.accepted" ||
event.type === "invoice.rejected" ||
event.type === "invoice.operatorRequired" ||
event.type === "invoice.ready"
) {
const current = await timbro.invoices.retrieve(event.data.id);
}Register the receiving endpoint through the hosting service's webhook endpoint API. All invoice types share these event types. invoice.ready means a positive authority outcome and retained XML/PDF files; it does not promise payment or email delivery. A notification can arrive before create returns. Match the recovered reference and source coordinates, and apply it through your existing durable processor.
Ask Timbro to email the buyer with invoices.deliver({ invoiceId, recipientEmail, idempotencyKey, attachments: { pdf: true, xml: true } }). The email leaves only after DGII accepts the Invoice, and always links the PDF; attachments adds the PDF, the signed XML, or both as {RNC}{e-NCF}.pdf and .xml. A rejected Invoice fails its pending emails. invoice.deliverySent and invoice.deliveryFailed announce each email once, with data.deliveryId naming it; retrieveDelivery returns its current state and lastResultCode. Call deliver only when Timbro is the company's declared e-CF sender, so the buyer never receives two copies.
Deduplicate notifications by event.id. data.id identifies the Invoice, and sequence stays a decimal string. Verification accepts any configured rotation secret and rejects invalid signatures, stale timestamps, malformed envelopes, and mismatched event IDs. It throws WebhookVerificationError with a specific rejection. An unusable configured secret requires receiver recovery; no rejected delivery should cause an application side effect.
Rust owns the Invoice wire schemas, and the SDK generates its public types and boundary validators from them. The SDK does not persist business work or replace ERP accounting. Source registration, restricted receipt sharing, supplier reception, and operational diagnostics retain their own scoped tasks. Local SDK checks do not establish hosted behavior, DGII qualification, or provider certification.
Administration and installation tooling use @timbro/invoices/admin. Source snapshots, custody transports, workload bindings, and operation timing do not appear in the ordinary package root. The public createTimbroInvoices call accepts the API key and optional origin; Invoice creation does not require assembling those administrative clients.
Compatibility
Packages that share a minor version, including @timbro/invoices, @timbro/protocol, and @timbro/payments, form a compatible set; a patch release of one package does not require the others to move. Install without a version or tag: npm install @timbro/invoices.
The SDK validates requests strictly, so a misspelled or unsupported property is rejected before anything is sent. It accepts unknown properties in responses, at every depth and in every union variant, and drops them from the value it returns. A server that adds a field therefore does not break an installed SDK, and the returned value contains only fields the typings describe. Unknown webhook event types follow the same rule: after the signature verifies, webhooks.verify returns an event whose type is a plain string and whose data is unknown. Narrow it with isRecognizedWebhookEvent before reading data, and acknowledge the delivery. Known event types still require their documented fields.
To decode Timbro responses yourself, for example in a service that reads the Rust-generated resources, import the validators from @timbro/invoices/admin/generated/response. They accept unknown properties and drop them, and have the same names as the strict validators in @timbro/invoices/admin/generated, which remain the right choice for requests.
Removing or renaming a field, and adding a value to an existing enum, are breaking changes and arrive in a new minor version. Read a value you did not expect in an enum as a signal to upgrade.
License
The SDK is Apache-2.0. The bundled fiscal engine is not open source; see LICENSE.
