eCF Connect Integration Docs
Qué es eCF Connect
eCF Connect es la plataforma de facturación electrónica de CardNET para República Dominicana: recibe órdenes de emisión de comprobantes fiscales electrónicos (e-CF) desde los sistemas del integrador, asigna la secuencia fiscal (e-NCF), construye y firma el XML según el estándar DGII, lo persiste de forma inmutable y gestiona el envío y el ciclo de vida del documento frente a la DGII.
Alcance de esta integración: el contrato interno v1 del IssuerGateway — emisión de Factura de Crédito Fiscal (e-CF 31), emisión de Nota de Crédito (e-CF 34) y consulta del estado local del documento. La comunicación con la DGII la hace eCF Connect; el integrador nunca habla con la DGII directamente.
AcceptedForProcessing = aceptado para procesamiento). Nunca
significa «aceptado por la DGII»: el veredicto fiscal es asíncrono y se consulta por el
endpoint de estado.Ambientes y Base URL
| Ambiente | Base URL | Notas |
|---|---|---|
| Sandbox (este PoC) | http://51.8.80.173 |
Datos 100 % sintéticos; sin tráfico DGII; endpoint provisional por IP mientras no
exista DNS (api-ecf-poc.cardnet.com.do queda preparado en el proxy). |
| Producción | pendiente de publicación | — |
Todo el tráfico del contrato viaja bajo /api/v1/issuance/. En sandbox el
transporte es HTTP provisional; producción será HTTPS obligatorio.
Autenticación y autorización
Toda petición lleva Authorization: Bearer <access token>. El token es un
JWT que la frontera de eCF Connect valida de forma cerrada e idéntica en todos los
ambientes: emisor (issuer), audiencia, firma RS256 (JWKS), identidad del
cliente y scopes por operación. Lo que cambia entre Sandbox y Producción es
únicamente de dónde obtiene usted el token, no cómo se valida ni el
contrato del API.
| Operación | Scope requerido |
|---|---|
| POST /api/v1/issuance/invoices | ecf:invoice:create |
| POST /api/v1/issuance/credit-notes | ecf:credit-note:create |
| POST /api/v1/issuance/document-status | ecf:document-status:read |
Sandbox — Sandbox Access Token
En este PoC usted usa un Sandbox Access Token que le provisiona el operador del PoC por canal privado (vida limitada; incluye los scopes de las tres operaciones y el cliente autorizado). Ese JWT lo valida la misma frontera real de eCF Connect (issuer / audience / RS256 / identidad de cliente / scopes).
En el sandbox usted NO llama a ningún endpoint /token de Keycloak:
recibe el token ya emitido y solo lo coloca en la cabecera Authorization. Este
mecanismo de provisión es exclusivo del PoC y no representa la arquitectura
productiva.
Producción — OAuth 2.0 Client Credentials
En Producción usted obtendrá el access token usted mismo mediante el flujo
OAuth 2.0 Client Credentials contra el Keycloak de CardNET (con su
client_id y su secreto de cliente), y reutilizará / cacheará
ese token en su aplicación hasta poco antes de su expiración (exp),
renovándolo entonces. Los detalles del realm, el client_id y el
endpoint de token se entregan en el onboarding productivo. El contrato de emisión
(/api/v1/issuance/*), los scopes y los headers son idénticos a los del
sandbox.
Headers obligatorios
| Header | Obligatorio | Regla |
|---|---|---|
Authorization | Sí | Bearer JWT válido con el scope de la operación |
Idempotency-Key | Sí en emisiones (no aplica a consulta de estado) | 8–64 caracteres [A-Za-z0-9._:-]; identifica la ORDEN de negocio |
Content-Type | Sí | application/json |
X-Correlation-Id | No (recomendado) | 2–64 caracteres [A-Za-z0-9._:-]; si no se envía, el servidor genera uno |
Quick Start
- Obtenga un token de sandbox (sección Autenticación).
- Guarde el documento de ejemplo como
factura.json:
{
"merchantId": "comercio-demo-01",
"document": {
"issueDate": "2026-08-31",
"issuer": {
"rnc": "111111111",
"legalName": "Emisor Ficticio SRL",
"address": "Calle Ficticia 1, Santo Domingo"
},
"buyer": {
"rnc": "22222222222",
"legalName": "Comprador Ficticio SA"
},
"payment": {
"type": "Cash"
},
"incomeType": "OperatingIncome",
"taxIncludedPricing": false,
"lines": [
{
"lineNumber": 1,
"billingIndicator": "TaxedItbisRate1",
"description": "Servicio de integración (demo)",
"kind": "Service",
"quantity": 1,
"unitPrice": 100.0,
"declaredAmount": 100.0
}
]
}
}
- Emita:
TOKEN="<TOKEN_DE_SANDBOX>" # se entrega por canal privado — ver «Credenciales»
curl -s http://51.8.80.173/api/v1/issuance/invoices \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: demo-$(date +%s)" \
-H "X-Correlation-Id: mi-sistema-0001" \
-H "Content-Type: application/json" \
-d @factura.json
- Respuesta
202 Accepted:
{
"outcome": "AcceptedForProcessing",
"businessRequestId": "demo-1756640000",
"merchantId": "comercio-demo-01",
"documentType": "CreditFiscalInvoice",
"electronicNcf": "E310000000001",
"lifecycleId": "0f37e9b2-0000-0000-0000-000000000000",
"fiscalState": "SubmissionRequested",
"correlationId": "mi-sistema-0001"
}
- Consulte el estado (el despacho es asíncrono; en segundos pasa a
Submitteden sandbox):
curl -s http://51.8.80.173/api/v1/issuance/document-status \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "merchantId": "comercio-demo-01", "electronicNcf": "E310000000001" }'
{
"merchantId": "comercio-demo-01",
"electronicNcf": "E310000000001",
"lifecycleId": "0f37e9b2-0000-0000-0000-000000000000",
"fiscalState": "Submitted",
"technicalState": "None",
"version": 6,
"signedArtifactId": "7c1f0d40-0000-0000-0000-000000000000",
"attemptCount": 0,
"lastFailureReason": null,
"createdAtUtc": "2026-08-31T15:00:00+00:00",
"updatedAtUtc": "2026-08-31T15:00:04+00:00",
"correlationId": "mi-sistema-0001"
}
Endpoints
Los tres endpoints del contrato v1 (detalle completo en la referencia OpenAPI; openapi.json). Todos viajan por POST — también la consulta de estado, porque los identificadores fiscales jamás van en URL/query (higiene de logs).
POST/api/v1/issuance/invoices
Emite un e-CF 31 (Factura de Crédito Fiscal). Cuerpo: InvoiceIssuanceRequestDto
(merchantId + document). Respuesta 202: IssuanceResponseDto.
POST/api/v1/issuance/credit-notes
Emite un e-CF 34 (Nota de Crédito) con referencia obligatoria al comprobante afectado
(reference.modifiedNcf, modificationCode). El monto acreditado se
valida atómicamente contra el saldo disponible del comprobante afectado.
POST/api/v1/issuance/document-status
Lectura del estado LOCAL del ciclo de vida (sin efectos; sin Idempotency-Key).
Cuerpo: merchantId + electronicNcf. Respuesta 200:
DocumentStatusResponseDto.
Catálogos (valores cerrados)
| Campo | Valores |
|---|---|
payment.type | Cash · Credit · Free (a crédito, deadline es obligatoria) |
incomeType | OperatingIncome · FinancialIncome · ExtraordinaryIncome · LeaseIncome · DepreciableAssetSaleIncome · OtherIncome |
lines[].kind | Good · Service |
lines[].billingIndicator | NotBillable · TaxedItbisRate1 (18 %) · TaxedItbisRate2 (16 %) · TaxedItbisRate3 (0 %) · Exempt |
reference.modificationCode (NC) | FullCancellation · TextCorrection · AmountCorrection · ContingencyReplacement · ConsumerInvoiceReference |
outcome (respuesta) | AcceptedForProcessing · AlreadyRequested · RejectedByFiscalRules · RejectedByPreSignValidation |
declaredAmount que no corresponde a
quantity × unitPrice (± redondeo fiscal); línea gravada sin
billingIndicator coherente; payment.deadline ausente a crédito;
RNC con formato inválido (9 u 11 dígitos). Los totales del documento los calcula SIEMPRE
el motor fiscal del servidor — no se envían totales.Ejemplos de cliente
C#:
using System.Net.Http.Headers;
using System.Net.Http.Json;
var http = new HttpClient { BaseAddress = new Uri("http://51.8.80.173") };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token); // token de sandbox
var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/issuance/invoices")
{
Content = JsonContent.Create(factura) // el objeto del ejemplo JSON
};
request.Headers.Add("Idempotency-Key", $"pedido-{pedidoId}");
request.Headers.Add("X-Correlation-Id", correlationId);
var response = await http.SendAsync(request);
// 202 => IssuanceResponseDto; 4xx/5xx => application/problem+json (RFC 9457)
var payload = await response.Content.ReadAsStringAsync();
TypeScript:
const BASE = "http://51.8.80.173";
async function emitirFactura(token: string, factura: unknown, pedidoId: string) {
const res = await fetch(`${BASE}/api/v1/issuance/invoices`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": `pedido-${pedidoId}`, // 8-64 [A-Za-z0-9._:-]
"X-Correlation-Id": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify(factura),
});
const body = await res.json(); // 202 => resultado; si no => problem+json
if (!res.ok) throw new Error(`${body.code}: ${body.title} (${body.correlationId})`);
return body;
}
Idempotencia
La Idempotency-Key identifica la ORDEN de negocio (p. ej. su número interno
de pedido). Reglas exactas del contrato:
- Replay (misma clave, mismo contenido): devuelve el MISMO resultado —
mismo e-NCF, sin consumir otra secuencia. La respuesta llega con
outcome = AlreadyRequestedsi la orden ya estaba aceptada. - Conflicto (misma clave, contenido funcional distinto):
409 Conflictconcode = idempotency-conflict. La clave queda ligada a la primera orden para siempre. - Clave inválida (fuera de 8–64 o caracteres no admitidos):
400concode = idempotency-key-invalid. - Un rechazo determinista (
RejectedByFiscalRules/RejectedByPreSignValidation) CONSUME la secuencia por política fiscal (fail-safe): una orden nueva exige clave nueva.
Ciclo de vida del e-CF
El estado local (fiscalState) avanza de forma monótona:
| Estado | Significado |
|---|---|
ReadyForSignature | Orden aceptada y secuencia asignada; firma pendiente |
Signed | Firma registrada (transitorio) |
Stored | XML firmado persistido de forma inmutable (hash verificado) |
SubmissionRequested | Intención durable de envío a DGII registrada (encolado) |
Submitted | Entregado al transporte; existe TrackId DGII |
InProcessAtDgii | DGII lo está procesando |
Accepted / AcceptedConditionally / Rejected |
Veredicto FISCAL de la DGII (asíncrono) |
Cancelled | Anulado por el flujo correspondiente |
Distinciones importantes: la aceptación local (202) ≠ persistencia
(Stored) ≠ encolado (SubmissionRequested) ≠ entrega
(Submitted, con TrackId) ≠ veredicto DGII (Accepted…/Rejected).
technicalState informa fallos técnicos del despacho
(RetryableFailure/PermanentFailure) sin alterar la verdad fiscal.
Submitted con un TrackId sintético y ahí se quedan — los estados
InProcessAtDgii/Accepted/Rejected solo ocurren en ambientes con DGII real.Errores, retries y timeouts
Todos los errores son application/problem+json (RFC 9457) con
type = urn:ecf-connect:problem:<code>, title estable,
code tipado, correlationId, traceId y, en
validaciones, errors[] por campo:
{
"type": "urn:ecf-connect:problem:request-invalid",
"title": "La orden de emisi\u00f3n no supera la validaci\u00f3n del contrato.",
"status": 400,
"code": "request-invalid",
"correlationId": "mi-sistema-0001",
"traceId": "00-\u2026",
"errors": [
{
"field": "document.lines[0].unitPrice",
"message": "\u2026"
}
]
}
| HTTP | code | Reintentar |
|---|---|---|
| 400 | request-invalid · idempotency-key-invalid · correlation-invalid · merchant-id-invalid | No (corrija la petición) |
| 401 | authentication-required | Renueve el token y reintente |
| 403 | operation-forbidden · merchant-inactive | No (alta/permisos) |
| 404 | resource-not-found | No (verifique merchantId/eNCF) |
| 409 | idempotency-conflict · operation-conflict | No con la misma clave y otro contenido |
| 413 | request-too-large | No (reduzca el cuerpo) |
| 422 | fiscal-rules-rejected · presign-validation-rejected · credit-note-rejected | No: rechazo DETERMINISTA — la secuencia se consumió; corrija y emita con CLAVE NUEVA |
| 429 | (rate limit del sandbox) | Sí, con backoff |
| 503 | dependency-unavailable | Sí: MISMA clave, backoff exponencial |
| 500 | internal-error | Sí: MISMA clave, backoff prudente |
Política de retries recomendada
- Timeout del cliente ≥ 30 s por petición.
- Outcome indeterminado (timeout, conexión cortada, 5xx sin cuerpo):
NO asuma fallo — la orden pudo quedar aceptada. Reintente con la MISMA
Idempotency-Key; el replay converge al resultado real. Alternativa: consultedocument-status… si aún no conoce el e-NCF, el replay es la única vía segura. - Jamás reintentar ciegamente: un 422 (rechazo determinista) con clave nueva «hasta que pase» quema secuencias fiscales; un 409 con contenido cambiado es un error de su sistema, no transitorio.
Try the Sandbox
| Base URL | http://51.8.80.173 (HTTP staged — ver aviso arriba) |
|---|---|
| Merchant de pruebas | comercio-demo-01 |
| RNC emisor sintético | 111111111 (obligatorio en document.issuer.rnc) |
| Credenciales | Sandbox Access Token provisionado por el operador del PoC por canal privado (ver Autenticación) |
| Rate limit | 5 req/s por IP (ráfaga 10) — el sandbox es funcional, no de carga |
Los ejemplos de Quick Start y Endpoints (curl, C#, TypeScript) apuntan a este endpoint y funcionan tal cual tras insertar el token. Colección Postman: descargar.
Soporte
Para reportar un problema envíe SIEMPRE el correlationId de la respuesta
(o el X-Correlation-Id que usted envió) y, si existe, el
lifecycleId. Con eso CardNET localiza la operación sin datos sensibles.
Buenas prácticas y qué NO hacer
- Use una
Idempotency-Keyderivada de SU identificador de negocio y persístala junto al pedido antes de llamar. - Envíe
X-Correlation-Idpropio y guárdelo en sus logs. - Trate
202como «aceptado para procesamiento»; consulte el estado para el avance real. No bloquee la venta esperando el veredicto DGII. - Valide catálogos y formatos ANTES de llamar (evita 400/422 y secuencias quemadas).
No haga esto:
- No genere claves de idempotencia aleatorias por reintento.
- No parsee
title/detailpara decidir lógica: el contrato escode(catálogo cerrado). - No reenvíe un 422 sin corregir el documento.
- No envíe identificadores fiscales en URLs/query strings, tampoco en sus propios logs.
- No use el sandbox para pruebas de carga.
Versionado, compatibilidad y changelog
El contrato vive bajo /api/v1/. Cambios compatibles (campos opcionales
nuevos, valores nuevos en catálogos de RESPUESTA) pueden llegar dentro de v1; cualquier
cambio incompatible será /api/v2/. El JSON de entrada rechaza campos
desconocidos (fail-closed): consulte el changelog antes de actualizar su cliente.
| Fecha | Cambio |
|---|---|
| 2026-08-31 | Primera publicación del sandbox y de esta documentación
(contrato v1: invoices, credit-notes, document-status). Fuente: main
c36a83abdac6. |
Troubleshooting
| Síntoma | Causa probable | Acción |
|---|---|---|
| 401 con token recién emitido | Token de otro ambiente, audiencia o reloj desviado | Verifique que usa el token de SANDBOX y NTP de su servidor |
| 403 operation-forbidden | El token no trae el scope de la operación | Solicite scopes completos |
| 403 merchant-inactive / 400 merchant-id-invalid | merchantId distinto de
comercio-demo-01 | Use el merchant de pruebas |
| 409 idempotency-conflict | Reutilizó una clave con contenido distinto | Clave nueva para órdenes nuevas |
| 422 fiscal-rules-rejected | Documento normativamente inválido | Revise errors[]; corrija; CLAVE NUEVA |
El estado se queda en SubmissionRequested | Worker del sandbox procesando (2 s de poll) | Reconsulte en unos segundos |
| 429 | Rate limit | Backoff; el sandbox no es para carga |