End-to-end flow¶
The API reference lists what each method does. This page shows the order to call them in — the part that is easy to get wrong and expensive to debug.
Every step below matches the official DGII documents; the normative reference
is project-docs/dgii-ground-truth.md.
The main flow¶
flowchart TD
A["Load the .p12<br/>P12Reader"] --> B["authenticate()<br/>GET semilla → sign → POST validarSemilla"]
B --> C{"Pre-flight (optional)<br/>checkServiceAvailability()"}
C -->|"No Disponible"| C1["Contingency: defer the batch"]
C -->|"Disponible"| D{"Document type and amount"}
D -->|"E32 < RD$250,000"| E["convertECF32ToRFCE()<br/>summary of the signed E32"]
D -->|"E31 · E33 · E34 · E41 · E43<br/>E44 · E45 · E46 · E47<br/>E32 ≥ RD$250,000"| F["Build the ECF payload"]
E --> G["Sign: XMLDSig<br/>inclusive C14N · RSA-SHA256"]
F --> G
G --> H{"validateBeforeSend?<br/>optional"}
H -->|"invalid"| H1["VALIDATION — fix before sending"]
H -->|"valid or skipped"| I{"Which endpoint?"}
I -->|"RFCE summary"| J["sendSummary()<br/>fc.dgii.gov.do · synchronous"]
I -->|"general e-CF"| K["sendDocument()<br/>ecf.dgii.gov.do · returns trackId"]
K --> L["statusTrackId(trackId)"]
L -->|"En Proceso"| L
L -->|"Aceptado / Aceptado Condicional"| M["Done"]
L -->|"Rechazado"| N{"secuenciaUtilizada?"}
N -->|"false"| O["Sequence reusable"]
N -->|"true"| P["Sequence consumed — request a new e-NCF"]
K -.->|"IDEMPOTENCY error"| Q["reconcileSubmission()"]
Q -->|"No encontrado"| R["Marker cleared — retry is safe"]
Q -->|"Delivered"| S["Do not retry"]
1. Authenticate¶
DGII issues a seed, you sign it with the certificate, and exchange it for a bearer token.
import { ECF, Environment, P12Reader } from '@deuriib/dgii-ecf';
const credentials = new P12Reader(passphrase).getKeyFromFile('cert.p12');
if (credentials.isErr()) throw credentials.error;
const ecf = new ECF(credentials.value, Environment.Prod);
const auth = await ecf.authenticate();
if (auth.isErr()) throw auth.error;
from py_dgii_ecf import ECF, Environment, P12Reader
credentials = P12Reader(passphrase).get_key_from_file("cert.p12")
if credentials.is_err():
raise RuntimeError(credentials.unwrap_err())
ecf = ECF(credentials.unwrap(), Environment.PROD)
auth = ecf.authenticate()
if auth.is_err():
raise RuntimeError(auth.unwrap_err())
The token is valid for one hour. Both clients refresh it shortly before it
expires and re-authenticate once on a 401, so a long-running worker does not
need to manage it.
2. Pre-flight (optional)¶
Before a batch, ask whether the services are up and whether the environment is inside a maintenance window. This needs the status API key, not the token.
import { StatusEnvironment } from '@deuriib/dgii-ecf';
const status = await ecf.checkServiceAvailability(StatusEnvironment.Production);
if (status.isOk() && status.value.estado === 'No Disponible') {
// Inside a maintenance window: defer the batch.
}
from py_dgii_ecf import StatusEnvironment
status = ecf.check_service_availability(StatusEnvironment.PRODUCTION)
if status.is_ok() and status.unwrap().estado == "No Disponible":
... # Inside a maintenance window: defer the batch.
The client needs statusApiKey / status_api_key for this call.
3. Route by document type and amount¶
This is the decision most integrators get wrong.
| Document | Endpoint | Host | Response |
|---|---|---|---|
| E32 below RD$250,000 | recepcionfc |
fc.dgii.gov.do |
synchronous result |
| E32 at or above RD$250,000 | recepcion |
ecf.dgii.gov.do |
trackId |
| E31, E33, E34, E41, E43, E44, E45, E46, E47 | recepcion |
ecf.dgii.gov.do |
trackId |
A consumption invoice below the threshold is not sent as-is: DGII only accepts the RFCE summary for it, and the summary is built from the already signed E32.
import { convertECF32ToRFCE } from '@deuriib/dgii-ecf';
const converted = convertECF32ToRFCE(signedEcf);
if (converted.isErr()) throw converted.error;
// converted.value.xml -> sign it as RFCE
// converted.value.securityCode -> CodigoSeguridadeCF
from py_dgii_ecf import convert_ecf32_to_rfce
converted = convert_ecf32_to_rfce(signed_ecf)
if converted.is_err():
raise RuntimeError(converted.unwrap_err())
payload = converted.unwrap()
# payload["xml"] -> sign it as RFCE
# payload["securityCode"] -> CodigoSeguridadeCF
sendDocument / send_document applies this routing for you and rejects an
E32 below the threshold instead of misrouting it.
4. Sign¶
Sign last, and never reformat the document afterwards — the signature covers the exact bytes.
import { Signature } from '@deuriib/dgii-ecf';
const signer = new Signature(credentials.value.key!, credentials.value.cert!);
const signed = signer.signXml(xml, 'ECF'); // or 'RFCE', 'ACECF', 'ANECF'
if (signed.isErr()) throw signed.error;
from py_dgii_ecf import Signature
signer = Signature(credentials.unwrap().key, credentials.unwrap().cert)
signed = signer.sign_xml(xml, "ECF") # or "RFCE", "ACECF", "ANECF"
if signed.is_err():
raise RuntimeError(signed.unwrap_err())
5. Submit¶
const response = await ecf.sendDocument(signed.value, `${rnc}${encf}.xml`);
if (response.isErr()) throw response.error;
// general e-CF: { trackId }
// RFCE summary: { codigo, estado, encf, secuenciaUtilizada }
response = ecf.send_document(signed.unwrap(), f"{rnc}{encf}.xml")
if response.is_err():
raise RuntimeError(response.unwrap_err())
Submissions are idempotent, keyed on RNC + e-NCF + SHA-256(signed XML).
A repeated call returns the cached result instead of issuing a second document.
6. Track the result¶
A general e-CF is acknowledged with a trackId; the fiscal verdict arrives
asynchronously.
let status = await ecf.statusTrackId(response.value.trackId!);
while (status.isOk() && status.value.estado === 'En Proceso') {
await new Promise((resolve) => setTimeout(resolve, 2_000));
status = await ecf.statusTrackId(response.value.trackId!);
}
import time
status = ecf.status_track_id(response.unwrap().track_id)
while status.is_ok() and status.unwrap().estado == "En Proceso":
time.sleep(2)
status = ecf.status_track_id(response.unwrap().track_id)
codigo |
estado |
Meaning |
|---|---|---|
| 0 | No encontrado | DGII has no record of the track id |
| 1 | Aceptado | Valid for tax purposes |
| 2 | Rechazado | Not valid — see mensajes |
| 3 | En Proceso | Still validating; keep polling |
| 4 | Aceptado Condicional | Valid with observations |
On a rejection, secuenciaUtilizada decides the next step: false means the
e-NCF may be reused, true means it is consumed and a new one is required.
7. Reconcile a pending submission¶
If the process dies mid-send, the idempotency marker stays pending and the
next attempt returns IDEMPOTENCY rather than re-sending. Resolve it before
retrying:
const outcome = await ecf.reconcileSubmission(signed.value);
if (outcome.isOk() && outcome.value.cleared) {
// DGII has no record: the retry is safe.
}
outcome = ecf.reconcile_submission(signed.unwrap())
if outcome.is_ok() and outcome.unwrap().cleared:
... # DGII has no record: the retry is safe.
The marker is cleared only when DGII reports No encontrado. If DGII has the
document, the marker stays and delivered is true — do not retry.
Auxiliary flows¶
flowchart LR
A["Commercial approval (ACECF)"] --> B{"Destination"}
B -->|"DGII"| C["sendCommercialApproval(xml, file)"]
B -->|"buyer host"| D["sendCommercialApproval(xml, file, host)<br/>HTTP 200 / 400"]
E["Range voiding (ANECF)"] --> F["voidENCF(xml, file)"]
G["Directory"] --> H["getCustomerDirectory(rnc)<br/>getDirectoryListing()"]
- ACECF to DGII returns
{ codigo, estado }:codigois"1"or"2". - ACECF to a buyer returns
{ status, accepted }. A400means the buyer received and rejected it — the issuer must then answer with an E34. - ANECF only voids sequences that were never sent to DGII nor delivered to the buyer. Anything already issued needs an E34.
- Directory gives you
urlRecepcion,urlAceptacionandurlOpcionalfor a given RNC, which is what the B2B flow uses.
Common mistakes¶
| Mistake | Consequence |
|---|---|
| Sending an E32 below RD$250,000 to the general endpoint | Rejected — it must go as an RFCE summary |
| Signing, then reformatting or re-indenting the XML | Invalid signature |
| Reusing a consumed e-NCF | Rejected — check secuenciaUtilizada |
| Retrying a pending submission without reconciling | IDEMPOTENCY, or a duplicate if the store is bypassed |
Treating Ok(...) as "accepted" |
A 200 can carry error populated; the verdict is in estado |
| Delivering an E41/E43/E45/E46/E47 straight to a buyer | DGII excludes those types from the B2B flow |