laravel-livespace maintained by beziworld
Laravel Livespace
Laravel client for the Livespace CRM API (https://SUBDOMAIN.livespace.io/api/public/json/Module/method):
- the documented authorization flow: a one-time token from
getTokenfor every call, then_api_sha = sha1(API_KEY . TOKEN . API_SECRET)with the session ID, exactly as the official SDK sends it - persons and companies: get, list, attribute search, lazy iteration, add (with duplicate check and wall switches), edit, delete, merge, tags, company assignment, marketing consents, tasks, wall notes and phone calls; company lookup by NIP in any format
- deals: get, search, lazy iteration, add with price and cost in the account's decimal format, edit, status (open, won, lost, outdated), process stages, tags, tasks, notes
- tasks (Todo): get, filters by completion, type, period and linked objects, tasks of an object, add, edit, complete, delete
- wall notes: list with filters, add, edit, delete
- files: attach a file by URL (with optional HTTP credentials) to persons, companies, deals and spaces
- users: current user, all users, add, invite, lock, unlock, remove
- custom fields (datasets) per object type with select options
- phrase search across contacts, companies and deals
- web form submissions turned into a person, a company and a follow-up task in one call
- several Livespace accounts (named connections per subdomain and runtime credentials per tenant)
- typed, immutable data objects that keep the raw response; money as decimal strings, dates as
CarbonImmutableinEurope/Warsaw - Livespace result codes (420 validation, 540 no permission, 562 incorrect key, ...) turned into typed exceptions; an unknown subdomain is reported as such; reads retried on 5xx, connection errors and HTTP 429, writes never
All requests go through the Laravel HTTP client, so everything works with Http::fake().
Why this package
Packagist (November 2026) lists the official livespace/livespace-php SDK (23k downloads, PHP 5.3 era, cURL with
certificate verification turned off, no tests) and indahash/livespace (2016), and no Laravel integration. This
package is a tested, Laravel-native client built on the official API guide (English version 1.13 of 13 May 2024 and
Polish version 1.12) and on the wire format of the official SDK.
Installation
composer require beziworld/laravel-livespace
php artisan vendor:publish --tag=livespace-config # optional
Generate the keys in Livespace under Settings > API. They belong to one user: every call runs with that user's permissions.
LIVESPACE_SUBDOMAIN=acme # https://acme.livespace.io (a full URL works too)
LIVESPACE_API_KEY=your-api-key
LIVESPACE_API_SECRET=your-api-secret
Other settings: LIVESPACE_TIMEOUT (30 s), LIVESPACE_CONNECT_TIMEOUT (10 s), LIVESPACE_RETRIES (2, reads only),
LIVESPACE_RETRY_DELAY (1000 ms, doubled per attempt), LIVESPACE_MAX_RETRY_WAIT (60 s), LIVESPACE_PAGE_SIZE
(500), LIVESPACE_DECIMAL_SEPARATOR (,, as in the API guide examples) and LIVESPACE_TIMEZONE (Europe/Warsaw).
php artisan livespace:check # default: OK (https://acme.livespace.io, API user Jan Kowalski).
php artisan livespace:check --all
Turn on "Show API identifiers on the object profiles" in Settings > API to see the IDs of contacts, deals, custom fields and sales process stages.
Persons and companies
use Beziworld\Livespace\Facades\Livespace;
$person = Livespace::persons()->create([
'firstname' => 'Jan',
'lastname' => 'Kowalski',
'emails' => ['jan@firma.pl'],
'phones' => [['phone_no' => '600100200', 'type' => 2, 'is_default' => 1]], // 1 phone, 2 mobile, 3 fax
'company' => ['name' => 'Firma Sp. z o.o.', 'nip' => '111-222-55-00'],
'dataset' => ['87de5630-d889-bdb1-bd09-d61335d27ff8' => 'value'], // custom fields
], checkIfExists: true, wall: false);
$person = Livespace::persons()->get($person->id);
$person->email();
Livespace::persons()->findByEmail('jan@firma.pl');
Livespace::persons()->search(['tags' => ['vip'], 'modified' => ['from' => now()->subDay()]], limit: 100);
foreach (Livespace::persons()->lazy() as $person) { /* every person, page by page */ }
Livespace::persons()->update($person->id, ['position' => 'CEO']);
Livespace::persons()->tag($person->id, add: ['newsletter'], remove: ['lead']);
Livespace::persons()->assignCompany($person->id, 'Firma Sp. z o.o.'); // ID or name; null removes
Livespace::persons()->setConsents($person->id, [['id' => $consentId, 'status_id' => $statusId]]);
Livespace::persons()->addCall($person->id, '+48600100200', outbound: true, durationSeconds: 231);
Livespace::persons()->merge($person->id, [$duplicateId]);
$company = Livespace::companies()->findByNip('PL 111-222-55-00');
Livespace::companies()->create(['name' => 'Bank XYZ', 'client_status' => 'current_client']);
Livespace::companies()->setClientStatus($company->id, 'former_client');
Deals
$deal = Livespace::deals()->create([
'name' => 'Website redesign',
'process_id' => $processId,
'company' => ['id' => $company->id],
'contact' => ['id' => $person->id],
'price' => '12500.00', // sent as "12500,00" with the default separator
'date_end' => now()->addMonth(),
]);
Livespace::deals()->search(['status' => ['open'], 'owner_login' => 'jan@firma.pl']);
Livespace::deals()->openForCompany($company->id);
Livespace::deals()->markStages($deal->id, [$stageId, $otherStageId => false]);
Livespace::deals()->markWon($deal->id);
$deal->price; // "12500.00"
Tasks, notes and files
Livespace::tasks()->create([
'title' => 'Call back',
'date' => now()->addDays(2)->setTime(10, 0),
'priority' => 1,
'objects' => [['type' => 'deal', 'id' => $deal->id]],
]);
Livespace::tasks()->list(completed: false, from: today(), to: today()->addWeek());
Livespace::tasks()->forObject('company', $company->id);
Livespace::tasks()->complete($taskId);
Livespace::deals()->addTask($deal->id, 'Send the offer', today()->addDay());
Livespace::notes()->add('Offer sent', 'deal', $deal->id, tags: ['offer']);
Livespace::notes()->list('deal', $deal->id, types: ['note', 'email']);
Livespace::files()->attachToDeal($deal->id, URL::temporarySignedRoute('offers.pdf', now()->addHour(), $offer), 'Offer');
Livespace documents only one way to add files: it downloads the file from a URL given with the edit call. A signed temporary route of your application works well. There is no documented upload of file contents or file download.
Users, custom fields and search
Livespace::users()->me();
Livespace::users()->all();
Livespace::users()->invite(['new@firma.pl'], [['structure_id' => [$teamId], 'role_id' => [$roleId], 'is_main' => 1]]);
Livespace::users()->lock('old@firma.pl');
$industry = Livespace::fields()->find('company', 'Industry');
Livespace::companies()->update($company->id, ['dataset' => [$industry->id => $industry->optionId('IT')]]);
Livespace::search()->query('contact', 'jan kowalski');
Web forms
use Beziworld\Livespace\Data\FormSubmission;
// From validated request input (name or first_name/last_name, email, phone, company, nip, message, city, ...)
Livespace::forms()->submit($request->validated(), source: 'contact form');
// Or explicitly
Livespace::forms()->submit(new FormSubmission(
name: 'Jan Kowalski',
email: 'jan@firma.pl',
companyName: 'Firma Sp. z o.o.',
message: 'Please send an offer',
tags: ['website'],
taskTitle: 'Contact the lead',
taskDate: today()->addDays(3),
));
Livespace has no form endpoint; the API guide (chapter 17) shows exactly this pattern: one Contact/addContact call
that creates the person, the company and a follow-up task. Unlike the guide example, duplicate checking stays on so a
returning visitor does not create a second contact (checkIfExists: false turns it off).
Several accounts
Livespace::connection('second-company')->deals()->search(['status' => 'open']);
Livespace::using([
'subdomain' => $tenant->livespace_subdomain,
'api_key' => $tenant->livespace_key,
'api_secret' => $tenant->livespace_secret,
])->persons()->create([...]);
Anything else
Every method of the API guide is available through call(), for example spaces, products and cost categories:
Livespace::call('Space/getAll', ['type' => 'space'], read: true);
Livespace::call('Deal/product_add', ['name' => 'Consulting', 'sku' => 'C-1', 'default_price' => 150]);
Livespace::response('Default/ping', ['param1' => 'value1']); // status, result, data, error
Notes on the API
- A new token is requested for every call, as the official SDK does; the guide does not say whether a token can be reused.
- The guide documents request parameters in detail but not the structure of read responses. The data objects map the
documented field names and keep the complete record in
raw. - No rate limits are documented. HTTP 429 is handled with
Retry-After.
Errors
| Exception | When |
|---|---|
Exceptions\ApiException |
"status": false: resultCode() (400 method error, 420 validation, 5xx API errors, 530 not logged in, 540 no permission, 560 to 564 authorization), error(), fieldErrors(), method(), body(); helpers isValidationError(), isAuthenticationError(), isPermissionError(), isUnknownAccount() (HTTP 404 for an unknown subdomain), isRetryable(). Also HTTP errors, unreadable responses and connection failures (isTransportError()) |
Exceptions\RateLimitExceeded |
HTTP 429 after the read retries, or on the first write; retryAfter() |
Exceptions\InvalidArgument |
Input violating a documented rule (missing names, unknown statuses or object types, more than 10 merged records, ...) |
Exceptions\InvalidConfiguration |
Missing subdomain or keys, unknown connection |
All extend Exceptions\LivespaceException.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration? Get in touch through beziworld.eu.
License
MIT. See LICENSE.md. This package is not affiliated with Livespace sp. z o.o.