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.

La respuesta de emisión es un resultado LOCAL (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

AmbienteBase URLNotas
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ónpendiente 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ónScope requerido
POST /api/v1/issuance/invoicesecf:invoice:create
POST /api/v1/issuance/credit-notesecf:credit-note:create
POST /api/v1/issuance/document-statusecf: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

HeaderObligatorioRegla
AuthorizationBearer JWT válido con el scope de la operación
Idempotency-KeySí en emisiones (no aplica a consulta de estado) 8–64 caracteres [A-Za-z0-9._:-]; identifica la ORDEN de negocio
Content-Typeapplication/json
X-Correlation-IdNo (recomendado) 2–64 caracteres [A-Za-z0-9._:-]; si no se envía, el servidor genera uno

Quick Start

  1. Obtenga un token de sandbox (sección Autenticación).
  2. 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
      }
    ]
  }
}
  1. 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
  1. 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"
}
  1. Consulte el estado (el despacho es asíncrono; en segundos pasa a Submitted en 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)

CampoValores
payment.typeCash · Credit · Free (a crédito, deadline es obligatoria)
incomeTypeOperatingIncome · FinancialIncome · ExtraordinaryIncome · LeaseIncome · DepreciableAssetSaleIncome · OtherIncome
lines[].kindGood · Service
lines[].billingIndicatorNotBillable · TaxedItbisRate1 (18 %) · TaxedItbisRate2 (16 %) · TaxedItbisRate3 (0 %) · Exempt
reference.modificationCode (NC)FullCancellation · TextCorrection · AmountCorrection · ContingencyReplacement · ConsumerInvoiceReference
outcome (respuesta)AcceptedForProcessing · AlreadyRequested · RejectedByFiscalRules · RejectedByPreSignValidation
Validaciones que más rechazan: montos con más decimales de los admitidos; 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:

Regla de oro: ante timeout o error de red, REINTENTE con LA MISMA clave. Jamás genere una clave nueva para «reintentar»: eso emite un segundo comprobante.

Ciclo de vida del e-CF

El estado local (fiscalState) avanza de forma monótona:

EstadoSignificado
ReadyForSignatureOrden aceptada y secuencia asignada; firma pendiente
SignedFirma registrada (transitorio)
StoredXML firmado persistido de forma inmutable (hash verificado)
SubmissionRequestedIntención durable de envío a DGII registrada (encolado)
SubmittedEntregado al transporte; existe TrackId DGII
InProcessAtDgiiDGII lo está procesando
Accepted / AcceptedConditionally / Rejected Veredicto FISCAL de la DGII (asíncrono)
CancelledAnulado 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.

En el sandbox no hay tráfico DGII: los documentos avanzan hasta 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"
    }
  ]
}
HTTPcodeReintentar
400request-invalid · idempotency-key-invalid · correlation-invalid · merchant-id-invalidNo (corrija la petición)
401authentication-requiredRenueve el token y reintente
403operation-forbidden · merchant-inactiveNo (alta/permisos)
404resource-not-foundNo (verifique merchantId/eNCF)
409idempotency-conflict · operation-conflictNo con la misma clave y otro contenido
413request-too-largeNo (reduzca el cuerpo)
422fiscal-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
503dependency-unavailableSí: MISMA clave, backoff exponencial
500internal-errorSí: MISMA clave, backoff prudente

Política de retries recomendada

Try the Sandbox

Base URLhttp://51.8.80.173 (HTTP staged — ver aviso arriba)
Merchant de pruebascomercio-demo-01
RNC emisor sintético111111111 (obligatorio en document.issuer.rnc)
CredencialesSandbox Access Token provisionado por el operador del PoC por canal privado (ver Autenticación)
Rate limit5 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

No haga esto:

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.

FechaCambio
2026-08-31Primera publicación del sandbox y de esta documentación (contrato v1: invoices, credit-notes, document-status). Fuente: main c36a83abdac6.

Troubleshooting

SíntomaCausa probableAcción
401 con token recién emitidoToken de otro ambiente, audiencia o reloj desviado Verifique que usa el token de SANDBOX y NTP de su servidor
403 operation-forbiddenEl token no trae el scope de la operación Solicite scopes completos
403 merchant-inactive / 400 merchant-id-invalidmerchantId distinto de comercio-demo-01Use el merchant de pruebas
409 idempotency-conflictReutilizó una clave con contenido distinto Clave nueva para órdenes nuevas
422 fiscal-rules-rejectedDocumento normativamente inválido Revise errors[]; corrija; CLAVE NUEVA
El estado se queda en SubmissionRequestedWorker del sandbox procesando (2 s de poll)Reconsulte en unos segundos
429Rate limitBackoff; el sandbox no es para carga