Search by

invoka / sdk

azurfacturacionelectronica

SDK oficial de PHP para la API de Invoka: facturación electrónica del SRI de Ecuador para programadores (emisión de comprobantes, consulta y descarga, consultas al SRI, créditos, empresas y webhooks).

v1.0.0 2026-10-03 01:48 UTC

This package is auto-updated.

Last update: 2026-10-03 01:49:29 UTC


README

Facturación electrónica del SRI de Ecuador desde PHP, con la API de Invoka: emita facturas, notas de crédito y débito, guías de remisión, retenciones y liquidaciones de compra; consulte su estado y descargue el XML y el PDF; consulte RUC y cédulas en el SRI; y reciba avisos en vivo por webhook.

$invoka = new Invoka\Sdk\Invoka('API_…');           // ambiente 1 (pruebas) por defecto

$r = $invoka->facturas->emitir([...]);
$final = $invoka->comprobantes->esperar($r->claveAcceso());
if ($final->autorizado()) {
    $invoka->comprobantes->pdf($r->claveAcceso())->guardar('/var/facturas');
}
  • PHP 8.1 o superior y Guzzle 7.
  • Reintentos automáticos con espera exponencial (429, 502/503/504 y cortes de red), sin duplicar comprobantes.
  • Excepciones tipadas: Validacion, SaldoInsuficiente, ComprobanteDuplicado, CredencialInvalida…
  • Verificación de webhooks (firma V2 con sello de tiempo, HMAC-SHA256 en tiempo constante).
  • Cálculo de la clave de acceso antes de emitir.

Índice

Instalación

composer require invoka/sdk

La API key (API_…) está en el panel de Invoka → Mi Perfil. Guárdela en una variable de entorno, nunca en el código.

Ambientes: pruebas y producción

Cada comprobante lleva un ambiente:

ambiente Qué es Créditos
1 · pruebas El SRI lo procesa pero no tiene validez tributaria. Gratis e ilimitado
2 · producción Comprobante REAL con validez tributaria. 1 crédito por comprobante autorizado

El SDK usa 1 por defecto. Para producción, póngalo en el cliente ('ambiente' => 2) o en el comprobante ('ambiente' => 2; el del comprobante manda). Antes de pasar a 2:

  • Pruebe toda su integración en 1.
  • Tenga créditos ($invoka->creditos->saldo()).
  • Tenga aprobados sus datos de Proveedor SRI (Res. NAC-DGERCGC26-00000027) en el panel → Proveedor SRI. Sin ellos, la emisión en producción se rechaza con ProveedorSriNoAprobado. Mientras no estén aprobados, cada respuesta trae aviso_proveedor_sri ($r->avisoProveedorSri()).

Primeros pasos

use Invoka\Sdk\Invoka;

$invoka = new Invoka(getenv('INVOKA_API_KEY'), [
    // Datos fijos de su empresa: se completan en cada comprobante (lo que mande en el comprobante manda).
    'emisor' => [
        'ruc' => '0991234567001',
        'razon_social' => 'MI EMPRESA S.A.',     // obligatoria en la factura
        'nombre_comercial' => 'MI EMPRESA',
        'direccion_matriz' => 'Av. Principal 123, Guayaquil',
        'direccion_establecimiento' => 'Av. Principal 123, Guayaquil',
        'obligado_contabilidad' => 'NO',
        'codigo_establecimiento' => '001',
        'codigo_puntoemision' => '001',
    ],
]);

$saldo = $invoka->creditos->saldo();   // también comprueba que la API key vale
echo $saldo['creditos_disponibles'];

La respuesta (Invoka\Sdk\Respuesta) es el JSON de Invoka, de solo lectura: $r['campo'], $r->get('a.b.c') con puntos, $r->toArray(), y atajos como claveAcceso(), autorizado(), requiereCorreccion(), avisoProveedorSri() y limiteRestante().

Emitir comprobantes

Recurso Comprobante Endpoint
$invoka->facturas Factura (01) POST /api/factura/emision
$invoka->notasCredito Nota de crédito (04) POST /api/credito/emision
$invoka->notasDebito Nota de débito (05) POST /api/debito/emision
$invoka->guias Guía de remisión (06) POST /api/guia/emision
$invoka->retenciones Retención 2.0 / ATS (07) — recomendada POST /api/retencionats/emision
$invoka->retenciones->emitirV1() Retención 1.0 (07) — obsoleta POST /api/retencion/emision
$invoka->liquidaciones Liquidación de compra (03) POST /api/liquidacion/emision

En la factura, emisor.razon_social es obligatoria (venga en el comprobante o en la opción emisor del cliente): sin ella el SDK lanza Validacion (datos_invalidos) sin llamar a la API.

Todos tienen emitir(array $datos). El cuerpo es el JSON de la referencia de la API. Antes de enviarlo el SDK completa, sin pisar lo que usted mande:

  • ambiente: el del cliente (1).
  • emisor: los datos de la opción emisor del cliente.
  • emisor.fecha_emision: hoy en Ecuador (aaaa/mm/dd). El SRI solo acepta la fecha del día. Si manda aaaa-mm-dd o un DateTime, se convierte.
  • codigo_establecimiento y codigo_puntoemision a 3 dígitos, y secuencial a 9.
$factura = [
    'emisor' => ['secuencial' => '125'],
    'comprador' => [
        'tipo_identificacion' => '05',          // 04 RUC · 05 cédula · 06 pasaporte · 07 consumidor final
        'identificacion' => '0912345678',
        'razon_social' => 'JUAN PÉREZ',
        'direccion' => 'Guayaquil',
        'correo' => 'juan@ejemplo.com',
    ],
    'items' => [[
        'codigo_principal' => 'P001',
        'descripcion' => 'Servicio de desarrollo',
        'cantidad' => 1,
        'precio_unitario' => 100,
        'descuento' => 0,
        'tipoproducto' => 2,                    // 1 bien · 2 servicio
        'tipo_iva' => 4,                        // tabla 17 del SRI: 0 → 0 %, 4 → 15 %, 5 → 5 %, 6 no objeto, 7 exento
    ]],
    'pagos' => [['tipo' => 20, 'total' => 115]],
    'informacion_adicional' => [['nombre' => 'Pedido', 'detalle' => 'WEB-1001']], // máx. 14 campos
];

$clave = $invoka->facturas->claveAcceso($factura);   // la clave que tendrá, ANTES de emitir
$r = $invoka->facturas->emitir($factura);
// $r['claveacceso'], $r['id_comprobante'], $r['estado'] === 'En procesamiento'

La emisión es asíncrona: Invoka responde enseguida y después genera el XML, lo firma, lo manda al SRI, arma el PDF y envía el correo al comprador (en segundos). Para saber cómo terminó: esperar(), consultar() o, mejor en producción, el webhook.

No ponga RUC Proveedor en informacion_adicional: lo añade Invoka.

Retenciones

$invoka->retenciones->emitir([             // 2.0 (ATS): lleva `sustento`
    'emisor' => ['secuencial' => '1'],
    'proveedor' => ['tipo_identificacion' => '04', 'identificacion' => '0992345678001',
                    'razon_social' => 'PROVEEDOR S.A.', 'direccion' => 'Guayaquil'],
    'sustento' => [
        'codigo_sustento_tributario' => '01',
        'codigo_documento_sustento' => '01',
        'numero_documento_sustento' => '001001000000123',
        'numero_autorizacion' => '…',
        'fecha_documento_sustento' => '2026/10/01',
        'gastos' => [['descripcion' => 'Servicios', 'subtotal_iva' => 100, 'subtotal_siniva' => 0,
                      'subtotal_ivacero' => 0, 'tipo_iva' => 4]],
    ],
    'retenciones' => [
        ['codigoimpuesto' => 1, 'codigo_retencion' => '303', 'base_imponible' => 100, 'porcentaje_retencion' => 10],
        ['codigoimpuesto' => 2, 'codigo_retencion' => '10', 'base_imponible' => 15, 'porcentaje_retencion' => 30],
    ],
]);

emitirV1() (retención 1.0) sigue funcionando pero está marcada @deprecated: use la 2.0 en integraciones nuevas.

Si se corta la red

Invoka identifica cada comprobante por su clave de acceso (RUC + tipo + serie + secuencial + fecha + ambiente) y no registra dos veces la misma. Por eso el SDK sí reintenta las emisiones ante un corte, y si el primer intento llegó (el reintento recibe «clave ya existe»), devuelve la consulta del comprobante con recuperado: true en vez de un error. Si el error de red llega hasta usted (ErrorRed), consulte la clave que calculó con claveAcceso() antes de reintentar.

Clave de acceso

use Invoka\Sdk\ClaveAcceso;

ClaveAcceso::calcular('01', '0991234567001', 1, '001', '001', 125);   // hoy
ClaveAcceso::esValida($clave);                                        // módulo 11
ClaveAcceso::descomponer($clave);   // fecha, tipo, ruc, ambiente, establecimiento, numero…

Consultar, descargar y reprocesar

$c = $invoka->comprobantes->consultar($clave);
$c->autorizado();          // true si el SRI lo autorizó
$c->requiereCorreccion();  // true si terminó con error: corrija y emita de nuevo
$c['estado_texto'];        // "Autorizado", "Devuelto por el SRI…", "Firma electronica invalida o caducada"…
$c['errores'];             // detalle del SRI cuando no se autorizó
$c['xml'], $c['pdf'];      // URLs temporales (24 h) cuando está autorizado

$final = $invoka->comprobantes->esperar($clave, tiempoMaximo: 120, intervalo: 5);

$pdf = $invoka->comprobantes->pdf($clave);    // Invoka\Sdk\Archivo
$pdf->guardar('/var/facturas');               // usa $pdf->nombre si es un directorio
return response($pdf->contenido, 200, ['Content-Type' => $pdf->tipo]);

$xml = $invoka->comprobantes->xml($clave);    // XML autorizado

$invoka->comprobantes->reprocesar($clave);    // tras corregir algo en el SRI (establecimiento, firma…)

xml() y pdf() lanzan Conflicto (codigo = no_autorizado) si el comprobante aún no está autorizado.

Consultas al SRI

$invoka->sri->ruc('0991234567001');               // data: razón social, estado, actividad…
$invoka->sri->cedula('0912345678');               // Registro Civil
$invoka->sri->consultar('0912345678');            // detecta cédula (10) o RUC (13)
$invoka->sri->establecimientos('0991234567001');  // establecimientos, total_establecimientos
$invoka->sri->razonSocial('FARMACIAS', limite: 10);

20 consultas gratuitas al mes; después, 1 de la billetera de consultas por cada consulta que devuelve datos (meta dice cuántas quedan). Sin cupo: SaldoInsuficiente con codigo = sin_consultas. Como pueden cobrarse, no se reintentan solas ante un corte de red.

Créditos

$s = $invoka->creditos->saldo();
$s['creditos_disponibles'];   // de la CUENTA, compartidos por todas sus empresas
$s['estado_saldo'];           // suficiente · bajo (≤ 10) · agotado
$s->get('cartera_consultas.total_consultas_disponibles');

$invoka->creditos->historial(['limit' => 50, 'page' => 1, 'tipo' => 'consumo', 'ambiente' => 2, 'ruc' => '…']);
foreach ($invoka->creditos->todoElHistorial(['tipo' => 'recarga']) as $movimiento) { … }

Empresas, firma y logo

$invoka->empresa->crear(['ruc' => '0991234567001', 'razon_social' => 'MI EMPRESA S.A.', 'regimen_rimpe' => true]);
$invoka->empresa->editar(['ruc' => '0991234567001', 'contribuyente_especial' => '5368']);
$invoka->empresa->subirFirma('0991234567001', '/ruta/firma.p12', 'clave-del-p12');  // ruta o contenido
$invoka->empresa->subirLogo('0991234567001', '/ruta/logo.png');                     // PNG/JPG, máx. 2 MB

Ojo: subirFirma() responde bien aunque la contraseña no abra el .p12 (Invoka le avisa por correo y los comprobantes fallarán al firmar). Emita una factura en ambiente 1 para comprobarlo.

Webhooks

Invoka avisa a su URL cuando un comprobante queda autorizado (comprobante.autorizado) o con error (comprobante.error). Reintenta 5 veces (1, 5, 15, 60 y 180 minutos) si su servidor no responde 2xx.

$w = $invoka->webhook->guardar('https://su-dominio.com/invoka/webhook');
$secret = $w['secret'];                 // ÚNICA vez que llega completo: guárdelo
$invoka->webhook->ver();                // configuración + últimas 10 entregas (secret enmascarado ••••••••XXXX)
$invoka->webhook->probar();             // manda un aviso «webhook.prueba»
$invoka->webhook->eliminar();
  • El secret completo solo viene en la respuesta de guardar() (POST /api/webhook); ver() lo devuelve enmascarado (••••••••XXXX) y no sirve para verificar.
  • La URL tiene que ser pública: localhost y las IP privadas se rechazan con 422 (Validacion). Para probar en local use un túnel (ngrok, cloudflared…).

Cabeceras de cada aviso:

Cabecera Contenido
X-Invoka-Event comprobante.autorizado, comprobante.error o webhook.prueba
X-Invoka-Delivery UUID estable entre reintentos del mismo aviso: deduplique por él
X-Invoka-Timestamp Segundos Unix del envío
X-Invoka-Signature-V2 t=<unix>,v1=<hex HMAC-SHA256(t . "." . cuerpo, secret)> — la que se verifica
X-Invoka-Signature sha256=<hex HMAC-SHA256(cuerpo, secret)> — V1, por compatibilidad

Laravel

use Invoka\Sdk\Webhook;
use Invoka\Sdk\Excepciones\FirmaWebhookInvalida;

Route::post('/invoka/webhook', function (Request $request) {
    try {
        $aviso = Webhook::verificar($request->getContent(), $request->headers->all(), config('services.invoka.webhook_secret'));
    } catch (FirmaWebhookInvalida) {
        abort(400);
    }
    // $aviso['evento'], $aviso['datos']['claveacceso'], $aviso['datos']['autorizado']…
    return response()->noContent();
});

PHP puro

$aviso = Webhook::verificar(file_get_contents('php://input'), $_SERVER, getenv('INVOKA_WEBHOOK_SECRET'));
  • Use el cuerpo crudo: si lo decodifica y lo vuelve a codificar, la firma no coincide.
  • Si llega X-Invoka-Signature-V2, el SDK verifica esa (tiempo constante) y exige que su t esté a menos de 300 s de ahora: pase tolerancia para cambiarlo (0 = no comprobar). Un aviso capturado no se puede reenviar más tarde.
  • Si no llega la V2, cae a la V1 (X-Invoka-Signature), que no lleva sello de tiempo; ahí tolerancia > 0 compara con enviado_en (los reintentos lo conservan: ≥ 16000 s). Con exigirV2: true se rechazan los avisos que solo traen V1.
  • Procese cada aviso de forma idempotente, deduplicando por $aviso['entrega'] (X-Invoka-Delivery, estable entre reintentos). $aviso['version'] dice qué firma se verificó.
  • Para probar su receptor: Webhook::avisoDePrueba([...], $secret) arma cuerpo y cabeceras firmados (V1 y V2); Webhook::firmarV2($cuerpo, $secret, $t) da solo la V2.

Invoka ID

Beta privada. Por ahora la API responde 403 beta_privada salvo a las cuentas habilitadas.

Verificación de identidad (prueba de vida, cédula, biometría). Cada verificación tiene costo.

$v = $invoka->invokaId->crear(['tipo' => 'completa', 'cedula_esperada' => '0912345678']);
// envíe a la persona a $v['url_verificacion']; luego:
$invoka->invokaId->consultar($v['id']);
$invoka->invokaId->listar();
$invoka->invokaId->verificarDirecto(['selfie' => $base64, 'cedula_frontal' => $base64]);

Errores

Todas las excepciones heredan de Invoka\Sdk\Excepciones\InvokaException.

Excepción Cuándo getCodigo()
Validacion Datos inválidos (HTTP 200 con creado: false, 400 o 422) datos_invalidos
ComprobanteDuplicado (es un Conflicto) Ese secuencial ya se usó hoy en la empresa comprobante_duplicado
Conflicto XML/PDF de algo no autorizado; empresa ya registrada no_autorizado, empresa_existente
NoEncontrado Comprobante de otra cuenta o inexistente; emisor.ruc que no es suyo; el SRI sin datos no_encontrado, empresa_no_encontrada
SaldoInsuficiente Sin créditos (ambiente 2) o sin consultas SRI (402) saldo_insuficiente, sin_consultas
ProveedorSriNoAprobado Ambiente 2 sin datos de Proveedor SRI aprobados proveedor_sri_no_aprobado
CredencialInvalida Falta la API key o no existe (401) credencial_invalida
LimiteExcedido Más de 300 peticiones por minuto, tras agotar los reintentos demasiadas_peticiones
ErrorServidor 5xx o respuesta inesperada error_servidor, respuesta_invalida
ErrorRed Sin respuesta (DNS, TLS, tiempo agotado) error_red
FirmaWebhookInvalida Aviso de webhook que no pasó la verificación firma_invalida, …
try {
    $invoka->facturas->emitir($factura);
} catch (Validacion $e) {
    foreach ($e->getErrores() as $motivo) { echo $motivo, PHP_EOL; }   // en español, listos para mostrar
} catch (ComprobanteDuplicado $e) {
    $invoka->comprobantes->consultar($e->getClaveAcceso());
} catch (SaldoInsuficiente $e) {
    // recargue créditos en el panel
} catch (InvokaException $e) {
    error_log($e->getCodigo() . ' ' . $e->getMessage() . ' ' . $e->getCuerpo());
}

Además: getEstadoHttp(), getRespuesta() (el JSON completo), getCuerpo(), esReintentable() y getAvisoProveedorSri().

Reintentos

Situación ¿Reintenta?
429 (límite de 300/min) Siempre: el límite se aplica antes de procesar nada. Respeta Retry-After si viene.
502, 503, 504 o corte de red en lecturas Sí
… en emisiones, reproceso, webhook, editar empresa, firma, logo Sí: no pueden duplicar nada
… en consultas SRI, crear empresa, probar webhook, Invoka ID No: podrían cobrarse o repetirse
4xx, creado: false No (son definitivos)

Espera exponencial con jitter: espera_base · 2^(intento-1), recortada a espera_maxima.

Opciones del cliente

new Invoka('API_…', [
    'ambiente' => 1,              // 1 pruebas (por defecto) · 2 PRODUCCIÓN (consume créditos)
    'emisor' => [...],            // datos del emisor que se completan en cada comprobante
    'base_url' => 'https://invoka.com.ec/api',   // vale también solo el dominio
    'timeout' => 60,              // segundos por petición
    'timeout_conexion' => 10,
    'reintentos' => 3,
    'espera_base' => 1,           // segundos
    'espera_maxima' => 30,
    'user_agent' => 'mi-app/2.0', // se antepone al del SDK
    'cabeceras' => [],            // cabeceras extra
    'http_client' => $guzzle,     // o 'handler' => HandlerStack: proxy, logs, pruebas
]);

Para lo que el SDK no envuelva: $invoka->solicitar('GET', 'ruta', $query, $cuerpo).

Referencia rápida

Método Endpoint
ping() GET /api/ping (no valida la API key)
facturas, notasCredito, notasDebito, guias, liquidaciones ->emitir() POST /api/{tipo}/emision
retenciones->emitir() / ->emitirV1() POST /api/retencionats/emision / POST /api/retencion/emision
comprobantes->consultar() GET /api/comprobante/consulta/{clave}
comprobantes->xml() / ->pdf() GET /api/comprobante/xml/{clave} / pdf/{clave}
comprobantes->reprocesar() POST /api/comprobante/reprocesar
comprobantes->consultarYProcesar() POST /api/consultaprocesar
sri->ruc(), cedula(), consultar(), establecimientos(), razonSocial() GET /api/sri/…
creditos->saldo() / ->historial() GET /api/creditos/saldo / historial
webhook->ver() (secret enmascarado), guardar() (secret completo), eliminar(), probar() GET/POST/DELETE /api/webhook, POST /api/webhook/probar
empresa->crear(), editar(), subirFirma(), subirLogo() POST /api/empresa/crear, PUT editar, POST subirfirma, POST subirlogo
invokaId->crear(), listar(), consultar(), verificarDirecto() (beta privada) /api/id/…
publico->estado(), planes(), calcularCreditos() GET /api/status, GET /api/public/pricing/tiers, calculate

Pruebas

composer install
composer test                       # unitarios, con HTTP simulado

# Integración contra la API real. SIEMPRE en ambiente 1 (pruebas):
INVOKA_SDK_API_KEY=API_… INVOKA_SDK_RUC=… composer test:integracion
# Opcionales: INVOKA_SDK_EMITIR=1 (emite 1 factura de prueba), INVOKA_SDK_SRI=1 (1 consulta al SRI),
# INVOKA_SDK_WEBHOOK=1 (cambia el webhook y lo restaura)

El test CoberturaOpenApiTest comprueba que cada operación pública del OpenAPI tiene su método en el SDK.

Soporte: soporte@invoka.com.ec

Licencia

MIT. Ver LICENSE.