storno / laravel-storno
Laravel package for Storno invoicing API — e-Factura, clients, invoices, webhooks
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
Laravel package for Storno — self-hosted invoicing with Romanian e-Factura (ANAF) integration.
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
Installation
composer require storno/laravel-storno
Publish the config file:
php artisan vendor:publish --tag=storno-config
Add to your .env:
STORNO_API_URL=https://invoices.yourapp.com STORNO_API_KEY=your-api-key STORNO_COMPANY_ID=your-company-uuid
Quick Start
Create a client and invoice
use Storno\Laravel\Facades\Storno; use Storno\Laravel\DTOs\Client; use Storno\Laravel\DTOs\Invoice; use Storno\Laravel\DTOs\InvoiceLine; // 1. Create or find a client $result = Storno::createClient(new Client( name: 'Acme SRL', type: 'company', email: 'billing@acme.ro', country: 'RO', city: 'București', cui: '12345678', )); $clientId = $result['client']['id']; $isNew = ! $result['existing']; // 2. Create an invoice $invoice = Storno::createInvoice(new Invoice( clientId: $clientId, currency: 'RON', paymentMethod: 'bank_transfer', orderNumber: 'ORD-2026-001', lines: [ new InvoiceLine( description: 'Widget Pro', quantity: 2, unitPrice: 100.00, vatRate: 21, ), new InvoiceLine( description: 'Shipping', quantity: 1, unitPrice: 15.00, vatRate: 21, ), ], )); $invoiceId = $invoice['invoice']['id']; // 3. Issue the invoice (changes status from draft to issued) if (config('storno.auto_issue')) { Storno::issueInvoice($invoiceId); } // 4. Download and store the PDF $pdf = Storno::downloadPdf($invoiceId); Storage::put("invoices/{$invoiceId}.pdf", $pdf);
Submit to e-Factura (ANAF)
// After issuing, optionally submit to Romanian e-Factura system Storno::submitInvoice($invoiceId);
Look up a company by CUI (Romanian tax ID)
$company = Storno::anafLookup('12345678'); // Returns name, address, VAT status, etc.
Configuration Reference
See config/storno.php for all options. Full docs at docs.storno.ro.
| Key | Default | Description |
|---|---|---|
api_url |
— | Your Storno instance URL |
api_key |
— | API key from Storno dashboard |
company_id |
— | Company UUID from Storno |
auto_issue |
true |
Automatically issue invoices after creation |
auto_apply_vat_rules |
true |
Apply VAT rules automatically |
document_series_id |
— | Default document series UUID |
invoice_language |
— | Invoice language (ro, en, etc.) |
payment_term_days |
30 |
Default payment term in days |
default_vat_rate |
21 |
Default VAT rate percentage |
shipping_vat_rate |
21 |
VAT rate for shipping lines |
default_unit |
buc |
Default unit of measure |
shipping_label |
Shipping |
Label for shipping line items |
discount_label |
Discount |
Label for discount line items |
invoice_notes |
— | Default notes on invoices |
internal_note_format |
#{order_number} |
Template for internal notes |
webhook_secret |
— | Secret for webhook signature verification |
webhook_path |
storno/webhook |
URL path for the webhook endpoint |
webhook_middleware |
['api'] |
Middleware for the webhook route |
timeout |
30 |
HTTP request timeout (seconds) |
retry.times |
3 |
Number of retry attempts |
retry.sleep |
1000 |
Delay between retries (ms) |
API Reference
All methods are available via the Storno:: facade or by injecting StornoClient.
Companies
$companies = Storno::listCompanies();
Clients
// Create or find a client (idempotent by email/CUI) $result = Storno::createClient(new Client( name: 'Acme SRL', type: 'company', // 'company' or 'individual' email: 'billing@acme.ro', address: 'Str. Victoriei 1', city: 'București', county: 'Ilfov', country: 'RO', postalCode: '012345', phone: '+40700000000', cui: '12345678', vatCode: 'RO12345678', isVatPayer: true, registrationNumber: 'J40/1234/2020', bankName: 'BCR', bankAccount: 'RO49AAAA1B31007593840000', contactPerson: 'Ion Popescu', notes: 'VIP', )); // Returns: ['client' => [...], 'existing' => bool] // Get client by ID $client = Storno::getClient('client-uuid'); // ANAF lookup by CUI $company = Storno::anafLookup('12345678');
Invoices
// Create invoice $invoice = Storno::createInvoice(new Invoice( clientId: 'client-uuid', lines: [...], currency: 'RON', // 'RON', 'EUR', 'USD', etc. paymentMethod: 'bank_transfer', // 'bank_transfer'|'cash'|'card'|'cheque'|'other' issueDate: '2026-04-04', dueDate: '2026-05-04', orderNumber: 'ORD-001', internalNote: '#ORD-001', idempotencyKey: 'unique-key-for-order', autoApplyVatRules: true, documentSeriesId: 'series-uuid', language: 'ro', notes: 'Thank you for your business.', )); // Get invoice $invoice = Storno::getInvoice('invoice-uuid'); // Issue invoice (draft -> issued) Storno::issueInvoice('invoice-uuid'); // Submit to e-Factura ANAF Storno::submitInvoice('invoice-uuid'); // Download PDF (returns raw binary) $pdfContent = Storno::downloadPdf('invoice-uuid'); file_put_contents('/path/to/invoice.pdf', $pdfContent);
Document Series
$series = Storno::listDocumentSeries(); // Returns array of series with id, name, prefix, nextNumber
VAT Rates
$rates = Storno::listVatRates(); // Returns array of available VAT rates for this company
Webhooks
// Register a webhook $webhook = Storno::createWebhook( url: 'https://yourapp.com/storno/webhook', events: ['invoice.issued', 'invoice.validated', 'invoice.rejected', 'invoice.paid'], description: 'My app webhook', ); // Returns: ['uuid' => '...', 'secret' => '...'] // Save the 'secret' as STORNO_WEBHOOK_SECRET in your .env // Delete a webhook Storno::deleteWebhook('webhook-uuid');
Webhooks
Setup
Register a webhook using the Artisan command (recommended):
php artisan storno:webhook:register
# or with explicit URL:
php artisan storno:webhook:register --url=https://yourapp.com/storno/webhook
The command automatically saves the secret to your .env file.
The webhook endpoint is registered automatically at POST /storno/webhook (configurable via STORNO_WEBHOOK_PATH).
Events
Listen for Storno events in your EventServiceProvider or using #[AsListener]:
use Storno\Laravel\Events\InvoiceIssued; use Storno\Laravel\Events\InvoiceValidated; use Storno\Laravel\Events\InvoiceRejected; use Storno\Laravel\Events\InvoicePaid; use Storno\Laravel\Events\WebhookReceived; // fires for every webhook class AppServiceProvider extends ServiceProvider { public function boot(): void { Event::listen(InvoiceIssued::class, function (InvoiceIssued $event) { $invoiceId = $event->payload->data['invoiceId']; // Send confirmation email, update order status, etc. }); Event::listen(InvoicePaid::class, function (InvoicePaid $event) { // Mark order as paid }); Event::listen(InvoiceRejected::class, function (InvoiceRejected $event) { // Handle ANAF rejection — log, notify admin, etc. }); } }
The WebhookPayload object has:
$event->payload->event // e.g. 'invoice.issued' $event->payload->id // event UUID $event->payload->data // array with invoice data $event->payload->occurredAt // ISO 8601 timestamp $event->payload->raw // full raw payload array
Excluding the webhook route from CSRF
Add the webhook path to your VerifyCsrfToken middleware exceptions:
// app/Http/Middleware/VerifyCsrfToken.php protected $except = [ 'storno/webhook', ];
Artisan Commands
Test connection
php artisan storno:test
Verifies the API connection and lists companies on the instance.
Register webhook
php artisan storno:webhook:register
php artisan storno:webhook:register --url=https://yourapp.com/storno/webhook
php artisan storno:webhook:register --url=... --events=invoice.issued --events=invoice.paid
php artisan storno:webhook:register --url=... --description="Production webhook"
List document series
php artisan storno:series
Shows all available document series with their ID, prefix, and next number.
Error Handling
use Storno\Laravel\Exceptions\StornoApiException; use Storno\Laravel\Exceptions\StornoConnectionException; use Storno\Laravel\Exceptions\InvalidSignatureException; try { $invoice = Storno::createInvoice($dto); } catch (StornoApiException $e) { // HTTP 4xx/5xx from the Storno API $statusCode = $e->getStatusCode(); // int $body = $e->getResponseBody(); // array Log::error('Storno API error', ['status' => $statusCode, 'body' => $body]); } catch (StornoConnectionException $e) { // Network error, timeout, etc. Log::error('Storno connection error', ['message' => $e->getMessage()]); }
Dependency Injection
You can inject StornoClient directly instead of using the facade:
use Storno\Laravel\StornoClient; class InvoiceService { public function __construct(private StornoClient $storno) {} public function createForOrder(Order $order): array { return $this->storno->createInvoice(/* ... */); } }
Links
License
MIT — see LICENSE.