> LOADING LARAVEL-INVOICE-EXPRESS v1.0...

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.

PHP 8.4+ Laravel 12-13 MIT License DTOs Tipados Webhooks Multi-Conta SAF-T Pest PHP

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.

Instalacao
bash
composer require digitaldev-lx/laravel-invoice-express
Publicar configuracao e migrar
bash
php artisan vendor:publish --tag=invoiceexpress-config
php artisan migrate
Opcional - publicar traducoes
bash
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).

.env (obrigatorias)
bash
INVOICEEXPRESS_ACCOUNT_NAME=your-account-subdomain
INVOICEEXPRESS_API_KEY=your-api-key
.env (opcionais)
bash
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.

Fluxo end-to-end
php
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.

Recursos da facade
php
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
Clientes - lookup por VAT, name, code
php
$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']);
Sequencias - registo no Portal das Financas
php
InvoiceExpress::sequences()->create(new Sequence(
    serie: '2026',
    documentType: 'Invoice',
    currentSequenceNumber: 1,
    defaultSequence: true,
));

InvoiceExpress::sequences()->setCurrent($id);
InvoiceExpress::sequences()->register($id, 'AAJ23K'); // codigo AT
SAF-T - exportar XML para a AT
php
$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
Emitir nota de credito a partir do mesmo DTO
php
// 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.

Emitir guia de transporte
php
$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.

Estados
text
draft  --finalize()-->  final  --settle()-->  settled
                        |
                        +--cancel()-->  canceled
Transicoes programaticas
php
$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):

Listar documentos relacionados
php
$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.

Gerar PDF
php
// 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

Email com cc, assunto e corpo
php
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

Obter URL do QR code
php
$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.

Registar pagamento
php
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
CashNU
ChequeCH
BankTransferTB
DirectDebitCD
MultibancoReferenceMB
MBWayMW
CreditCardCC
PayPalPP
PromissoryNoteLC
CompensationCS
OtherOU

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).

Manager isolado por conta
php
$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

.env
bash
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

Listener para pagamento confirmado
php
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.

Consultar log de webhooks
php
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.

Trait no model
php
use DigitaldevLx\LaravelInvoiceExpress\Concerns\HasInvoiceExpressDocuments;

final class Order extends Model
{
    use HasInvoiceExpressDocuments;
}
Migration - adicionar colunas
php
$table->unsignedBigInteger('invoiceexpress_document_id')->nullable()->index();
$table->string('invoiceexpress_document_type')->nullable();
$table->string('invoiceexpress_state')->nullable();
$table->string('invoiceexpress_account_name')->nullable();
API fluente sobre o model
php
$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, ClientUpdatedCliente criado ou actualizado
ItemCreated, ItemUpdatedItem de catalogo mutado
DocumentCreatedDocumento em rascunho emitido
DocumentFinalizedDocumento finalizado
DocumentPaidDocumento liquidado
DocumentCanceledDocumento cancelado (carrega o motivo)
DocumentDeletedDocumento eliminado
EmailSentDocumento enviado por email
PdfGeneratedPDF foi descarregado
PaymentReceivedPagamento registado
PaymentCanceledPagamento cancelado
WebhookReceivedWebhook assinado recebido
WebhookSignatureFailedWebhook com assinatura invalida

Tratamento de Erros

A hierarquia de excepcoes e granular para que possa ramificar sobre o tipo de falha especifico.

Hierarquia de excepcoes
text
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)
Branching por tipo de falha
php
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).

Knobs de configuracao
bash
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.

Jobs com backoff inteligente
php
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.

Comandos disponiveis
bash
# 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.

Pest test com Http::fake()
php
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'));
});
Assertar eventos
php
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.

> COOKIE_CONSENT_REQUIRED

Utilizamos cookies essenciais para o funcionamento do site e cookies analíticos (Google Analytics) para compreender como utiliza o nosso site. Os cookies analíticos só são ativados com o seu consentimento. Política de Privacidade