Laravel InvoiceXpress
Integracao completa com a API V2 da InvoiceXpress, plataforma de faturacao portuguesa certificada pela AT (#192). Cobre toda a superficie da API com type safety, webhooks assinados e multi-conta em runtime.
Overview
O Laravel InvoiceXpress integra a sua aplicacao Laravel com a InvoiceXpress API V2 -- a plataforma de faturacao portuguesa certificada pela Autoridade Tributaria (certificado #192). Cobre toda a superficie da API: faturas, faturas simplificadas, notas de credito, notas de debito, recibos, orcamentos (quotes / proformas / fees notes), guias (transporte / envio / devolucao / global), encomendas a fornecedores, clientes, artigos, impostos, sequencias, contas, tesouraria e exportacao SAF-T.
Tipos de documentos suportados
- Faturas
- Faturas Simplificadas
- Fatura-Recibos
- Faturas a Dinheiro
- Faturas VAT MOSS
- Notas de Credito
- Notas de Debito
- Recibos
- Orcamentos (Quotes)
- Pro-formas
- Notas de Honorarios
- Estimativas Genericas
- Guias de Transporte
- Guias de Envio
- Guias de Devolucao
- Guias Globais / Encomendas a Fornecedores
Instalacao
Instale via Composer (service provider e auto-descoberto), publique a configuracao e execute as migrations para o log de webhooks.
composer require digitaldev-lx/laravel-invoice-express
php artisan vendor:publish --tag=invoiceexpress-config
php artisan migrate
php artisan vendor:publish --tag=invoiceexpress-translations
Configuracao
Adicione as credenciais da InvoiceXpress ao ficheiro .env. O subdominio corresponde ao prefixo do URL do dashboard (e.g., acme em acme.app.invoicexpress.com).
INVOICEEXPRESS_ACCOUNT_NAME=your-account-subdomain
INVOICEEXPRESS_API_KEY=your-api-key
INVOICEEXPRESS_TIMEOUT=15
INVOICEEXPRESS_RETRY_TIMES=3
INVOICEEXPRESS_RETRY_BACKOFF_MS=1000
INVOICEEXPRESS_RATE_LIMIT=780
INVOICEEXPRESS_CACHE=redis
INVOICEEXPRESS_LOG=false
INVOICEEXPRESS_LOG_CHANNEL=stack
# Webhooks
INVOICEEXPRESS_WEBHOOKS_ENABLED=true
INVOICEEXPRESS_WEBHOOKS_PREFIX=invoiceexpress/webhooks
INVOICEEXPRESS_WEBHOOK_SECRET=whsec_a_long_random_string
INVOICEEXPRESS_WEBHOOKS_LOG=true
Variaveis principais
| Variavel | Descricao |
|---|---|
| INVOICEEXPRESS_ACCOUNT_NAME | Subdominio da conta InvoiceXpress |
| INVOICEEXPRESS_API_KEY | API key gerada em /users/api |
| INVOICEEXPRESS_RATE_LIMIT | Quota por minuto/conta (default 780) |
| INVOICEEXPRESS_CACHE | Cache store para o throttler preventivo |
| INVOICEEXPRESS_WEBHOOK_SECRET | Segredo HMAC-SHA256 para verificar webhooks |
Quick Start
Ciclo completo: criar cliente, emitir fatura, finalizar, gerar PDF, enviar por email e marcar como paga.
use DigitaldevLx\LaravelInvoiceExpress\Facades\InvoiceExpress;
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\Client;
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\DocumentItem;
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\Invoice;
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\Tax;
use DigitaldevLx\LaravelInvoiceExpress\Enums\Country;
use DigitaldevLx\LaravelInvoiceExpress\Enums\DocumentType;
// 1. Criar cliente
$client = InvoiceExpress::clients()->create(new Client(
name: 'Acme Lda',
code: 'ACM-001',
email: 'finance@acme.pt',
fiscalId: '500000000',
address: 'Rua das Flores 1',
city: 'Lisboa',
postalCode: '1000-001',
country: Country::PT,
));
// 2. Criar fatura em rascunho
$invoice = InvoiceExpress::invoices()->create(new Invoice(
type: DocumentType::Invoice,
date: '2026-05-01',
dueDate: '2026-05-31',
items: [
new DocumentItem(
name: 'Consultoria',
quantity: 4,
unitPrice: 100.00,
tax: new Tax(name: 'IVA23', value: 23.0),
),
],
client: ['name' => 'Acme Lda', 'fiscal_id' => '500000000'],
));
// 3. Finalizar, gerar PDF e enviar por email
$id = (int) $invoice['id'];
InvoiceExpress::invoices()->finalize($id);
$pdfBytes = InvoiceExpress::invoices()->pdf($id);
InvoiceExpress::invoices()->email($id, $emailMessage);
// 4. Marcar como paga
InvoiceExpress::invoices()->payment($id, $paymentDto);
Recursos disponiveis
Todos os recursos sao acessiveis atraves da facade InvoiceExpress ou injectando DigitaldevLx\LaravelInvoiceExpress\InvoiceExpress.
InvoiceExpress::clients(); // Clientes
InvoiceExpress::items(); // Artigos / catalogo
InvoiceExpress::taxes(); // Impostos
InvoiceExpress::sequences(); // Series de numeracao
InvoiceExpress::accounts(); // Contas bancarias / caixa
InvoiceExpress::treasury(); // Movimentos de tesouraria
InvoiceExpress::saft(); // Exportacao SAF-T
InvoiceExpress::invoices(); // Faturas e similares
InvoiceExpress::estimates(); // Orcamentos / proformas
InvoiceExpress::guides(); // Guias de transporte
InvoiceExpress::purchaseOrders(); // Encomendas a fornecedores
$result = InvoiceExpress::clients()->all(['page' => 1, 'per_page' => 30]);
$client = InvoiceExpress::clients()->find(42);
$client = InvoiceExpress::clients()->findByName('Acme Lda');
$client = InvoiceExpress::clients()->findByCode('ACM-001');
InvoiceExpress::clients()->create(new Client(
name: 'Acme Lda',
fiscalId: '500000000',
country: Country::PT,
));
InvoiceExpress::clients()->update($id, ['email' => 'new@acme.pt']);
InvoiceExpress::sequences()->create(new Sequence(
serie: '2026',
documentType: 'Invoice',
currentSequenceNumber: 1,
defaultSequence: true,
));
InvoiceExpress::sequences()->setCurrent($id);
InvoiceExpress::sequences()->register($id, 'AAJ23K'); // codigo AT
$xml = InvoiceExpress::saft()->generate(2026, 4); // Abril 2026
file_put_contents(storage_path('saft-2026-04.xml'), $xml);
Documentos
O mesmo recurso invoices() emite todos os documentos do tipo fatura, alterando o DocumentType. O routing para invoices.json, simplified_invoices.json, credit_notes.json, etc. acontece automaticamente.
Tipos de documentos
| DocumentType | Endpoint | Use case |
|---|---|---|
| Invoice | invoices.json | Fatura standard |
| SimplifiedInvoice | simplified_invoices.json | Ate €1000 (€100 para nao-empresas) |
| InvoiceReceipt | invoice_receipts.json | Fatura + recibo num so documento |
| CreditNote | credit_notes.json | Reembolsos / correcoes |
| DebitNote | debit_notes.json | Cobrancas adicionais |
| Receipt | receipts.json | Recibo contra fatura previa |
| CashInvoice | cash_invoices.json | Fatura paga na hora |
| VatMossInvoice | vat_moss_invoices.json | EU VAT MOSS |
// Tipo default e Invoice
InvoiceExpress::invoices()->create($invoiceDto);
// Emitir nota de credito alterando o type
InvoiceExpress::invoices()->create(
new Invoice(
type: DocumentType::CreditNote,
date: '2026-05-15',
items: [...],
client: [...],
),
);
// Ou passar explicitamente
InvoiceExpress::invoices()->create($invoiceDto, DocumentType::SimplifiedInvoice);
Orcamentos e Guias
Os recursos estimates() e guides() seguem o mesmo padrao -- routing automatico via EstimateType e GuideType.
$guide = InvoiceExpress::guides()->create(new Guide(
type: GuideType::Transport,
date: '2026-05-15',
loadedAt: '2026-05-15 10:00',
loadedFrom: 'Lisboa',
loadedTo: 'Porto',
vehicleRegistration: '00-AA-00',
items: [new DocumentItem(name: 'Pallet', quantity: 2)],
client: ['name' => 'Acme'],
));
Document Lifecycle
Cada recurso de documento (invoices(), estimates(), guides(), purchaseOrders()) tem os mesmos metodos de transicao, vindos de concerns reutilizaveis.
draft --finalize()--> final --settle()--> settled
|
+--cancel()--> canceled
$id = (int) $invoice['id'];
// Verbos especificos (recomendados)
InvoiceExpress::invoices()->finalize($id);
InvoiceExpress::invoices()->cancel($id, 'Cliente desistiu');
InvoiceExpress::invoices()->settle($id, 'Pago via TB');
// API generica
InvoiceExpress::invoices()->changeState($id, DocumentState::Final);
Documentos relacionados
Cada documento mantem ligacoes a outros (recibos, notas de credito, devolucoes):
$related = InvoiceExpress::invoices()->relatedDocuments($id);
// ['related_documents' => [['id' => 8, 'type' => 'Receipt'], ...]]
PDF e Envio por Email
A InvoiceXpress segue um fluxo em dois passos: pedir um URL temporario do PDF (valido 24h) e depois fazer download. O pacote expoe ambos.
// Apenas o envelope JSON (URL valido 24h)
$envelope = InvoiceExpress::invoices()->pdfUrl($id);
$url = $envelope['output']['pdfUrl'];
// Os dois passos numa so chamada (devolve binary)
$pdfBytes = InvoiceExpress::invoices()->pdf($id);
file_put_contents(storage_path('invoice.pdf'), $pdfBytes);
// Segunda via (com marca de agua "2.a via")
$pdfBytes = InvoiceExpress::invoices()->pdf($id, secondCopy: true);
Enviar por email
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\EmailMessage;
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\EmailRecipient;
InvoiceExpress::invoices()->email($id, new EmailMessage(
to: new EmailRecipient(email: 'finance@acme.pt'),
subject: 'A sua fatura n.o FAC2026/123',
body: 'Em anexo a fatura referente aos servicos de Maio.',
cc: new EmailRecipient(email: 'contabilidade@acme.pt'),
logo: true,
));
QR Code
$qr = InvoiceExpress::invoices()->qrCode($id);
$qr = InvoiceExpress::guides()->qrCode($guideId);
Pagamentos
Registar e cancelar pagamentos contra documentos finalizados, com codigos SAF-T tipados via enum PaymentMethod.
use DigitaldevLx\LaravelInvoiceExpress\DataTransferObjects\Payment;
use DigitaldevLx\LaravelInvoiceExpress\Enums\PaymentMethod;
InvoiceExpress::invoices()->payment($id, new Payment(
paymentMechanism: PaymentMethod::BankTransfer,
amount: 246.00,
paymentDate: '2026-05-15',
observations: 'IBAN PT50...',
));
// Cancelar um pagamento previamente registado
InvoiceExpress::invoices()->cancelPayment($id, $paymentId, note: 'Erro de imputacao');
Codigos SAF-T do enum PaymentMethod
| Metodo | Codigo |
|---|---|
| Cash | NU |
| Cheque | CH |
| BankTransfer | TB |
| DirectDebit | CD |
| MultibancoReference | MB |
| MBWay | MW |
| CreditCard | CC |
| PayPal | PP |
| PromissoryNote | LC |
| Compensation | CS |
| Other | OU |
Multi-conta em runtime
Trocar credenciais em runtime -- util quando uma aplicacao Laravel serve varias identidades de faturacao (por exemplo, um SaaS para gabinetes de contabilidade).
$secondCompany = InvoiceExpress::useAccount('outra-empresa', 'api-key-da-outra');
$secondCompany->invoices()->all();
$secondCompany->saft()->generate(2026, 4);
// O singleton default fica intacto
InvoiceExpress::client()->accountName(); // 'your-default-account'
useAccount() devolve um manager fresco ligado a um clone do HTTP client com as novas credenciais. As caches de recursos sao isoladas por clone, portanto os eventos continuam a disparar correctamente.
Webhooks
A InvoiceXpress envia notificacoes quando documentos sao emitidos, finalizados, pagos ou cancelados. O pacote regista o endpoint receptor automaticamente em POST /invoiceexpress/webhooks.
1. Activar o receiver
INVOICEEXPRESS_WEBHOOKS_ENABLED=true
INVOICEEXPRESS_WEBHOOKS_PREFIX=invoiceexpress/webhooks
INVOICEEXPRESS_WEBHOOK_SECRET=whsec_a_long_random_string
INVOICEEXPRESS_WEBHOOKS_LOG=true
2. Assinar o payload
Configure a InvoiceXpress para enviar X-InvoiceXpress-Signature: <hmac> -- onde <hmac> e o HMAC-SHA256 hex do raw body assinado com o secret. Se o secret nao estiver definido, a verificacao e ignorada (com warning) -- util para dev local com expose ou ngrok.
3. Reagir a eventos
use DigitaldevLx\LaravelInvoiceExpress\Events\DocumentPaid;
use DigitaldevLx\LaravelInvoiceExpress\Events\WebhookReceived;
class HandlePaidInvoice
{
public function handle(DocumentPaid $event): void
{
$documentId = $event->documentId;
$type = $event->type;
$payload = $event->data;
// sincronizar a Order, enviar email de agradecimento, etc.
}
}
// Ou ouvir tudo genericamente:
class LogWebhook
{
public function handle(WebhookReceived $event): void
{
Log::info('InvoiceXpress webhook', $event->payload->toArray());
}
}
4. Audit log
Quando INVOICEEXPRESS_WEBHOOKS_LOG=true, todos os payloads sao persistidos em invoice_express_webhook_logs.
use DigitaldevLx\LaravelInvoiceExpress\Models\InvoiceExpressWebhookLog;
$lastFinalized = InvoiceExpressWebhookLog::query()
->where('event', 'document.finalized')
->latest('received_at')
->first();
Integracao Eloquent
Para aplicacoes onde cada linha do dominio (Order, Subscription, ...) corresponde a uma fatura InvoiceXpress, use a trait shortcut HasInvoiceExpressDocuments.
use DigitaldevLx\LaravelInvoiceExpress\Concerns\HasInvoiceExpressDocuments;
final class Order extends Model
{
use HasInvoiceExpressDocuments;
}
$table->unsignedBigInteger('invoiceexpress_document_id')->nullable()->index();
$table->string('invoiceexpress_document_type')->nullable();
$table->string('invoiceexpress_state')->nullable();
$table->string('invoiceexpress_account_name')->nullable();
$order = Order::find(1);
$order->createInvoiceXpressInvoice($invoiceDto);
$order->finalizeInvoiceXpress();
$order->emailInvoiceXpress($emailMessage);
$order->settleInvoiceXpress(new Payment(
paymentMechanism: PaymentMethod::BankTransfer,
amount: $order->total,
paymentDate: now()->toDateString(),
));
$order->cancelInvoiceXpress('Customer refunded');
$pdf = $order->downloadInvoiceXpressPdf();
// Predicates
$order->invoiceXpressDocumentId(); // ?int
$order->invoiceXpressIsFinalized(); // bool
$order->invoiceXpressIsPaid();
$order->invoiceXpressIsCanceled();
Eventos
Cada operacao dispara um evento final readonly class com propriedades publicas e imutaveis. Use o auto-discovery do Laravel ou registe manualmente no EventServiceProvider.
| Evento | Trigger |
|---|---|
| ClientCreated, ClientUpdated | Cliente criado ou actualizado |
| ItemCreated, ItemUpdated | Item de catalogo mutado |
| DocumentCreated | Documento em rascunho emitido |
| DocumentFinalized | Documento finalizado |
| DocumentPaid | Documento liquidado |
| DocumentCanceled | Documento cancelado (carrega o motivo) |
| DocumentDeleted | Documento eliminado |
| EmailSent | Documento enviado por email |
| PdfGenerated | PDF foi descarregado |
| PaymentReceived | Pagamento registado |
| PaymentCanceled | Pagamento cancelado |
| WebhookReceived | Webhook assinado recebido |
| WebhookSignatureFailed | Webhook com assinatura invalida |
Tratamento de Erros
A hierarquia de excepcoes e granular para que possa ramificar sobre o tipo de falha especifico.
RuntimeException
+-- InvoiceExpressException (base - apanhar para "qualquer falha")
+-- AuthenticationException (HTTP 401, expoe accountName)
+-- BadRequestException (HTTP 400)
+-- ValidationException (HTTP 422 - field-level errors)
+-- NotFoundException (HTTP 404 - expoe resource + id)
+-- RateLimitException (HTTP 429 - expoe retryAfter)
+-- ServerException (HTTP 5xx)
+-- UnknownEndpointException (developer error)
+-- WebhookException (signature invalida / payload malformado)
use DigitaldevLx\LaravelInvoiceExpress\Exceptions\RateLimitException;
use DigitaldevLx\LaravelInvoiceExpress\Exceptions\ValidationException;
try {
InvoiceExpress::invoices()->create($dto);
} catch (ValidationException $e) {
foreach ($e->getFieldErrors() as $field => $message) {
logger()->warning("InvoiceXpress validation: {$field} - {$message}");
}
} catch (RateLimitException $e) {
sleep($e->retryAfter); // ou release() do queued job com delay
}
Retry, Backoff e Rate Limiting
A InvoiceXpress permite 780 requests por minuto por conta. O HTTP client retenta automaticamente em 429/5xx/falhas de conexao usando Http::retry() com backoff exponencial (1s -> 2s -> 4s).
INVOICEEXPRESS_RETRY_TIMES=3 # 0 desactiva retry
INVOICEEXPRESS_RETRY_BACKOFF_MS=1000
Definindo INVOICEEXPRESS_CACHE=redis (ou qualquer cache store), o cliente activa um throttler preventivo: levanta RateLimitException localmente quando 95% da quota minuto e atingida -- de modo a que jobs em queue facam backoff antes da InvoiceXpress responder 429.
use DigitaldevLx\LaravelInvoiceExpress\Exceptions\RateLimitException;
try {
InvoiceExpress::invoices()->all();
} catch (RateLimitException $e) {
$this->release($e->retryAfter); // job back-off
}
Console Commands
Comandos artisan para teste, sincronizacao e exportacao SAF-T. Todos aceitam --account= e --key= para uso ad-hoc multi-conta.
# Smoke-test da API key
php artisan invoiceexpress:test-connection
php artisan invoiceexpress:test-connection --account=other --key=other-api-key
# Dump tabular de todas as sequencias
php artisan invoiceexpress:sync-sequences
# Gerar SAF-T XML para um periodo
php artisan invoiceexpress:saft --year=2026 --month=4 --out=storage/saft.xml
Testar a Integracao
O proprio pacote usa Http::fake(). A sua aplicacao pode fazer o mesmo.
use Illuminate\Support\Facades\Http;
use DigitaldevLx\LaravelInvoiceExpress\Facades\InvoiceExpress;
it('creates an invoice on the API', function (): void {
Http::fake([
'*invoicexpress.com/invoices.json*' => Http::response([
'invoice' => ['id' => 99, 'status' => 'draft'],
], 201),
]);
$result = InvoiceExpress::invoices()->create($invoiceDto);
expect($result['id'])->toBe(99);
Http::assertSent(fn ($request) => $request->method() === 'POST'
&& str_contains($request->url(), '/invoices.json'));
});
use Illuminate\Support\Facades\Event;
use DigitaldevLx\LaravelInvoiceExpress\Events\DocumentCreated;
Event::fake();
InvoiceExpress::invoices()->create($invoiceDto);
Event::assertDispatched(DocumentCreated::class);
Para webhooks, use postJson() contra /invoiceexpress/webhooks com um header X-InvoiceXpress-Signature valido.
$ composer require digitaldev-lx/laravel-invoice-express
Pronto para automatizar a faturacao com a InvoiceXpress?
Consulte o repositorio no GitHub para a documentacao completa, issues e contribuicoes.