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.