timbrix/sdk-php es el cliente oficial de Timbrix para PHP 8.1 o superior. No depende de ningún framework (solo de Guzzle), devuelve modelos de solo lectura con autocompletado y lanza excepciones tipadas en lugar de errores genéricos de HTTP.
Instalación
Quickstart
Sigue el mismo flujo que en Quickstart: regístrate en app.timbrix.mx/register, copia tu API key de sandbox desde Configuración → API Keys, y crea cliente → producto → CFDI en ese orden.Opciones del cliente
httpClient (un GuzzleHttp\ClientInterface propio, por ejemplo con un proxy). El SDK envía el canal sdk en el header X-Timbrix-Client, que queda registrado en cada CFDI como channel. Una opción desconocida lanza InvalidArgumentException.
Cada API key pertenece a una sola organización; para emitir con otro RFC emisor, usa una API key de esa organización.
Idempotencia y reintentos
Un CFDI duplicado es un problema fiscal real, así que el SDK es estricto con qué reintenta:- Solo se reintenta ante errores de red,
429,5xxy, conidempotencyKey, el409 PAC_STAMP_PENDING, con espera exponencial y respetandoRetry-After. Por defecto son 2 reintentos (maxRetries). GETyDELETEse reintentan siempre.invoices->createeinvoices->registerPaymentse reintentan solo si pasasidempotencyKey. Sin ella, un fallo de red o un5xxllega como excepción y el SDK no repite el timbrado.cancel,sendEmaily losPOST/PUTde clientes y productos nunca se reintentan automáticamente.previewPdfsí, porque no timbra nada.- El SDK no inventa una
idempotencyKey: usa una llave estable que tú controles, como el id de tu pedido. Con la misma llave, un reintento devuelve el CFDI ya timbrado en vez de crear otro. - Con
idempotencyKey, un409con códigoPAC_STAMP_PENDING(otra petición con la misma llave sigue en curso) también se reintenta. Con elmaxRetriespor defecto (2) el SDK espera solo unos segundos, así que unPAC_STAMP_PENDINGque dure más llega comoConflictException: reintenta más tarde (por ejemplo, desde un job en cola) con la mismaidempotencyKey.
PAC_STAMP_UNCONFIRMED, no se sabe todavía si el PAC timbró. Llega como ConflictException y no se reintenta: espera el webhook invoice.stamped o invoice.stamping_failed, o vuelve a intentar más tarde con la misma idempotencyKey.
Del mismo modo, PAC_CANCEL_UNCONFIRMED al cancelar significa que no se sabe si el SAT procesó la cancelación: consulta la factura con $timbrix->invoices->get($uuid) antes de intentarlo de nuevo.
Manejo de errores
Toda petición fallida lanza una subclase deTimbrix\Exceptions\TimbrixException, nunca una excepción de Guzzle.
Todas exponen
getStatusCode() (0 si no hubo respuesta), getErrorCode() (el código de dominio de la API), getSuggestion(), getMessages() y getBody(). RateLimitException agrega getRetryAfter().
Verificar webhooks
Timbrix firma el body exacto de cada webhook con HMAC-SHA256 y lo manda en el headerX-Webhook-Signature con el formato sha256=<hex>. Webhook::constructEvent verifica la firma y devuelve el evento ya decodificado. Pásale el body crudo, tal como llegó: si lo decodificas y lo vuelves a serializar, la firma ya no coincide.
VerifyCsrfToken en Laravel 10 o anterior, validateCsrfTokens(except: [...]) en Laravel 11), porque Timbrix no envía el token.
Si solo necesitas saber si la firma es válida, Webhook::verify($payload, $signature, $secret) devuelve un booleano y nunca lanza excepciones.
Para los eventos disponibles y la forma de cada payload, ver Create Webhook.
Laravel
El SDK funciona hoy en Laravel con el ejemplo de arriba. Un paquete dedicado para Laravel (service provider y facade) está planeado, sin fecha definida.Qué no incluye la v1
- Series y webhooks: se configuran una sola vez desde el panel. Al timbrar, eliges la serie por nombre con
seriesen el payload. Ver Series y folios. - Autenticación: solo API key. No hay soporte de bearer token ni OAuth.
- Administración: organizaciones, API keys, usuarios y logs se manejan desde el panel o el CLI de Timbrix.
- Exportaciones masivas y reportes.