madtec / omnileads-sdk
PHP SDK for OmniLeads API with Laravel integration
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.8
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- psr/log: ^3.0
Requires (Dev)
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.34|^3.0
- phpstan/phpstan: ^1.11
This package is auto-updated.
Last update: 2026-04-28 08:33:01 UTC
README
Production-ready PHP SDK for the OmniLeads API. Ships with first-class Laravel integration (ServiceProvider, Facade, config publishing) but the client itself is framework-agnostic and works in any PHP 8.2+ project.
Requirements
- PHP 8.2+
- Laravel 10.x / 11.x / 12.x (optional — only required when using the Laravel integration)
- Guzzle HTTP 7.8+
Installation
composer require madtec/omnileads-sdk
Publish the Laravel config (optional but recommended):
php artisan vendor:publish --tag=omnileads-config
Set the environment variables in .env:
OMNILEADS_BASE_URL=https://api.madtec.ru/api OMNILEADS_API_KEY=your-api-key-here OMNILEADS_JWT=your-jwt-here # only for /Auth/me/webhook-settings OMNILEADS_TIMEOUT=30 OMNILEADS_RETRY_ENABLED=true OMNILEADS_RETRY_TIMES=3 OMNILEADS_RETRY_SLEEP_MS=200 OMNILEADS_LOG_CHANNEL=stack # optional: PSR-3 channel for request logs
Quick start
1. Create a project
use Madtec\OmniLeads\DTO\Requests\CreateProjectRequest; use Madtec\OmniLeads\Enums\ProjectState; use Madtec\OmniLeads\Facades\OmniLeads; $result = OmniLeads::projects()->create(new CreateProjectRequest( name: 'CRM import', limit: 1000, limitTypeId: 1, description: 'Imported daily', state: ProjectState::ACTIVE, geoRegions: [77, 78], )); $projectId = $result->id;
2. Add a source
use Madtec\OmniLeads\DTO\Requests\CreateSourceRequest; use Madtec\OmniLeads\Enums\SourceData; use Madtec\OmniLeads\Enums\SourceType; use Madtec\OmniLeads\Facades\OmniLeads; OmniLeads::sources()->add($projectId, new CreateSourceRequest( sourceTypeId: SourceType::DOMAIN, sourceDataId: SourceData::MEGAFON, sourceValues: ['example.com'], limit: 500, ));
3. Fetch data for a day
use Madtec\OmniLeads\Facades\OmniLeads; // All projects, no pagination $data = OmniLeads::data()->byDay('2026-04-08'); // Or stream through paginated pages foreach (OmniLeads::data()->iterateByDay('2026-04-08', pageSize: 1000) as $project) { foreach ($project->segments as $segment) { foreach ($segment->phones as $phone) { // process $phone->phone, $phone->createdAt } } }
4. Sync projects in bulk
use Madtec\OmniLeads\DTO\Requests\SyncProjectDTO; use Madtec\OmniLeads\DTO\Requests\SyncProjectsRequest; use Madtec\OmniLeads\DTO\Requests\SyncSegmentDTO; use Madtec\OmniLeads\Enums\SourceData; use Madtec\OmniLeads\Enums\SourceType; use Madtec\OmniLeads\Facades\OmniLeads; $result = OmniLeads::sync()->push(new SyncProjectsRequest([ new SyncProjectDTO( name: 'Daily import', limit: 5000, limitType: 'Дневной', segments: [ SyncSegmentDTO::fromEnums(SourceType::DOMAIN, SourceData::MEGAFON, ['a.com'], 500), SyncSegmentDTO::fromEnums(SourceType::PHONE, SourceData::MTS, ['79001234567']), ], ), ])); if (! $result->success) { foreach ($result->errors as $error) { report(new \RuntimeException($error)); } }
5. Receive webhook notifications
See Webhook handler below.
Available methods
use Madtec\OmniLeads\Facades\OmniLeads; // Projects OmniLeads::projects()->list(); OmniLeads::projects()->get($projectId); OmniLeads::projects()->create($createProjectRequest); OmniLeads::projects()->update($projectId, $updateProjectRequest); OmniLeads::projects()->changeState($projectId, 'paused'); OmniLeads::projects()->activate($projectId); // shortcut OmniLeads::projects()->pause($projectId); // shortcut // Sources OmniLeads::sources()->add($projectId, $createSourceRequest); OmniLeads::sources()->update($projectId, $sourceId, $updateSourceRequest); OmniLeads::sources()->delete($projectId, $sourceId); // Data OmniLeads::data()->byDay('2026-04-08'); OmniLeads::data()->byDayPaged('2026-04-08', page: 1, pageSize: 1000); OmniLeads::data()->iterateByDay('2026-04-08'); // Reference data OmniLeads::limitTypes()->all(); OmniLeads::geoRegions()->all(); // remote API OmniLeads::geoRegions()->builtin(); // hardcoded list, no HTTP // Webhook settings (Bearer JWT auth) OmniLeads::auth()->getWebhookSettings(); OmniLeads::auth()->updateWebhookSettings($webhookSettingsRequest); // Sync OmniLeads::sync()->push($syncProjectsRequest); OmniLeads::sync()->pull();
Built-in enums
use Madtec\OmniLeads\Enums\DayOfWeek; use Madtec\OmniLeads\Enums\GeoRegions; use Madtec\OmniLeads\Enums\ProjectState; use Madtec\OmniLeads\Enums\SourceData; use Madtec\OmniLeads\Enums\SourceType; SourceType::DOMAIN->value; // 3 SourceData::MEGAFON->value; // 8 ProjectState::ACTIVE; // active|paused DayOfWeek::MONDAY->value; // 1 GeoRegions::name(77); // "Москва" GeoRegions::all(); // [0 => "Все регионы", 1 => "Адыгея", ...]
Error handling
Every error raised by the SDK extends
Madtec\OmniLeads\Exceptions\OmniLeadsException. The HTTP-derived ones extend
ApiException:
use Madtec\OmniLeads\Exceptions\ApiException; use Madtec\OmniLeads\Exceptions\AuthenticationException; use Madtec\OmniLeads\Exceptions\ConfigurationException; use Madtec\OmniLeads\Exceptions\NotFoundException; use Madtec\OmniLeads\Exceptions\ServerException; use Madtec\OmniLeads\Exceptions\ValidationException; use Madtec\OmniLeads\Facades\OmniLeads; try { $project = OmniLeads::projects()->get($id); } catch (ConfigurationException $e) { // Local pre-flight validation failed (bad GUID, missing JWT, ...) } catch (ValidationException $e) { // 400 Bad Request — $e->errorMessage holds the API message } catch (AuthenticationException $e) { // 401/403 } catch (NotFoundException $e) { // 404 } catch (ServerException $e) { // 5xx — already retried per `OMNILEADS_RETRY_*` } catch (ApiException $e) { // any other HTTP error or network failure ($e->statusCode === 0) }
ApiException exposes statusCode, responseBody, requestMethod, requestUri,
errorMessage, and requestId (when present in the body).
Retry behaviour
Retries are enabled by default for 5xx responses and network failures
(connection refused, DNS timeouts, etc.). Backoff is exponential:
sleep_ms * 2^(attempt - 1). 4xx responses are never retried because they signal a
client-side error.
Disable retries entirely:
OMNILEADS_RETRY_ENABLED=false
Or per-request, by constructing a fresh OmniLeadsClient with
RetryConfig::disabled().
Known API quirks
Documented here so you do not have to rediscover them. Each item was confirmed by running the SDK against the production OmniLeads API.
GET /Project is paginated
The published spec lists the response as a flat array, but the endpoint actually returns a paginated wrapper:
{
"items": [...],
"page": 1,
"pageSize": 10,
"totalCount": N,
"totalPages": N,
"hasPreviousPage": false,
"hasNextPage": false
}
OmniLeads::projects()->list() unwraps items for you. Use listPaged() for
explicit pagination control or iterateAll() to walk every page.
Source limit is read-only when project limitTypeId = 1 (Эффективный)
If the project uses the balanced-between-sources limit type, the API
auto-distributes the project's total limit across its sources. Trying to
PUT /Project/{id}/sources/{sourceId} with a limit in the body returns:
HTTP 400
{ "message": "Редактирование лимита источника запрещено для выбранного типа лимита проекта" }
The SDK surfaces this as a ValidationException with the API message in
$e->errorMessage. To control source limits manually, switch the project to
limitTypeId = 3 (Ручной) first via PUT /Project/{id}.
There is no DELETE /Project/{id}
The API responds with HTTP 405 Method Not Allowed. To soft-delete a project:
use Madtec\OmniLeads\Facades\OmniLeads; use Madtec\OmniLeads\DTO\Requests\UpdateProjectRequest; $projectId = '...'; // 1. drop every source $project = OmniLeads::projects()->get($projectId); foreach ($project->projectSources as $src) { OmniLeads::sources()->delete($projectId, $src->id); } // 2. pause it OmniLeads::projects()->pause($projectId); // 3. (optional) flag it visually OmniLeads::projects()->update($projectId, new UpdateProjectRequest( name: '[ARCHIVED] '.$project->name, ));
POST /Project/sync is currently unstable
At the time of writing, the sync endpoint returns HTTP 500 {"message":"Внутренняя ошибка сервера"} for every payload — even the exact
example from the official documentation. The SDK serializes the payload
correctly; you will get a ServerException until OmniLeads ships a fix.
Track this with the platform's support if you depend on the sync flow.
PUT /Project/{id} returns the project but skips a few optional fields
workingDaysOfWeek, workingDates, excludedDates, and description come back
as null in the immediate PUT response, even when the change was applied.
A subsequent GET /Project/{id} returns them correctly. The SDK exposes
whatever the server sends — if you need the canonical state right after an
update, re-fetch with OmniLeads::projects()->get($id).
Source ids are inconsistently typed
POST /Project/{id}/sources returns id as a string, while
PUT /Project/{id}/sources/{sourceId} returns it as a number. The SDK
normalizes both into string $id on the response DTO, so consumer code does
not have to care.
Webhook phone is a JSON number
Each phone in import.completed arrives as "phone": 79001234567 (numeric, not
quoted). The SDK casts it to string on DayPhone::$phone so you keep
arbitrary-precision values without losing leading-zero edge cases.
Webhook handler
The OmniLeads platform pushes an import.completed event after each finished import.
Below is a complete example of how to receive it in Laravel.
Route
// routes/web.php use App\Http\Controllers\OmniLeadsWebhookController; Route::post( '/webhooks/omnileads/{secret}', OmniLeadsWebhookController::class, )->where('secret', '[a-f0-9]{32}')->name('webhooks.omnileads');
The OmniLeads API does not currently sign webhook payloads. Protect the endpoint with a shared secret embedded in the URL (as above) and/or an IP allow list. See SECURITY.md.
Controller
// app/Http/Controllers/OmniLeadsWebhookController.php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Madtec\OmniLeads\Exceptions\ValidationException; use Madtec\OmniLeads\Webhook\ImportCompletedReceived; use Madtec\OmniLeads\Webhook\WebhookPayloadParser; final class OmniLeadsWebhookController { public function __construct( private readonly WebhookPayloadParser $parser, ) {} public function __invoke(Request $request, string $secret): JsonResponse { if (! hash_equals(config('services.omnileads.webhook_secret', ''), $secret)) { return response()->json(['error' => 'invalid secret'], 403); } try { $event = $this->parser->parse($request->getContent()); } catch (ValidationException $e) { return response()->json(['error' => $e->errorMessage], 400); } ImportCompletedReceived::dispatch($event); return response()->json(['ok' => true]); } }
Listener
// app/Listeners/ProcessOmniLeadsImport.php namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use Madtec\OmniLeads\Webhook\ImportCompletedReceived; final class ProcessOmniLeadsImport implements ShouldQueue { public function handle(ImportCompletedReceived $eventEnvelope): void { $event = $eventEnvelope->event; foreach ($event->projects as $project) { foreach ($project->segments as $segment) { foreach ($segment->phones as $phone) { // store $phone->phone, $phone->createdAt } } } } }
Register it in EventServiceProvider:
protected $listen = [ \Madtec\OmniLeads\Webhook\ImportCompletedReceived::class => [ \App\Listeners\ProcessOmniLeadsImport::class, ], ];
Testing your application
The Facade exposes a fake() helper plus a swap() method on the manager so you can
inject a custom client in tests:
use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use Madtec\OmniLeads\Config\OmniLeadsConfig; use Madtec\OmniLeads\Config\RetryConfig; use Madtec\OmniLeads\Facades\OmniLeads; use Madtec\OmniLeads\OmniLeadsClient; $mock = new MockHandler([ new Response(200, [], json_encode([])), ]); $guzzle = new GuzzleClient([ 'base_uri' => 'https://api.test/api/', 'handler' => HandlerStack::create($mock), ]); OmniLeads::fake(new OmniLeadsClient( config: new OmniLeadsConfig( baseUrl: 'https://api.test/api', apiKey: 'test', jwt: null, timeout: 5, retry: RetryConfig::disabled(), ), guzzle: $guzzle, )); $result = OmniLeads::projects()->list(); // returns []
Without Laravel
The package works in any PHP project — Laravel only adds sugar:
use Madtec\OmniLeads\Config\OmniLeadsConfig; use Madtec\OmniLeads\Config\RetryConfig; use Madtec\OmniLeads\OmniLeadsClient; $client = new OmniLeadsClient( config: new OmniLeadsConfig( baseUrl: 'https://api.madtec.ru/api', apiKey: getenv('OMNILEADS_API_KEY') ?: null, jwt: getenv('OMNILEADS_JWT') ?: null, timeout: 30, retry: new RetryConfig(enabled: true, times: 3, sleepMs: 200), ), ); $projects = $client->projects()->list();
Changelog
See CHANGELOG.md for the full release history.
Credits
Contributions are welcome — see CONTRIBUTING.md. A list of contributors is generated automatically by contrib.rocks.
Security
If you discover a security vulnerability, please follow the disclosure process in SECURITY.md. Do not open a public GitHub issue.
License
Released under the MIT License — see LICENSE for the full text.