devio/pipedrive
A Laravel/PHP client for the Pipedrive CRM API, offering an expressive, framework-friendly way to work with deals, leads, persons, organizations, activities, and other Pipedrive resources.
Install (pin 2.17.0+ if you're on PHP 8.3/8.4):
composer require devio/pipedrive
2.17.0 only clears deprecation warnings — no API changes — but it's the release you want if your logs are full of E_DEPRECATED noise from the package's signatures. Anything below it still works; it just complains on modern PHP.
Laravel (5.5+ auto-discovers the provider/facade). Publish config:
php artisan vendor:publish --provider="Devio\Pipedrive\PipedriveServiceProvider"
PIPEDRIVE_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Standalone PHP:
use Devio\Pipedrive\Pipedrive;
$pipedrive = Pipedrive::make(env('PIPEDRIVE_TOKEN'));
First call — the 30-second validation loop:
$response = $pipedrive->deals->all(['status' => 'open', 'limit' => 10]);
if ($response->isSuccess()) {
foreach ($response->getData() as $deal) {
echo $deal->id . ' — ' . $deal->title . PHP_EOL;
}
}
Where to look first (reading the source beats reading the README):
src/Pipedrive.php — the factory and the list of supported resources (deals, persons, organizations, leads, activities, notes, products, pipelines, stages, users, files, webhooks, filters, *Fields, goals, roles, activityTypes). If a resource is listed here, the rest is trivial.src/Resources/Resource.php — every resource is a thin wrapper. all(), find($id), add($data), update($id, $data), delete($id) are the contract; anything past that (search, deals->products, deals->followers) is resource-specific.src/Http/Response.php (or PipedriveResponse) — how to read getData(), getStatusCode(), and the raw additional_data block. You will use this class more than any other.src/Resources/*.php — skim the few you actually need. There are no hidden abstractions; each method is ~5 lines mapping to a URL.First real use case: a lead-intake job that creates a Person, an Organization, then a Deal linked via person_id/org_id, plus a Note. That sequence exercises the four resources you'll touch 90% of the time.
Resource-as-property (or facade method) style. Resources are resolved lazily through __get, so $pipedrive->deals->add([...]) and Pipedrive::deals()->add([...]) are equivalent. Wrap the client once and inject it — don't call Pipedrive::make() in controllers.
Service layer / Laravel container binding:
// AppServiceProvider
$this->app->singleton(\Devio\Pipedrive\Pipedrive::class, fn () =>
\Devio\Pipedrive\Pipedrive::make(config('pipedrive.token'))
);
Then type-hint Pipedrive in a CrmService and mock it in tests.
Create-or-update via find + update. There is no upsert. The common pattern:
$existing = $pipedrive->persons->find($id);
$pipedrive->persons->update($id, ['email' => $email]);
// vs
$pipedrive->persons->add(['name' => $name, 'email' => $email]);
Wrap it in your own repository method so idempotent syncs are possible — the API otherwise happily creates duplicates.
Custom fields. Pipedrive stores custom data under hashed keys (abc123..._value). Fetch the mapping once and cache it:
$map = collect($pipedrive->dealFields->all()->getData())
->keyBy('name') // or 'field_code'
->map->key; // the hash you send
$pipedrive->deals->add([
$map['Lead Source'] => 'Website',
]);
Cache $map for minutes, not per-request — it's an extra API call every time.
Manual pagination. all() returns one page. Loop using additional_data.pagination:
$start = 0;
do {
$res = $pipedrive->deals->all(['start' => $start, 'limit' => 100]);
$deals = array_merge($deals, $res->getData() ?? []);
$pagination = $res->getData()['additional_data']['pagination'] ?? null;
$start = $pagination['next_start'] ?? null;
} while ($pagination && ($pagination['more_items_in_collection'] ?? false));
(Adjust: on this package getData() typically returns only the resource array; the wrapper exposes the full payload via its getContent()/additional-data accessor — check src/Http/Response.php in your pinned version.)
Webhook receiver as the integration point. Register with $pipedrive->webhooks->add(['subscription_url' => ..., 'event_action' => '*', 'event_object' => 'deal']), and in your controller always return response('', 200) before queuing work; Pipedrive retries on non-2xx.
Queue everything outbound. A CreateDealJob per write, and a scheduled SyncDealsCommand per read. This package has no retry/backoff, so jobs are your rate-limit strategy.
Batch writes. No bulk endpoint in the wrapper. Chunk your payloads (e.g., collect($rows)->chunk(50)) and usleep(250_000) between calls to stay under the 80-requests-per-2-seconds ceiling.
composer update and nothing else. If you'd been filtering E_DEPRECATED for this package in phpunit.xml, a custom error handler, or error_reporting(), you can drop that workaround now — and if you were suppressing deprecations globally to hide this package's noise, 2.17.0 lets you tighten error reporting again and catch your own deprecations.Retry-style job wrapper if you scrape or bulk-import.isSuccess() is mandatory. getData() returns null on a 4xx/5xx rather than throwing at the resource layer. Any foreach ($res->getData() as ...) without a success check will emit a warning or throw on null./deals/search, /persons/search) are deprecated in favor of /items/search and the v2 search APIs, which this client does not wrap. Test any search() call against your account before relying on it.api.pipedrive.com; some accounts/regions need a custom base (Pipedrive::make($token, $baseUrl)). If you get cryptic 404s on every call, check the base URL and token — a revoked token returns 401 with a valid-looking JSON body.Pipedrive::make() per request or use a different SDK.files->upload() is multipart. It does not go through the JSON path; it expects a local file path/CURLFile-style structure. Don't pass a URL and expect it to fetch.delete returns a boolean-ish payload, not the deleted entity — don't try to ->getData()->id afterward.*Fields at startup.all() default limits. Without ['limit' => 100] you'll silently under-read on large accounts, and without pagination you'll silently drop the tail.ConnectException bubbles up. Catch \GuzzleHttp\Exception\GuzzleException at the service-layer boundary and translate to your own domain exception.Pipedrive::make() accepts a token and the resource layer only uses Guzzle, you can point it at a local mock server (e.g., https://localhost:8080) or bind a fake client. There's no built-in HTTP fake — build one in your test base class rather than mocking every resource individually.$pipedrive->getClient()->request(...) if that accessor is exposed in your pinned version. New Pipedrive endpoints appear faster than this repo's release cadence (2.17.0 is a maintenance release, so don't expect v2 API coverage here).How can I help you explore Laravel packages today?