PHPackages                             g-efac/sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. g-efac/sdk

ActiveLibrary

g-efac/sdk
==========

SDK de PHP para la API /v1 de G-eFac (facturacion electronica e-CF, DGII Republica Dominicana)

v0.2.0(yesterday)02↑2900%1proprietaryPHPPHP &gt;=8.2

Since Aug 8Pushed yesterdayCompare

[ Source](https://github.com/Walewsky/efac-php-sdk)[ Packagist](https://packagist.org/packages/g-efac/sdk)[ RSS](/packages/g-efac-sdk/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (10)Versions (2)Used By (1)

G-eFac SDK para PHP
===================

[](#g-efac-sdk-para-php)

SDK oficial para la API `/v1` de G-eFac: emisión de e-CF, consulta de estado, historial y anulaciones, el archivo fiscal, y re-obtención de los ingredientes del QR / Representación Impresa para reimpresiones.

Estabilidad: versión `0.x`
--------------------------

[](#estabilidad-versión-0x)

`/v1` es **provisional** — todavía no está congelada. Mientras siga así, este paquete se publica en `0.x` y las versiones **menores pueden romper compatibilidad**: no hay promesa de semver hasta que la API se congele. Cuando se active el disparador de congelación, el SDK pasa a `1.0.0` y adopta semver en serio — a partir de ahí, ninguna versión menor rompe compatibilidad. El detalle completo del disparador y del compromiso de compatibilidad vive en `docs/API-VERSIONING-POLICY.md`, dentro del repositorio de la plataforma (ese archivo no se incluye en este paquete).

Instalación
-----------

[](#instalación)

```
composer require g-efac/sdk
```

El paquete no impone ningún cliente HTTP: solo depende de las interfaces PSR-18 (cliente) y PSR-17 (fábricas de request/stream). `build()` necesita las tres piezas — cliente, fábrica de request y fábrica de stream — para funcionar.

Si tu proyecto ya tiene implementaciones de esas interfaces instaladas (Guzzle + su dependencia `guzzlehttp/psr7`, Symfony HttpClient, Nyholm) puedes sumar `php-http/discovery` y `build()` las detecta solas, sin pasar nada. Si no instalas `php-http/discovery`, tienes que inyectarlas explícitamente con `httpClient(...)`, `requestFactory(...)` y `streamFactory(...)` — las tres, no solo el cliente: si falta cualquiera de ellas, `build()` lanza `InvalidArgumentException`. Los ejemplos de este README usan inyección explícita con Guzzle para que funcionen tal cual, sin depender de qué tengas instalado.

Para Laravel usa el paquete `g-efac/laravel` (`composer require g-efac/laravel`), que hace todo el cableado por ti. Requiere Laravel 12 — es la única versión con soporte de seguridad activo, y `composer` bloquea las anteriores por defecto.

Primer comprobante
------------------

[](#primer-comprobante)

```
use Efac\Sdk\EfacClient;
use Efac\Sdk\Enum\EcfTipo;
use Efac\Sdk\Enum\TaxTreatment;
use Efac\Sdk\Invoice\Invoice;
use Efac\Sdk\Invoice\Item;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;

// HttpFactory implementa tanto RequestFactoryInterface como StreamFactoryInterface,
// y viene incluida al instalar guzzlehttp/guzzle (composer require guzzlehttp/guzzle).
$factory = new HttpFactory();

$efac = EfacClient::builder()
    ->baseUrl('https://api.g-efac.com')
    ->credentials($clientId, $clientSecret)
    ->httpClient(new Client())
    ->requestFactory($factory)
    ->streamFactory($factory)
    ->build();

$factura = Invoice::of(EcfTipo::CreditoFiscal31)
    ->seller('101000001', 'ACME SRL', 'Av. Principal 1')
    ->buyer('130000002', 'Cliente SA', email: 'cxp@cliente.do')
    ->addItem(Item::goods('Widget', qty: 2, unitPrice: 500.00, tax: TaxTreatment::Itbis18))
    ->build();

$resultado = $efac->invoices()->emit($factura, 'orden-1042');

echo $resultado->encf;      // E310000000001
echo $resultado->qrUrl;     // URL del timbre para la Representación Impresa
```

No hay que escribir nada de OAuth: el cliente pide el token a `/connect/token`, lo cachea y lo renueva antes de que expire.

La llave de idempotencia es obligatoria
---------------------------------------

[](#la-llave-de-idempotencia-es-obligatoria)

`emit()` exige una llave, y no la genera por ti. La razón es fiscal, no estética:

> Sin `Idempotency-Key`, reintentar una petición que expiró por timeout puede emitir un e-CF duplicado y consumir un segundo e-NCF, que es un recurso fiscal con seguimiento legal.

Usa un identificador **estable y propio de tu sistema** — el número de orden, el id de la factura interna. Un UUID nuevo en cada reintento cambiaría en cada intento y anularía el mecanismo justo cuando hace falta.

Reenviar la misma llave con el mismo cuerpo repite la respuesta original en lugar de volver a emitir. Reutilizarla con un cuerpo distinto, o mientras una emisión bajo esa llave sigue en curso, devuelve `409`.

```
if ($resultado->wasReplayed()) {
    // El servidor repitió una respuesta anterior; no se emitió nada nuevo.
}
```

Si de verdad necesitas emitir sin llave, existe `emitWithoutIdempotency()`. El nombre es incómodo a propósito.

Cuando la emisión queda en cola (`202`)
---------------------------------------

[](#cuando-la-emisión-queda-en-cola-202)

DGII no siempre responde de inmediato. En ese caso el servidor devuelve `202` y el e-CF queda en cola:

```
if ($resultado->isQueued()) {
    $estado = $efac->invoices()->status($resultado->encf);
}
```

Para esperar el veredicto hay un ayudante con backoff exponencial. **Es opt-in**: nunca lo llames dentro de una petición web — úsalo en un worker de cola o en un job de consola.

```
use Efac\Sdk\Exception\TimeoutException;

try {
    $estado = $efac->invoices()->awaitTerminal($resultado->encf, timeoutSeconds: 60);
} catch (TimeoutException $e) {
    // OJO: esto NO es un fallo de emisión. El e-CF se emitió; DGII simplemente no
    // había dado veredicto dentro de la ventana. $e->lastStatus trae lo último visto.
}
```

Reimpresiones
-------------

[](#reimpresiones)

```
$qr = $efac->invoices()->qr('E310000000001');

$qr->qrUrl;            // idéntico byte a byte al de la emisión original
$qr->qrImage;          // PNG como data URI
$qr->codigoSeguridad;
```

Facturas en cola e historial
----------------------------

[](#facturas-en-cola-e-historial)

`queued()` lista los e-CF que todavía no tienen veredicto de DGII, paginado por cursor:

```
$pagina = $efac->invoices()->queued(limit: 100);

foreach ($pagina->items as $q) {
    echo $q->encf, ' — ', $q->horasRestantes, "h restantes\n";
}

if ($pagina->hasMore) {
    $siguiente = $efac->invoices()->queued(cursor: $pagina->nextCursor, limit: 100);
}
```

`history()` busca en el historial de e-CF emitidos, con filtros opcionales y paginación por número de página:

```
use Efac\Sdk\Enum\EstadoGrupo;
use Efac\Sdk\Resource\HistoryFilter;

$filtro = (new HistoryFilter())
    ->query('Cliente SA')
    ->estado(EstadoGrupo::Aceptado);

$pagina = $efac->invoices()->history($filtro, page: 1, pageSize: 50);

echo $pagina->total, ' de ', $pagina->pageCount, " páginas\n";
foreach ($pagina->rows as $fila) {
    echo $fila->encf, ' ', $fila->estado?->value, "\n";
}
```

Omitir un filtro omite ese parámetro por completo; `history()` sin argumentos trae la primera página sin filtrar.

Anulaciones
-----------

[](#anulaciones)

`void()` anula (cancela) un rango de e-NCF. **Es irreversible, y a diferencia de `emit()` no tiene `Idempotency-Key`**: si la petición expira por timeout, la respuesta por sí sola no dice si la anulación ocurrió o no. Antes de reintentar una llamada que expiró, revisa `history()` para ver el estado del rango — no reenvíes a ciegas.

```
use Efac\Sdk\Enum\EcfTipo;

$resultado = $efac->invoices()->void(EcfTipo::CreditoFiscal31, from: 1042, to: 1050);

echo $resultado->voided; // 9
```

Archivo fiscal
--------------

[](#archivo-fiscal)

`$efac->archive()` da acceso de solo lectura a la ventana de 10 años del archivo fiscal:

```
foreach ($efac->archive()->years() as $anio) {
    echo $anio->year, ': ', $anio->total, " documentos\n";
}

foreach ($efac->archive()->months(2026) as $mes) {
    echo $mes->month, ': ', $mes->count ?? 'futuro', "\n";
}

foreach ($efac->archive()->documents(2026, 7) as $doc) {
    echo $doc->encf, ' ', $doc->estado?->value, "\n";
}
```

`documents()` está limitado a 500 filas por mes en el servidor; hoy no existe paginación para ese límite.

Errores
-------

[](#errores)

Todas las respuestas de error son RFC 7807 (`application/problem+json`) y llegan como excepciones tipadas. No hace falta mirar códigos de estado.

ExcepciónHTTPSignificado`ValidationException`400, 422El servidor rechazó el contenido. Revisa `$e->problem['errors']``AuthException`401, 403Credenciales inválidas, sin permiso sobre el recurso, cuenta suspendida, o ambiente no habilitado`ConflictException`409Llave de idempotencia reusada, secuencia e-NCF agotada, falta el archivo del e-CF, o el rango de `void()` ya fue anulado o se solapa con una anulación previa. Lee `$e->detail``NotFoundException`404Este emisor no tiene ningún e-CF con ese e-NCF`DgiiUnavailableException`502DGII no responde. Reintentable`ServerException`500Fallo inesperado del servidor`TransportException`—Falló el cliente HTTP: conexión, DNS, TLS, timeout`InvalidInvoiceException`—Validación local, antes de salir a la redTodas heredan de `EfacException` y traen `status`, `title`, `detail`, `instance` y el cuerpo completo en `problem`.

Validación: solo estructura
---------------------------

[](#validación-solo-estructura)

`build()` valida lo que se puede saber sin conocimiento fiscal: campos obligatorios presentes, RNC de 9 u 11 dígitos, cantidades y precios no negativos.

**No** aplica reglas de impuestos, requisitos por tipo ni umbrales. Esa autoridad es del servidor, y una copia aquí terminaría rechazando comprobantes que el servidor sí acepta. Si el contenido es incorrecto, la respuesta es un `422` con el mensaje del servidor.

Timeouts
--------

[](#timeouts)

PSR-18 no define una API de timeouts, así que se configuran en el cliente que inyectes:

```
// Guzzle
use GuzzleHttp\Psr7\HttpFactory;

$factory = new HttpFactory();

$efac = EfacClient::builder()
    ->baseUrl($baseUrl)
    ->credentials($clientId, $clientSecret)
    ->httpClient(new \GuzzleHttp\Client(['connect_timeout' => 5, 'timeout' => 30]))
    ->requestFactory($factory)
    ->streamFactory($factory)
    ->build();

// Symfony HttpClient — Psr18Client implementa a la vez ClientInterface,
// RequestFactoryInterface y StreamFactoryInterface, así que se inyecta tres veces
$symfony = new \Symfony\Component\HttpClient\Psr18Client(
    \Symfony\Component\HttpClient\HttpClient::create(['timeout' => 30]),
);

$efac = EfacClient::builder()
    ->baseUrl($baseUrl)
    ->credentials($clientId, $clientSecret)
    ->httpClient($symfony)
    ->requestFactory($symfony)
    ->streamFactory($symfony)
    ->build();
```

Por la misma razón, este SDK **no** reintenta con backoff ante errores 5xx: esa política pertenece a tu cliente HTTP. La única excepción es un `401`, que renueva el token y reintenta una sola vez — seguro incluso para `emit()`, porque un `401` es rechazo en la puerta de autenticación y ahí no se emitió ningún e-CF.

Caché del token
---------------

[](#caché-del-token)

Por defecto el token se guarda en memoria del proceso. En una aplicación web conviene inyectar un almacén PSR-16 compartido para no re-autenticar en cada petición:

```
$factory = new \GuzzleHttp\Psr7\HttpFactory();

$efac = EfacClient::builder()
    ->baseUrl($baseUrl)
    ->credentials($clientId, $clientSecret)
    ->httpClient(new \GuzzleHttp\Client())
    ->requestFactory($factory)
    ->streamFactory($factory)
    ->tokenCache($miCachePsr16)
    ->build();
```

La clave se calcula por host, `client_id` y scope, de modo que varios emisores dentro de la misma aplicación nunca comparten token. El secreto nunca forma parte de la clave.

Alcance de esta versión
-----------------------

[](#alcance-de-esta-versión)

Cubierto: `POST /v1/invoices`, `GET /v1/invoices/{encf}`, `GET /v1/invoices/{encf}/qr`, `GET /v1/invoices` (en cola), `GET /v1/issued-invoices` (historial), `POST /v1/void-requests`(anulaciones), `GET /v1/archive/years`, `GET /v1/archive/{year}`, `GET /v1/archive/{year}/{month}`y la obtención del token.

Fuera de alcance por ahora (siguen disponibles vía la colección de Postman): secuencias, certificado, directorio, estado DGII, documentos recibidos, configuración y aprobaciones comerciales.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance100

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community5

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4038498?v=4)[Walewsky](/maintainers/Walewsky)[@Walewsky](https://github.com/Walewsky)

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/g-efac-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/g-efac-sdk/health.svg)](https://phpackages.com/packages/g-efac-sdk)
```

###  Alternatives

[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k19](/packages/tempest-framework)[cakephp/cakephp

The CakePHP framework

8.9k20.0M1.8k](/packages/cakephp-cakephp)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M656](/packages/shopware-core)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
