DGI E-Invoicing Sandbox: Test Your Integration
Because the national DGI platform is not yet open, EFact operates as a full-scale test environment: every invoice passes through the real pipeline (validation, UBL 2.1, XAdES signature) and is cleared by our DGI simulator. You can validate your entire integration today with no tax consequences.
In short
The EFact DGI simulator is the default testing mode: your invoices pass through the real pipeline (validation, UBL 2.1 XML, XAdES signature) without submitting to the official tax platform. In July 2026, every valid invoice moves to ACCEPTED approximately 5 seconds after transmission. You can test HTTP 400, 409, 401, and 429 error handling today; forced DGI rejection is planned.
Sandbox by design: the simulator replicates the official clearance flow end-to-end - submission, signature validation, reference assignment, QR code generation, and callbacks. When the national platform opens, your code does not change: only the transmission target changes on EFact servers. See Architecture - Environments.
The nominal cycle
In the sandbox, every valid invoice is accepted: create the invoice, track its status through to ACCEPTED, and retrieve the clearance reference and QR code.
import { EFact } from '@itzenata/efact-node'
const efact = new EFact(process.env.EFACT_SECRET_KEY!)
// 1. Create: invoice is returned normalized (UBL 2.1) and signed (XAdES)
const invoice = await efact.invoices.create({
invoiceNumber: `TEST-${Date.now()}`, // unique per organization
invoiceDate: '2026-07-14',
currency: 'MAD',
paymentMethod: 'BANK_TRANSFER',
buyerIce: '002345678901234',
buyerName: 'Test Client SARL',
lines: [
{ lineNumber: 1, description: 'Test integration', quantity: 1, unitPrice: 100, vatRate: 20 },
],
})
console.log(invoice.status) // 'SIGNED'
// 2. Track clearance: simulator responds within seconds
let status = await efact.invoices.retrieveStatus(invoice.id)
while (status.status === 'SIGNED' || status.status === 'TRANSMITTED') {
await new Promise((r) => setTimeout(r, 2000))
status = await efact.invoices.retrieveStatus(invoice.id)
}
console.log(status.status) // 'ACCEPTED'
console.log(status.dgiReference) // clearance reference
console.log(status.dgiQrPayload) // QR code payloadExpected timeline
| Status | When |
|---|---|
SIGNED | Immediate: the response to POST is already signed |
TRANSMITTED | Moments after creation (submission to simulator) |
ACCEPTED | Approximately 5 seconds after transmission (simulated callback). If a transient error occurs, recovery schedulers resubmit automatically (2 to 5 minutes) |
A polling interval of 2 to 5 seconds is recommended. Stay under the limit of 300 requests/minute; the SDK automatically handles 429 rate limits.
Error cases to cover
All of these failure modes can be tested on demand in the sandbox:
| Case | How to trigger | Expected result |
|---|---|---|
| Invalid payload | ICE ≠ 15 digits, empty lines, invalid currency... | 400 VALIDATION_ERROR + field details |
| Duplicate number | Replay a create with the same invoiceNumber | 409 INVOICE_NUMBER_EXISTS |
| Immutability | Call update or cancel on a signed invoice | 409 INVOICE_NOT_EDITABLE |
| Authentication | Invalid or missing API key | 401 API_KEY_INVALID / API_KEY_MISSING |
| Tenant isolation | UUID of an invoice belonging to another organization | 404 INVOICE_NOT_FOUND (never 403) |
| Rate limiting | Over 300 requests/minute | 429 + Retry-After |
| Legal details | Create an invoice before completing ICE/IF/RC details in the dashboard | 409 MISSING_LEGAL_MENTIONS |
import { EFactValidationError, EFactConflictError } from '@itzenata/efact-node'
// Invalid ICE (14 digits instead of 15) -> 400 VALIDATION_ERROR
try {
await efact.invoices.create({ ...input, buyerIce: '12345678901234' })
} catch (e) {
if (e instanceof EFactValidationError) {
console.log(e.errors) // [{ field: 'buyerIce', message: '...' }]
}
}
// Invoice number already used -> 409 INVOICE_NUMBER_EXISTS
try {
await efact.invoices.create({ ...input, invoiceNumber: 'TEST-DUPLICATE' })
await efact.invoices.create({ ...input, invoiceNumber: 'TEST-DUPLICATE' })
} catch (e) {
if (e instanceof EFactConflictError) {
console.log(e.code) // 'INVOICE_NUMBER_EXISTS'
}
}
// Modifying an already signed invoice -> 409 INVOICE_NOT_EDITABLE
try {
await efact.invoices.update(invoice.id, { buyerName: 'New name' })
} catch (e) {
if (e instanceof EFactConflictError) {
console.log(e.code) // 'INVOICE_NOT_EDITABLE'
}
}Handling DGI rejectionsPlanned
Today, the sandbox accepts every valid invoice: forcing a REJECTED status on demand is not yet supported (dedicated test values, Stripe-style, are planned). You should still write your rejection handler today - the contract is documented and stable:
// Write this handler today: REJECTED contract is stable,
// even though the sandbox does not produce on-demand rejections yet.
const status = await efact.invoices.retrieveStatus(invoice.id)
switch (status.status) {
case 'ACCEPTED':
// Cleared invoice: archive dgiReference + QR, mark payable
break
case 'REJECTED':
// Clearance rejected: display dgiErrorCode / dgiErrorMessage
// to user, correct invoice data, issue a new invoice
console.error(status.dgiErrorCode, status.dgiErrorMessage)
break
case 'FAILED':
// Unrecoverable technical failure: alert team, contact support
break
}Once rejected, an invoice cannot be modified: update your data and issue a new invoice with a new invoice number.
Verifying artifacts
After ACCEPTED, verify that your integration retrieves and archives all required tax artifacts:
// Once ACCEPTED, artifacts can be downloaded directly:
GET /v1/invoices/{id}/pdf # Invoice PDF
GET /v1/invoices/{id}/xml # Signed UBL 2.1 XML (legal original)
GET /v1/invoices/{id}/qr # Clearance QR code (PNG)Testing best practices
- • Unique invoice numbers: prefix your test numbers (
TEST-...) and include a timestamp - aninvoiceNumberis unique per organization forever. - • Never blindly retry invoice creation: after a network timeout, retry with the exact same number; either it succeeds or a
409confirms the invoice was already created. - • Handle intermediate statuses gracefully: your application should tolerate seeing
SIGNEDorTRANSMITTEDfor several minutes (automatic recovery) without panicking. - • Dedicated test API keys: create a separate key for test environments so it can be rotated or revoked safely.
Next steps
Frequently asked questions
How do I test the EFact API without sending real invoices to the DGI?
EFact runs with a DGI simulator as its default test mode: your invoices pass through the real pipeline (validation, UBL 2.1, XAdES signature) and are cleared by the simulator, without transmission to the official platform. You can test the complete lifecycle safely with no tax consequences.
What result does the EFact DGI simulator return?
In July 2026, every valid invoice is accepted (ACCEPTED) approximately 5 seconds after transmission via a simulated callback. You can test validation errors (400), immutability (409), authentication (401), and rate limits (429) today. Triggering forced DGI rejections is planned.
How frequently should I poll invoice status during testing?
A polling interval of 2 to 5 seconds is sufficient: the simulator responds in a few seconds. Stay under the limit of 300 requests per minute to avoid 429 rate limit responses. In production, webhooks replace polling so you receive final status notifications without repeated queries.