Skip to content

Integration guide

fiscaliacore integration guide

Audience. Developers integrating a product (POS / ERP / accounting SaaS / payment processor) with fiscaliacore. First reader: fiscalia-bk. This guide is the source of truth for how to talk to the API. The OpenAPI spec is the machine-readable companion.

1. Mental model in 30 seconds

You (the integrator) hold the fiscal data — your customers' invoices, their emisor profiles, their .p12 certificates. fiscaliacore is the pipe that signs things with the .p12 and forwards them to DGII.

Every call follows the same shape:

  1. Open a session with the .p12 + password → receive a 15-min bearer token.
  2. Call business endpoints with Authorization: Bearer <token> and the full payload in the body.
  3. Close the session (or let the TTL do it).

fiscaliacore persists nothing about the transaction. You get back the signed XML + DGII's response — you archive both on your side.

2. Auth model: session tokens

Why not just upload the .p12 once?

Because we don't want to hold your customers' signing keys at rest. If a DB of ours leaks tomorrow, nothing fiscally sensitive comes with it. You hold the .p12; we unlock it in memory for 15 minutes at a time.

Session lifecycle

                        <.p12> + password
   your product ──POST /api/v1/sessions──►  fiscaliacore

                                    [unlock .p12 in JVM heap;
                                     mint 32-byte opaque token;
                                     put in ConcurrentHashMap
                                     keyed by token, TTL 15m]

   your product ◄──201 { token, expiresAt }──────┘

   your product ──POST /api/v1/acecf/issue──►  fiscaliacore
                    Authorization: Bearer <token>             [look up session;
                                                               sign payload with
                                                               in-memory key;
                                                               submit to DGII]

   your product ◄──200 { trackId, signedXml, ... }────────────┘

   your product ──DELETE /api/v1/sessions/current──►  fiscaliacore
                    Authorization: Bearer <token>             [evict session +
                                                               DGII bearers]

   your product ◄──204────────────────────────────────────────┘

Token handling on your side

  • Lifetime. 15 min TTL. Refresh around the 14-minute mark or on demand.
  • Binding. One bearer = one end-customer's .p12. If you serve multiple end-customers, you'll have multiple concurrent sessions open. Key your cache by your own end-customer id, not by the bearer.
  • Sensitivity. The bearer token + the .p12 password are equivalent credentials during the 15-min window. Store in the same secrets tier as an OAuth access token. Never log.
  • Restart behavior. fiscaliacore restarts invalidate every active session. Clients handle this transparently — see the retry pattern in §4.

3. First working call (curl)

3.1 Open a session

bash
curl -X POST https://api.fiscaliacore.com/api/v1/sessions \
  -H 'X-Api-Key: $FISCALIACORE_API_KEY' \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "certificate": "$(base64 -w0 < /path/to/customer.p12)",
  "password": "$CUSTOMER_P12_PASSWORD"
}
EOF

Response (201):

json
{
  "token": "dXNXRUxTMkZINDBiUXR1WUhpelprenVRZlVtZzkzNndSdzJrNnY",
  "signerCedula": "40209547971",
  "subjectDn": "CN=ALEX RODRIGUEZ,SERIALNUMBER=40209547971,O=VIAFIRMA DOMINICANA,C=DO",
  "certExpiresAt": "2027-04-20T00:00:00Z",
  "expiresAt": "2026-04-23T17:15:00Z"
}

3.2 Issue a commercial approval (ACECF)

bash
curl -X POST https://api.fiscaliacore.com/api/v1/acecf/issue \
  -H "X-Api-Key: $FISCALIACORE_API_KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "rncEmisor": "131793916",
    "rncComprador": "40209547971",
    "eNCF": "E310000000005",
    "fechaEmisionOriginal": "2026-04-23",
    "montoTotalOriginal": 1180.00,
    "estado": 1,
    "environment": "CERTIFICATION"
  }'

Response (200):

json
{
  "trackId": "abc-123-def",
  "dgiiEstado": "Aceptada",
  "signedXml": "<ACECF>...</ACECF>",
  "submittedAt": "2026-04-23T17:02:14Z",
  "success": true,
  "statusMessage": "Aceptada"
}

Archive the signedXml on your side — fiscaliacore doesn't. You'll need it for your own audit + to prove to DGII that you submitted something specific if they ever ask.

3.3 Close the session

bash
curl -X DELETE https://api.fiscaliacore.com/api/v1/sessions/current \
  -H "X-Api-Key: $FISCALIACORE_API_KEY" \
  -H "Authorization: Bearer $TOKEN"

204 No Content. Idempotent — safe to call even if the TTL already expired.

4. The mandatory retry pattern

Because session tokens can be evicted (process restart or TTL), every business call needs a fallback. The pattern:

call ──► 401 SESSION_EVICTED ──► re-open session ──► retry once


                success or propagate

TypeScript reference implementation

typescript
class FiscaliacoreClient {
  private tokenPromise: Promise<string> | null = null;

  constructor(
    private readonly baseUrl: string,
    private readonly apiKey: string,
    private readonly certificateBase64: () => Promise<string>,
    private readonly password: () => Promise<string>,
  ) {}

  private async openSession(): Promise<string> {
    const res = await fetch(`${this.baseUrl}/api/v1/sessions`, {
      method: 'POST',
      headers: {
        'X-Api-Key': this.apiKey,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        certificate: await this.certificateBase64(),
        password: await this.password(),
      }),
    });
    if (!res.ok) throw new Error(`session open failed: ${res.status}`);
    const body = (await res.json()) as { token: string; expiresAt: string };
    // Refresh ~60s before the server's TTL.
    setTimeout(() => (this.tokenPromise = null),
      Math.max(0, new Date(body.expiresAt).getTime() - Date.now() - 60_000));
    return body.token;
  }

  private async token(): Promise<string> {
    if (!this.tokenPromise) this.tokenPromise = this.openSession();
    return this.tokenPromise;
  }

  /** Run `call` with a fresh bearer; on 401 evict + retry once. */
  async withSession<T>(call: (bearer: string) => Promise<Response>): Promise<T> {
    let bearer = await this.token();
    let res = await call(bearer);
    if (res.status === 401) {
      this.tokenPromise = null;
      bearer = await this.token();
      res = await call(bearer);
    }
    if (!res.ok) {
      const body = await res.text();
      throw new Error(`${res.status}: ${body}`);
    }
    return res.json() as Promise<T>;
  }

  async issueAcecf(payload: AcecfIssueRequest): Promise<AcecfIssueResponse> {
    return this.withSession((bearer) =>
      fetch(`${this.baseUrl}/api/v1/acecf/issue`, {
        method: 'POST',
        headers: {
          'X-Api-Key': this.apiKey,
          'Authorization': `Bearer ${bearer}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      })
    );
  }
}

Key design decisions to copy:

  • One session per end-customer. If your product serves many emisores, maintain one FiscaliacoreClient per emisor RNC. Don't share bearers.
  • Evict on 401 once, not in a loop. If the second attempt also 401s, propagate — something's wrong beyond a stale session.
  • Proactive refresh. The setTimeout to null out the token-promise 60s before TTL avoids the 401-retry round-trip on the first call after the TTL.

5. Error envelope

Every fiscaliacore error has this shape:

json
{
  "timestamp": "2026-04-23T20:00:00Z",
  "status": 401,
  "code": "SESSION_EVICTED",
  "message": "no active signing session — open one via POST /api/v1/sessions",
  "details": [],
  "path": "/api/v1/acecf/issue"
}

code is the stable identifier — branch on that, not on message. Live codes today:

CodeHTTPMeaning
SESSION_EVICTED401Bearer expired or server restarted. Re-open session + retry.
INVALID_PASSWORD401.p12 password didn't unlock the keystore. Double-check and re-try.
MALFORMED_P12401The base64 didn't decode into a PKCS#12 keystore. Ensure you're sending the raw .p12 bytes, base64-encoded.
VALIDATION_FAILED400Your request body failed bean validation (e.g. RNC wrong length). details[] has per-field messages.
BAD_REQUEST400Generic malformed payload.
XML_BUILD_FAILED422Your payload deserialized fine but the DGII-shape XML can't be built. Usually a business-rule violation (required field missing for a specific e-CF type).
SIGNING_FAILED500The in-memory signer raised. Rare; usually indicates a cert that we can unlock but is structurally weird (e.g. non-RSA key). Contact support.
INTERNAL_ERROR500Our side. We log the stack trace with the request path; paste the timestamp + path into a support ticket.

DGII-rejection bodies are slightly different: the outer HTTP is 400 but success is false on the response envelope, with DGII's reason verbatim in statusMessage. See §6 for an example.

6. Two-phase DGII semantics

DGII rejects = business rejections

If DGII receives a syntactically valid submission but rejects it on business grounds (wrong amount, duplicate eNCF, invalid reference), you'll see:

json
{
  "trackId": "",
  "dgiiEstado": "Rechazada",
  "signedXml": "<ACECF>...</ACECF>",
  "submittedAt": "2026-04-23T17:02:14Z",
  "success": false,
  "statusMessage": "La propiedad TotalITBIS no coincide con el valor del conjunto de datos..."
}

HTTP is 400 (we bubble up DGII's rejection as a client error). statusMessage is DGII's verbatim text — surface it to your users, they'll often want to correct the submission and retry.

DGII 5xx = infrastructure failure

If DGII itself is down or times out, we return 502 (Bad Gateway) with:

json
{ "code": "INTERNAL_ERROR", "message": "DGII submission failed: ..." }

Your responsibility: retry with exponential backoff. fiscaliacore doesn't persist failed submissions for later (by design — that state belongs in your system). If DGII is down for hours, your system is what decides when to stop retrying and surface the outage to the end user.

7. Environments

environment valueWhat hitsWhen to use
TESThttps://ecf.dgii.gov.do/testecf/...Private pre-cert development. Not commonly useful.
CERTIFICATIONhttps://ecf.dgii.gov.do/certecf/...DGII's 15-step certification portal. Use while your end-customer walks through Paso 1–15. Default when the field is omitted on /api/v1/acecf/issue.
PRODUCTIONhttps://ecf.dgii.gov.do/ecf/...Real money. Use only after the end-customer's Paso 15 is finalizado.

Your integrator-side account should track, per end-customer, which environment to pass on each request. Don't hard-code one in your product.

8. Rate limits + best practices

RuleWhy
At most 1 active session per end-customer RNC.Multiple concurrent sessions for the same RNC is pointless and makes your integrator-side concurrency harder to reason about.
Reuse bearers within the 15-min window.Each session open = one DGII semilla round-trip. Reusing the bearer saves ~500ms per call.
Target < 10 /issue calls per second per session.Kong + DGII themselves impose quotas. If you're over, batch on your side.
Do NOT log the bearer or the .p12 base64.Both are credentials. Redact in every log sink — we redact on our side already.
Use idempotent externalDocumentIds on the (future) /ecf/issue endpoint.Phase 3 will ship it; the DTO carries an externalDocumentId UUID. Same UUID twice → idempotent return, safe to retry on transport failure.

9. Going to production — checklist

Before you flip environment to PRODUCTION for a customer:

  • [ ] The end-customer has completed DGII Paso 15 (Finalizado) and has production e-NCF ranges authorized via OFV.
  • [ ] Your product stores every signedXml returned by fiscaliacore. DGII will audit you, not us.
  • [ ] Your product handles the three DGII return shapes (accept / business-reject / infra-5xx) with correct user messaging.
  • [ ] You've rotated off the development API key and are using a production key from the fiscaliacore platform.
  • [ ] Your session-token cache has a redis or equivalent backing (not just in-memory) if your integrator runs multiple replicas, or you accept the small 401-retry cost per replica.
  • [ ] Your .p12 storage is audited — HSM, KMS-encrypted-at-rest, or equivalent. Never plain files on disk.

10. Current API surface (Phase 3)

MethodPathPurposeStatus
POST/api/v1/sessionsOpen a signing session✅ Live
DELETE/api/v1/sessions/currentClose the signing session✅ Live
POST/api/v1/acecf/issueIssue a commercial approval (Paso 3 scope)✅ Live
POST/api/v1/ecf/issue-statelessIssue an e-CF, all 10 types — bearer-session stateless path✅ Live (feature-flagged dgii.ecf.stateless-issue.enabled=true)
POST/api/v1/ecf/issueLegacy tenantId-keyed e-CF issuance🔶 Stays alive through Phase 6 for transition; removed when stateless takes over this path
GET/api/v1/status/{trackId}Proxy DGII's consulta-resultado🔶 Phase 4
POST/api/v1/ri/generateGenerate Representación Impresa PDF from signed XML🔶 Phase 4
Host-routed inbound ({rnc}.ecf.fiscaliacore.com/fe/*)Receive e-CFs / ACECFs from other integrators, forwarded to your callback URL✅ Phase 5

See docs/stateless-bridge-refactor.md for the phase timeline.

Stateless e-CF issuance — request shape

The stateless e-CF endpoint authenticates with the same Authorization: Bearer <session-token> as ACECF; every business fact travels on the request body. No tenantId, no externalDocumentId idempotency (the integrator owns eNCF issuance and retry).

bash
curl -X POST https://api.fiscaliacore.com/api/v1/ecf/issue-stateless \
  -H "X-Api-Key: $FISCALIACORE_API_KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "rncEmisor": "40209547971",
    "environment": "CERTIFICATION",
    "tipoeCF": "31",
    "overrideEncf": "E310000000005",
    "issueDate": "2026-04-24",
    "razonSocialEmisor": "MI EMPRESA SRL",
    "direccionEmisor": "Av. 27 de Febrero 100",
    "municipio": "010100",
    "provincia": "010000",
    "receiverRnc": "131793916",
    "razonSocialComprador": "CLIENTE SRL",
    "subtotal": 1000.00,
    "taxAmount": 180.00,
    "totalAmount": 1180.00,
    "lines": [
      {"description": "Servicio de consultoría", "quantity": 1, "unitPrice": 1000.00, "lineTotal": 1000.00}
    ]
  }'
json
{
  "ecfNumber": "E310000000005",
  "trackId": "abc-123-def",
  "dgiiEstado": "Aceptado",
  "signedXml": "<ECF>...</ECF>",
  "submittedAt": "2026-04-24T17:02:14Z",
  "success": true,
  "statusMessage": "Accepted for processing (TrackID abc-123-def)"
}

Dry run. Set dryRun: true to get back signedXml without hitting DGII — used for Paso 2 fourth-batch manual-upload scenarios and for regenerating signed XML after a builder change.

E32 < RD$250k auto-routes. The orchestrator detects tipoeCF="32" + totalAmount < 250000 and submits an RFCE summary to fc.dgii.gov.do instead of the primary Recepción endpoint. The response's signedXml remains the FULL e-CF (suitable for the portal's "Facturas de consumo < 250Mil" manual upload).

401 retry. If DGII returns 401 on submission, the orchestrator invalidates the cached DGII bearer and retries once. No caller intervention required.

See docs/api/paso-2-client-reference.md for the complete field catalog (all 100+ fields across Emisor / Comprador / Totales / Lines / DescuentosORecargos / ImpuestosAdicionales / references / foreign currency / transport).

11. Support

  • Interactive API playground: https://api.fiscaliacore.com/swagger-ui.html — paste your API key + bearer, exercise every endpoint.
  • Docs site (in progress): platform.fiscaliacore.com.
  • Email: inceptumrex+fiscaliacore@gmail.com.
  • Github: alexrf00/fiscaliacore — file issues, read the source.