invoka / sdk
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).
Requires
- php: >=8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.4
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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
- Ambientes: pruebas y producción
- Primeros pasos
- Emitir comprobantes
- Consultar, descargar y reprocesar
- Consultas al SRI
- Créditos
- Empresas, firma y logo
- Webhooks
- Invoka ID
- Errores
- Reintentos
- Opciones del cliente
- Referencia rápida
- Pruebas
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 traeaviso_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ónemisordel cliente.emisor.fecha_emision: hoy en Ecuador (aaaa/mm/dd). El SRI solo acepta la fecha del día. Si mandaaaaa-mm-ddo unDateTime, se convierte.codigo_establecimientoycodigo_puntoemisiona 3 dígitos, ysecuenciala 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
secretcompleto solo viene en la respuesta deguardar()(POST /api/webhook);ver()lo devuelve enmascarado (••••••••XXXX) y no sirve para verificar. - La URL tiene que ser pública:
localhosty 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 sutesté a menos de 300 s de ahora: pasetoleranciapara 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 conenviado_en(los reintentos lo conservan: ≥ 16000 s). ConexigirV2: truese 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_privadasalvo 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.