A Laravel package for generating and submitting TicketBAI (Ticket BAI) invoices for the Basque Country (Euskadi), Spain. This package provides a flexible and configurable solution for integrating TicketBAI compliance into your Laravel application.
- Features
- Requirements
- Installation
- Configuration
- Usage
- Database Configuration
- API Reference
- Examples
- Testing
- Contributing
- License
- ✅ Generate TicketBAI-compliant invoices
- ✅ Automatic invoice signing with X.509 certificates
- ✅ Queue-based invoice submission to TicketBAI API
- ✅ Flexible database table and column configuration
- ✅ Support for custom table structures
- ✅ Optional columns (signature, data, territory) for maximum flexibility
- ✅ Encadenamiento: firma y territorio siempre en columna JSON
databajo clave configurable (por defectoticketbai) - ✅ QR code generation for invoices
- ✅ Support for multiple territories (Araba, Bizkaia, Gipuzkoa)
- ✅ Automatic fingerprint calculation from previous invoices
- ✅ Artisan command to resend failed/pending invoices (
ticketbai:resend)
- PHP >= 8.2
- Laravel >= 8.0 (tested with Laravel 10.x and 11.x)
- barnetik/ticketbai package
- X.509 certificate (.p12 file) for signing invoices
- TicketBAI license and credentials
Install the package via Composer:
composer require ebethus/laravel-ticketbaiPublish the configuration file:
php artisan vendor:publish --tag=ticketbai-configThis will create config/ticketbai.php where you can configure table and column mappings.
If you want to use the default invoice table structure:
php artisan migrateNote: You can also use your own table structure by configuring column mappings (see Database Configuration).
Add your TicketBAI credentials to config/services.php:
'ticketbai' => [
'license' => env('TICKETBAI_LICENSE', ''),
'nif' => env('TICKETBAI_NIF', ''),
'appName' => env('TICKETBAI_APP_NAME', ''),
'appVersion' => env('TICKETBAI_APP_VERSION', ''),
'certPassword' => env('TICKETBAI_CERT_PASSWORD', ''),
'disk' => env('TICKETBAI_DISK', 'local'),
],Add these to your .env file:
TICKETBAI_LICENSE=TB12345678
TICKETBAI_NIF=B1111111
TICKETBAI_APP_NAME=My Application
TICKETBAI_APP_VERSION=1.0
TICKETBAI_CERT_PASSWORD=your_certificate_password
TICKETBAI_DISK=local
TICKETBAI_CERT_PATH=certificado.p12Use TICKETBAI_CERT_PATH to override the certificate path. It can be a path relative to storage_path() (e.g. certificado.p12 for storage/certificado.p12) or an absolute path (e.g. /etc/certs/ticketbai.p12 on Linux).
By default the package looks for the X.509 certificate (.p12) at storage/certificado.p12. Set TICKETBAI_CERT_PATH in your .env to use a different path (relative to storage_path() or absolute). The certificate is used to sign invoices before submission.
use EBethus\LaravelTicketBAI\TicketBAI;
// Get TicketBAI instance (configured via service provider)
$ticketbai = app('ticketbai');
// Or use the facade
use TicketBAI;
// Set issuer information
$ticketbai->issuer(
nif: 'B12345678',
name: 'Company Name',
idIssuer: 1,
serie: '' // Optional
);
// Set VAT percentage
$ticketbai->setVat(21); // 21% VAT
// Add invoice items
$ticketbai->add(
desc: 'Product description',
unitPrice: 100.00,
q: 2,
discount: 0 // Optional
);
// Generate and sign invoice
$qrUrl = $ticketbai->invoice(
territory: 'BIZKAIA', // or 'ARABA', 'GIPUZKOA'
description: 'Invoice description'
);
// The invoice is automatically saved and queued for submission
// $qrUrl contains the QR code URL for the invoiceuse EBethus\LaravelTicketBAI\TicketBAI;
class InvoiceController extends Controller
{
public function __construct(
protected TicketBAI $ticketbai
) {}
public function create(Request $request)
{
$this->ticketbai->issuer(
nif: $request->nif,
name: $request->company_name,
idIssuer: $request->issuer_id
);
$this->ticketbai->setVat(21);
foreach ($request->items as $item) {
$this->ticketbai->add(
desc: $item['description'],
unitPrice: $item['price'],
q: $item['quantity'],
discount: $item['discount'] ?? 0
);
}
$qrUrl = $this->ticketbai->invoice(
territory: 'BIZKAIA',
description: $request->description
);
return response()->json(['qr_url' => $qrUrl]);
}
}You can attach additional JSON data to invoices:
$ticketbai->data([
'order_id' => 12345,
'customer_id' => 67890,
'custom_field' => 'value'
]);This data will be stored in the data column if configured (see Database Configuration).
The library supports flexible table and column configuration, allowing you to use your existing database structure.
The default migration creates an invoices table with columns issuer, provider_reference, path, data, sent, and timestamps. TicketBAI stores signature and territory in the data JSON column under the key ticketbai.
If you use your own table with different column names (e.g. transaction_id instead of issuer), override the mappings via environment variables. Example: TICKETBAI_COLUMN_ISSUER=transaction_id, TICKETBAI_COLUMN_NUMBER=invoice_number, TICKETBAI_COLUMN_SENT=attempted_at.
Configure column mappings in config/ticketbai.php:
'table' => [
'name' => env('TICKETBAI_TABLE_NAME', 'invoices'),
'columns' => [
// Defaults match the default migration. For your own table use env, e.g.:
// TICKETBAI_COLUMN_ISSUER=transaction_id, TICKETBAI_COLUMN_NUMBER=invoice_number
'issuer' => env('TICKETBAI_COLUMN_ISSUER', 'issuer'),
'number' => env('TICKETBAI_COLUMN_NUMBER', 'provider_reference'),
'territory' => env('TICKETBAI_COLUMN_TERRITORY', 'territory'),
'signature' => env('TICKETBAI_COLUMN_SIGNATURE', 'signature'),
'path' => env('TICKETBAI_COLUMN_PATH', 'path'),
'data' => env('TICKETBAI_COLUMN_DATA', 'data'),
'sent' => env('TICKETBAI_COLUMN_SENT', 'sent'),
'created_at' => env('TICKETBAI_COLUMN_CREATED_AT', 'created_at'),
'updated_at' => env('TICKETBAI_COLUMN_UPDATED_AT', 'updated_at'),
],
],Use these only when you have a custom table with different column names:
TICKETBAI_TABLE_NAME=invoices
TICKETBAI_COLUMN_ISSUER=transaction_id
TICKETBAI_COLUMN_NUMBER=provider_reference
TICKETBAI_COLUMN_TERRITORY=territory
TICKETBAI_COLUMN_SIGNATURE=signature
TICKETBAI_COLUMN_PATH=path
TICKETBAI_COLUMN_DATA=data
TICKETBAI_COLUMN_SENT=attempted_at
TICKETBAI_COLUMN_CREATED_AT=created_at
TICKETBAI_COLUMN_UPDATED_AT=updated_atSignature and territory are always stored in the JSON data column under a configurable key so that encadenamiento (signature chaining) works. Default key: ticketbai. Set in .env or config:
TICKETBAI_DATA_KEY=ticketbaiThe package stores and reads signature (first 100 chars) and territory under data->ticketbai. Path stays in the path column. Example:
{
"ticketbai": {
"signature": "first 100 chars of chain signature",
"territory": "02"
},
"order_id": 12345
}Your table needs: id, issuer, number, path (file path), data (JSON), sent, created_at, updated_at. Other providers can use other keys in data (e.g. data->other_provider).
data: Set tonullor empty to use the default column name'data'.
path: Required - stores the signed XML file path (filesystem).data: Required - JSON column where TicketBAI stores signature and territory underTICKETBAI_DATA_KEY.
Set the issuer information for the invoice.
$nif: Tax identification number (NIF/CIF)$name: Company name$idIssuer: Internal issuer ID (used for database storage)$serie: Optional invoice series
Set the VAT percentage for invoice items.
$vatPerc: VAT percentage (e.g., 21 for 21%)
Add an item to the invoice.
$desc: Item description$unitPrice: Unit price (including VAT)$q: Quantity$discount: Optional discount amount
Attach additional JSON data to the invoice.
$data: Array or object to be stored as JSON
Generate, sign, and save the invoice. Returns the QR code URL.
$territory: Territory code:'ARABA','BIZKAIA', or'GIPUZKOA'(or numeric codes'01','02','03')$description: Invoice description
Returns: string - QR code URL
Get the Eloquent model instance for the saved invoice.
Returns: Invoice
Get the underlying TicketBAI object from the barnetik/ticketbai package.
Returns: \Barnetik\Tbai\TicketBai
use EBethus\LaravelTicketBAI\TicketBAI;
$ticketbai = app('ticketbai');
// Configure issuer
$ticketbai->issuer(
nif: 'B12345678',
name: 'My Company S.L.',
idIssuer: 1
);
// Set VAT
$ticketbai->setVat(21);
// Add items
$ticketbai->add('Product A', 50.00, 2, 0);
$ticketbai->add('Product B', 30.00, 1, 5.00);
// Add extra data
$ticketbai->data([
'order_id' => 12345,
'customer_email' => 'customer@example.com'
]);
// Generate invoice
$qrUrl = $ticketbai->invoice(
territory: 'BIZKAIA',
description: 'Order #12345'
);
echo "QR Code: $qrUrl";// In config/ticketbai.php or .env
// TICKETBAI_TABLE_NAME=my_invoices
// TICKETBAI_COLUMN_ISSUER=user_id
// TICKETBAI_COLUMN_NUMBER=invoice_ref
// TICKETBAI_COLUMN_SIGNATURE= (empty, disabled)
// TICKETBAI_COLUMN_DATA= (empty, disabled)
$ticketbai = app('ticketbai');
$ticketbai->issuer('B12345678', 'Company', 1);
$ticketbai->setVat(21);
$ticketbai->add('Item', 100, 1);
$qrUrl = $ticketbai->invoice('BIZKAIA', 'Invoice');$ticketbai = app('ticketbai');
// ... configure and generate invoice ...
$model = $ticketbai->getModel();
echo $model->provider_reference; // Invoice number (default column name)
echo $model->path; // XML file pathRun the test suite:
composer testOr with PHPUnit directly:
./vendor/bin/phpunitOptional: run static analysis (PHPStan) and code style (Laravel Pint):
composer analyse # PHPStan
composer format # Pint (fixes style)Invoice submission is asynchronous: after generating and signing an invoice, the package dispatches an InvoiceSend job to the Laravel queue. You must have at least one queue worker running for invoices to be sent to the TicketBAI API:
php artisan queue:work- Production: Use a process manager (e.g. Supervisor) to keep
queue:workrunning. - Testing / sync: If you use
QUEUE_CONNECTION=sync, jobs run immediately in the same process (no worker needed, but slower and no retries).
The InvoiceSend job submits the invoice to the TicketBAI API and updates the sent timestamp on success.
Invoices that were not sent (e.g. API error or worker down) have sent = null. To re-queue them for sending:
# List and resend all pending invoices
php artisan ticketbai:resend --all
# Resend a single invoice by ID
php artisan ticketbai:resend --id=123
# Dry run: only list what would be resent
php artisan ticketbai:resend --all --dry-runResend requires territory and path: territory is read from data[ticketbai_data_key] (default data->ticketbai), path from the path column.
- Ensure
storage/certificado.p12exists and is readable - Verify the certificate password is correct
- Check file permissions
- Verify column mappings in
config/ticketbai.php - Ensure required columns exist in your table
- Set optional columns to
nullif not needed
- Ensure queue worker is running
- Check queue connection configuration
- Review failed jobs:
php artisan queue:failed
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This package is open-sourced software licensed under the MIT license.
- Built on top of barnetik/ticketbai
- Developed by EBethus
For issues and feature requests, please use the GitHub issue tracker.