Skip to content

TypeScript

Package: @deuriib/dgii-ecf

Bun-first, native fetch, strict TypeScript, and result-pattern (neverthrow). Resilience via cockatiel.

Authenticate

import { ECF, Environment, P12Reader } from '@deuriib/dgii-ecf';

const credentials = new P12Reader(
  process.env.CERT_PASSWORD ?? '',
).getKeyFromFile('/secure/identity.p12');
if (credentials.isErr()) throw credentials.error;

const ecf = new ECF(credentials.value, Environment.Cert);
const auth = await ecf.authenticate();
if (auth.isErr()) throw auth.error;

console.log(auth.value.token);

Sign a document

import { Signature } from '@deuriib/dgii-ecf';

const signed = new Signature(
  credentials.value.key!,
  credentials.value.cert!,
).signXml(xml);
if (signed.isErr()) throw signed.error;

Submit

const response = await ecf.sendElectronicDocument(
  signed.value,
  '130862346E310000000001.xml',
);
response.match(
  (invoice) => console.log(invoice.trackId),
  (error) => console.error(error.code, error.message),
);

Result pattern

Every fallible operation returns a Result:

const result = await ecf.statusTrackId('track-id');
if (result.isOk()) {
  console.log(result.value.estado);
} else {
  console.error(result.error.code, result.error.message);
}

API

Every method returns a Result; buyerHost / ambiente are optional unless noted.

Method Returns
authenticate(buyerHost?) Result<AuthToken>
sendElectronicDocument(xml, fileName, buyerHost?) Result<InvoiceResponse>
sendSummary(xml, fileName) Result<InvoiceSummaryResponse>
sendDocument(xml, fileName, buyerHost?) Result<InvoiceResponse \| InvoiceSummaryResponse>
statusTrackId(trackId) Result<TrackingStatusResponse>
inquiryStatus(rnc, ncf, comprador?, codigo?) Result<InquiryStatusResponse>
trackStatuses(rnc, encf) Result<SummaryTrackingStatusResponse[]>
getCustomerDirectory(rnc) Result<ServiceDirectoryResponse[] \| undefined>
getDirectoryListing() Result<ServiceDirectoryResponse[]>
getSummaryInvoiceInquiry(rnc, encf, codigo) Result<InquiryInvoiceSummary>
sendCommercialApproval(xml, fileName, buyerHost?) Result<CommercialApprovalResponse \| B2BCommercialApprovalResponse>
voidENCF(xml, fileName) Result<VoidNCFResponse>
reconcileSubmission(xml) Result<ReconciliationResult>
getServiceStatus() Result<ServiceStatusEntry[]>
getMaintenanceWindows() Result<MaintenanceWindow[]>
checkServiceAvailability(ambiente) Result<ServiceAvailabilityResponse>

Routing with sendDocument

sendDocument reads the root element, TipoeCF and MontoTotal from the signed XML and picks the endpoint for you: an RFCE goes to the summary reception, an ECF to the general reception. An E32 below RD$250,000 is rejected with VALIDATION — build the RFCE first — and an RFCE cannot be delivered directly to a buyerHost.

Use resolveReceptionRoute(signedXml) when you need the decision without sending it.

Service status

getServiceStatus, getMaintenanceWindows and checkServiceAvailability talk to the statusecf.dgii.gov.do host. It authenticates with an API key, not the bearer token, so pass statusApiKey to the constructor and skip authenticate():

const ecf = new ECF(credentials.value, Environment.Prod, {
  statusApiKey: process.env.DGII_STATUS_API_KEY,
});
const availability = await ecf.checkServiceAvailability(
  StatusEnvironment.Production,
);
if (availability.isOk() && availability.value.estado === 'No Disponible') {
  // Inside a maintenance window — defer the batch.
}

B2B applicability

A direct Emisor↔Receptor delivery (buyerHost / buyer_host) is not allowed for every document type. DGII excludes E32 below RD$250,000 (only the RFCE summary applies) plus E41, E43, E45, E46 and E47. E31, E33, E34, E44 and an E32 at or above the threshold can be delivered peer-to-peer.

The client checks this before issuing the request and fails with VALIDATION without calling the buyer host. Disable the check with enforceB2BRules: false (TypeScript) or enforce_b2b_rules=False (Python) when you need to reproduce a certification scenario:

const result = await ecf.sendElectronicDocument(xml, 'f.xml', buyerHost);
if (result.isErr() && result.error.code === 'VALIDATION') {
  // The type or amount is not eligible for direct delivery.
}

Production-only endpoints

getSummaryInvoiceInquiry (RFCE inquiry, fc.dgii.gov.do/consultarfce) is only exposed by DGII in production (Environment.Prod / eCF). In Dev or Cert it returns a VALIDATION error without issuing a request:

const inquiry = await ecf.getSummaryInvoiceInquiry(rnc, encf, codigoSeguridad);
if (inquiry.isErr()) {
  console.error(inquiry.error.code); // 'VALIDATION' outside production
}

A direct ACECF delivery goes to the buyer's fe/aprobacioncomercial/api/ecf endpoint and returns B2BCommercialApprovalResponse instead of DGII's {codigo, estado} payload. DGII documents HTTP 200 as satisfactory and HTTP 400 as unsatisfactory, so a 400 is reported as accepted: false rather than as a transport failure:

const ack = await ecf.sendCommercialApproval(acecf, 'acecf.xml', buyerHost);
if (ack.isOk() && !ack.value.accepted) {
  // The receiver rejected the approval; the issuer must answer with an E34.
}

Schema validation

validateDocument checks a document against the official DGII XML Schema and reports each violation with its element path. It shells out to xmllint, so there is no bundled engine and no native dependency:

import { validateDocument } from '@deuriib/dgii-ecf';

const result = validateDocument(signedXml);
if (result.isOk()) {
  for (const violation of result.value) {
    console.log(violation.path, violation.message);
  }
}

validateBeforeSend: true runs the same check before a general e-CF submission and fails with VALIDATION without calling DGII:

const ecf = new ECF(credentials.value, Environment.Prod, {
  validateBeforeSend: true,
});

The binary is looked up on PATH; set DGII_XMLLINT_BIN (or the xmllintBin client option) to point at it. A missing binary is an error, never a silent pass.

Twelve schemas are bundled: e-CF 32, 33, 34, 41, 43, 44, 45, 46, 47, ANECF, ARECF and Semilla. Three published schemas do not compile — e-CF 31, RFCE 32 and ACECF — so those types are reported as schema unavailable. See examples/fixtures/schemas/README.md for the reasons.

Monetary rounding

Transformer.json2xml(data, true) rounds decimal values to two places with ROUND_HALF_UP (2.665 → 2.67, -2.675 → -2.68), computed on the decimal string rather than via toFixed (which loses precision: (2.675).toFixed(2) === '2.67'). Version and non-monetary text are never rounded.

See Signing, Resilience, and the package README.