xavicabot / laravel-holded
PHP client for the Holded API, ready for Laravel
v3.0
2026-03-17 17:10 UTC
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.34|^3.8
- phpunit/phpunit: ^10.5|^11.5
README
PHP client for the Holded API, designed for Laravel 11 and 12.
Requirements
| Laravel | PHP |
|---|---|
| 11.x | ^8.2 |
| 12.x | ^8.2 |
Installation
composer require xavicabot/laravel-holded
Publish the configuration file:
php artisan vendor:publish --tag=config --provider="XaviCabot\Laravel\Holded\HoldedServiceProvider"
Add to your .env:
HOLDED_API=your_api_key
Usage
use XaviCabot\Laravel\Holded\Facades\Holded;
Contacts
$contacts = Holded::contact()->list(); $contact = Holded::contact()->get('contactId'); $new = Holded::contact()->create(['name' => 'Acme Inc']); Holded::contact()->update('contactId', ['name' => 'Acme Corp']); Holded::contact()->delete('contactId');
You can also use the ContactData DTO for validation and normalization:
use XaviCabot\Laravel\Holded\DTOs\ContactData; $data = new ContactData( name: 'Acme Inc', email: 'info@acme.com', type: 'client', countryCode: 'ES', ); Holded::contact()->create($data);
Documents
use XaviCabot\Laravel\Holded\Enums\DocumentType; $invoices = Holded::document()->list(page: 1, docType: DocumentType::INVOICE); $invoice = Holded::document()->get('docId', DocumentType::INVOICE); Holded::document()->delete('docId', DocumentType::INVOICE);
Create a document using the DocumentData DTO:
use XaviCabot\Laravel\Holded\DTOs\DocumentData; $data = new DocumentData( date: new DateTime(), contactName: 'Acme Inc', items: [ ['description' => 'Consulting', 'quantity' => 10, 'price' => 75.00], ], ); Holded::document()->create($data, DocumentType::INVOICE);
Available document types: INVOICE, SALES_RECEIPT, CREDIT_NOTE, SALES_ORDER, PROFORM, WAYBILL, ESTIMATE, PURCHASE, PURCHASE_ORDER, PURCHASE_REFUND.
Attach files
Holded::document()->attach('docId', '/path/to/file.pdf', setMain: true, docType: DocumentType::PURCHASE);
Get document PDF
$response = Holded::document()->pdf('docId', DocumentType::INVOICE); $pdfContent = base64_decode($response['data']);
Multi-account
Define accounts in config/holded.php:
'accounts' => [ 'main' => [ 'api_key' => env('HOLDED_API_MAIN'), 'base_url' => env('HOLDED_API_URL_MAIN', 'https://api.holded.com/api/invoicing/v1/'), ], ],
Then use them:
Holded::account('main')->contact()->list();
Or set credentials at runtime:
Holded::withCredentials('API_KEY', 'https://api.holded.com/api/invoicing/v1/') ->contact() ->list();
Error handling
All API errors throw a HoldedException with access to status code and response body:
use XaviCabot\Laravel\Holded\Exceptions\HoldedException; try { Holded::contact()->get('invalid-id'); } catch (HoldedException $e) { $e->getStatusCode(); // 404 $e->getResponseBody(); // ['message' => '...'] $e->isRateLimited(); // true on 429 $e->isServerError(); // true on 5xx $e->isClientError(); // true on 4xx }
Rate-limited (429) and server error (5xx) requests are automatically retried with backoff. Configure in config/holded.php:
'retry' => [ 'max_retries' => 3, 'retry_delay' => 1000, // milliseconds 'retry_on_server_error' => true, ],
Testing
composer test
License
MIT