PHPackages                             jcolombo/leadfeeder-api-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. jcolombo/leadfeeder-api-php

ActiveLibrary[HTTP &amp; Networking](/categories/http)

jcolombo/leadfeeder-api-php
===========================

PHP SDK for the Leadfeeder website visitor tracking and lead generation API

v0.1.0-alpha(2mo ago)00MITPHPPHP &gt;=8.1CI passing

Since Apr 8Pushed 2mo agoCompare

[ Source](https://github.com/jcolombo/leadfeeder-api-php)[ Packagist](https://packagist.org/packages/jcolombo/leadfeeder-api-php)[ Docs](https://github.com/jcolombo/leadfeeder-api-php)[ RSS](/packages/jcolombo-leadfeeder-api-php/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (2)Versions (2)Used By (0)

Leadfeeder API for PHP
======================

[](#leadfeeder-api-for-php)

[![Latest Version](https://camo.githubusercontent.com/14d497cf965dd896b1c083ac167ac767bdd9f223d423dbdcd5d178047ab261a3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a636f6c6f6d626f2f6c6561646665656465722d6170692d7068702e737667)](https://packagist.org/packages/jcolombo/leadfeeder-api-php)[![PHP Version](https://camo.githubusercontent.com/40bb62030f40a3b16426759d5b8da7e6067bdf60d0dec51d39bb85a50a66f8bb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6a636f6c6f6d626f2f6c6561646665656465722d6170692d7068702e737667)](https://packagist.org/packages/jcolombo/leadfeeder-api-php)[![License](https://camo.githubusercontent.com/9953da4419035f262f1ee2c8bb3fa751355071dc660c35d56f77c7d4813715cd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6a636f6c6f6d626f2f6c6561646665656465722d6170692d706870)](LICENSE)[![GitHub Issues](https://camo.githubusercontent.com/15dce086b979830a20adb507b9e6a4a583ec12490bbc286fa8016fc7c156c3b7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732f6a636f6c6f6d626f2f6c6561646665656465722d6170692d706870)](https://github.com/jcolombo/leadfeeder-api-php/issues)

---

Overview
--------

[](#overview)

This independently developed package provides a developer-friendly PHP toolkit for interacting with the Leadfeeder API. It is not affiliated with or endorsed by Leadfeeder / Dealfront.

**Leadfeeder Homepage:** **API Documentation:**

> **Stability Notice:** This package is in active development (v0.x-alpha). The API surface may change before v1.0. Pin to `^0.x` in production.

---

Features
--------

[](#features)

- **Read-Only Resource Access** — Fetch and list Accounts, Leads, Visits, Custom Feeds, and Website Tracking Scripts
- **Fluent Interface** — Chainable methods for clean, readable code
- **JSON:API Parsing** — Automatic envelope parsing with include resolution (1–2 levels)
- **Date Range Filtering** — Built-in `dateRange()` for temporal queries (required for Lead/Visit lists)
- **Smart Query Building** — Server-side WHERE filters and client-side HAS post-filters
- **Relationship Includes** — Resolve Location entities on Lead/Visit responses
- **Custom Feed Scoping** — Filter leads by custom feed with `forFeed()`
- **Lead-Scoped Visits** — Retrieve visits for a specific lead with `forLead()`
- **Export Manager** — Async create/poll/download lifecycle for bulk lead export
- **IP Enrichment** — Identify companies by IP address via the Leadfeeder Discover API
- **Multi-Scope Rate Limiting** — Four independent sliding windows (per-token, per-account, export, IP-Enrich)
- **Auto-Pagination** — `fetchAll()` iterates through all pages (10,000-lead cap for leads)
- **Response Caching** — Built-in file-based caching with custom backend support
- **Request Logging** — Conditional file-based logging for debugging
- **Type Coercion** — Automatic property type conversion with extended type system
- **Zero Dev Dependencies** — Custom test framework requires no PHPUnit or dev packages

---

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- A [Leadfeeder](https://www.leadfeeder.com) account with API access
- Your Leadfeeder API token (from **Settings → Personal → API Tokens**)
- [Composer](https://getcomposer.org)
- *(Optional)* A Leadfeeder Discover API key for IP enrichment

---

Installation
------------

[](#installation)

```
composer require jcolombo/leadfeeder-api-php
```

The package is published on [Packagist](https://packagist.org/packages/jcolombo/leadfeeder-api-php) and follows standard PSR-4 autoloading under the `Jcolombo\LeadfeederApiPhp` namespace. No additional configuration is required to get started — sensible defaults are loaded automatically from the bundled `default.leadfeederapi.config.json`.

---

Quick Start
-----------

[](#quick-start)

### Connecting and Selecting an Account

[](#connecting-and-selecting-an-account)

Authentication is Bearer token–based. Call `Leadfeeder::connect()` once with your token. The connection is cached as a singleton, so calling it again with the same token returns the same instance. Most API endpoints are scoped to a specific account ID, which you provide via `setAccount()`.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Account;

// Establish a connection using your Leadfeeder API token
$lf = Leadfeeder::connect('your-api-token-here');

// List all accounts accessible to this token
$accounts = Account::list($lf)->fetch();

foreach ($accounts as $account) {
    echo $account->name . ' (' . $account->id . ')' . PHP_EOL;
}

// Bind the connection to a specific account for subsequent requests
$lf->setAccount('your-account-id');
```

### Fetching Leads

[](#fetching-leads)

Lead list requests require a date range. The `dateRange()` method sets the `start_date` and `end_date` server-side filters in a single call. Without a date range the Leadfeeder API will return an error; in `devMode` the SDK emits a warning before the request is sent.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Fetch one page of leads for a date range
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->pageSize(25)
    ->fetch();

foreach ($leads as $lead) {
    echo $lead->name . ' — ' . $lead->website_url . PHP_EOL;
}

// Fetch a single lead by ID
$lead = Lead::new($lf)->fetch('abc123');
echo $lead->name . PHP_EOL;
```

### Fetching Visits

[](#fetching-visits)

Visit lists are also date-range-required. You can iterate over a collection directly with a `foreach` loop because collections implement the `Iterator` interface.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Visit;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$visits = Visit::list($lf)
    ->dateRange('2024-06-01', '2024-06-30')
    ->pageSize(50)
    ->fetch();

foreach ($visits as $visit) {
    echo $visit->started_at . ' — ' . $visit->source . PHP_EOL;
}
```

---

Supported Resources
-------------------

[](#supported-resources)

The SDK models six Leadfeeder API entities. Each resource class lives under `Jcolombo\LeadfeederApiPhp\Entity\Resource\`.

ResourceClassScopePatternNotesAccount`Account`Token`list()`, `fetch($id)`Lists all accounts for the tokenLead`Lead`Account`list()`, `fetch($id)`Date range required for listVisit`Visit`Account`list()`, `fetch($id)`Date range required for listCustomFeed`CustomFeed`Account`list()`, `fetch($id)`Read-only feed definitionsLocation`Location`Include-onlyResolved via includeCannot be fetched or listed directlyWebsiteTrackingScript`WebsiteTrackingScript`Account`fetch()` (singleton)`list()` throws `RuntimeException`Account requests are token-scoped (no account ID needed). All other requests are automatically prefixed with `accounts/{accountId}/` when an account has been set on the connection. The Location entity is resolved only as a relationship include on Lead and Visit responses — it has no standalone API endpoint.

---

Date Range Filtering
--------------------

[](#date-range-filtering)

The Leadfeeder API requires `start_date` and `end_date` parameters on Lead and Visit list requests. Omitting them will cause the API to return an error. The SDK provides the `dateRange()`fluent method as a first-class concept to make this explicit and convenient.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// dateRange maps to start_date and end_date server-side filters
$leads = Lead::list($lf)
    ->dateRange('2024-03-01', '2024-03-31')
    ->fetch();
```

Dates must be formatted as `YYYY-MM-DD` strings. Both start and end are inclusive.

> **devMode warning:** When `devMode` is enabled in configuration, the SDK emits a `WARN`-level error if you call `fetch()` or `fetchAll()` on a Lead or Visit collection without first calling `dateRange()`. It does not throw an exception, so the request still proceeds — but the API will likely return an error response.

---

Pagination
----------

[](#pagination)

The Leadfeeder API uses 1-indexed, page-number-based pagination with a configurable page size. The SDK default page size is 100. Individual requests return a single page; `fetchAll()` continues fetching until the `links.next` key is absent from the API response.

### Fetching a Single Page

[](#fetching-a-single-page)

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Default: page 1, 100 results
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->fetch();

echo count($leads) . ' leads on this page' . PHP_EOL;
```

### Controlling Page Number and Page Size

[](#controlling-page-number-and-page-size)

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Fetch page 3 with 25 results per page
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->page(3)
    ->pageSize(25)
    ->fetch();
```

### Auto-Pagination with fetchAll()

[](#auto-pagination-with-fetchall)

The `fetchAll()` method iterates through all available pages automatically, accumulating results into a single collection. For Lead collections, auto-pagination stops at 10,000 records regardless of whether more pages exist — this cap prevents runaway memory consumption on large accounts.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Retrieve all leads across all pages (up to 10,000)
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-12-31')
    ->pageSize(100)
    ->fetchAll();

echo 'Total leads loaded: ' . count($leads) . PHP_EOL;
```

Key pagination facts:

- Pages are 1-indexed. Passing `page(0)` will result in unexpected API behavior.
- The SDK default page size is 100. The Leadfeeder API maximum is typically 1,000.
- `fetchAll()` stops when `links.next` is absent from the API response or the 10,000-lead cap is reached.
- Visit collections use the base `fetchAll()` with no hard cap; only LeadCollection enforces 10,000.

---

Query Building
--------------

[](#query-building)

The SDK separates filtering into two complementary strategies: server-side WHERE filters that are sent as query parameters in the API request, and client-side HAS filters that are evaluated after the response is received. Understanding which approach to use for a given condition is important for both correctness and performance.

### WHERE Filters (Server-Side)

[](#where-filters-server-side)

Server-side filters are passed as query parameters and evaluated by the Leadfeeder API. Only the fields explicitly listed in each resource's `WHERE_OPERATIONS` constant are valid server-side filter keys.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Filter by a specific custom feed (server-side)
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->where('custom_feed_id', 'feed-abc123')
    ->fetch();
```

Supported server-side filter fields by resource:

ResourceFieldOperatorLead`start_date``=`Lead`end_date``=`Lead`custom_feed_id``=`Visit`start_date``=`Visit`end_date``=`Note that `start_date` and `end_date` are set automatically by `dateRange()` — you do not need to pass them via `where()` directly. Using `where()` for dates is supported but redundant when `dateRange()` is already called.

> **devMode warning:** When `devMode` is enabled, calling `where()` with a field that is not in the resource's `WHERE_OPERATIONS` table emits a `WARN`-level error. This helps catch typos and unsupported filters during development.

### HAS Filters (Client-Side)

[](#has-filters-client-side)

Client-side HAS filters are evaluated in PHP after the API response is received. They can match against any property in the resource's `PROP_TYPES` definition, not just the fields supported as server-side query parameters. This makes `has()` useful for any narrowing that the API does not natively support.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Only include leads with a quality score of 3 or higher (client-side)
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->has('quality', 3, '>=')
    ->fetch();

// Combine server-side and client-side filters
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->where('custom_feed_id', 'feed-abc123')
    ->has('employee_count', 50, '>')
    ->has('status', 'new')
    ->fetch();
```

Supported `has()` operators:

OperatorMeaning`=`Equal (default)`!=`Not equal`>`Greater than`>=`Greater than or equal` ' . ($step['path'] ?? '') . PHP_EOL;
        }
    }
}
```

The `visit_route` property is typed as `array:object`, meaning it is an array of associative arrays. Each element represents a single page view within the visit session.

---

Website Tracking Script (Singleton)
-----------------------------------

[](#website-tracking-script-singleton)

The Website Tracking Script is a singleton resource — there is exactly one per account, and it has no list endpoint. Fetch it by calling `fetch()` with no arguments. Calling `list()` on this resource will throw a `RuntimeException`.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\WebsiteTrackingScript;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Fetch the singleton — no ID argument needed
$script = WebsiteTrackingScript::new($lf)->fetch();

echo 'Script hash: ' . $script->script_hash . PHP_EOL;
echo 'Timezone: ' . $script->timezone . PHP_EOL;

// script_html is typed as 'html' — raw HTML string ready to embed
echo $script->script_html . PHP_EOL;
```

Available properties: `id`, `script_hash`, `script_html` (typed `html`), `timezone`.

> The `list()` method on `WebsiteTrackingScript` is disabled and throws: `RuntimeException: WebsiteTrackingScript is a singleton entity and cannot be listed.`

---

Export Workflow
---------------

[](#export-workflow)

The Leadfeeder export system is asynchronous. You create an export job, poll for completion, and then download the processed data once it is ready. The `ExportManager` class handles this full lifecycle.

### Full Lifecycle: Create, Wait, Download

[](#full-lifecycle-create-wait-download)

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Export\ExportManager;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Step 1: Create the export job
$export = ExportManager::create($lf, [
    'account_id' => 'your-account-id',
    'start_date' => '2024-01-01',
    'end_date'   => '2024-01-31',
]);

echo 'Export created: ' . $export->getExportId() . PHP_EOL;

// Step 2: Wait for completion (polls every 10s, up to 30 attempts = 5 minutes max)
$export->waitForCompletion();

// Step 3: Download the data
$rows = $export->download();

foreach ($rows as $row) {
    echo $row['id'] . PHP_EOL;
}
```

### Manual Polling Alternative

[](#manual-polling-alternative)

If you need finer control over the polling interval or want to integrate the status check into your own event loop, use `checkStatus()` directly.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Export\ExportManager;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$export = ExportManager::create($lf, [
    'account_id' => 'your-account-id',
    'start_date' => '2024-06-01',
    'end_date'   => '2024-06-30',
]);

// Poll manually until status changes from 'pending'
$attempts = 0;
while ($export->getStatus() === 'pending' && $attempts < 60) {
    sleep(5);
    $status = $export->checkStatus();
    echo 'Status: ' . $status . PHP_EOL;
    $attempts++;
}

if ($export->getStatus() === 'processed') {
    $rows = $export->download();
    echo count($rows) . ' rows downloaded' . PHP_EOL;
}
```

### Filtering by Custom Feed

[](#filtering-by-custom-feed)

Pass `custom_feed_id` in the params array to scope the export to a specific feed.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Export\ExportManager;

$lf = Leadfeeder::connect('your-api-token-here');

$export = ExportManager::create($lf, [
    'account_id'     => 'your-account-id',
    'start_date'     => '2024-01-01',
    'end_date'       => '2024-01-31',
    'custom_feed_id' => 'your-feed-id',
]);

$export->waitForCompletion(pollIntervalSeconds: 15, maxAttempts: 40);
$rows = $export->download();
```

**Required parameters:** `account_id`, `start_date`, `end_date`. `custom_feed_id` is optional.

The export creation POST request uses the `export` rate-limit scope (5 requests/minute). Status polls use the per-token rate limit scope. Download requests use an unauthenticated client — the pre-signed download URL itself serves as the credential.

---

IP Enrichment
-------------

[](#ip-enrichment)

The Leadfeeder Discover API lets you identify company information for a given IP address. This is a separate API service with its own endpoint (`https://api.lf-discover.com`) and authentication method (`X-API-KEY` header). It requires a distinct API key separate from your Leadfeeder token.

The `IpEnrichClient` can be created directly or via the `Leadfeeder::connectIpEnrich()` factory, which caches clients by API key.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;

// Create an IP Enrichment client via the factory (cached singleton per API key)
$enrichClient = Leadfeeder::connectIpEnrich('your-discover-api-key');

// Look up a company by IP address
$company = $enrichClient->lookup('203.0.113.42');

if ($company !== null) {
    // $company is a raw associative array — not an entity object
    echo $company['name'] . PHP_EOL;
    echo $company['domain'] . PHP_EOL;
} else {
    // null indicates no company was found (404) or an error occurred
    echo 'No company identified for this IP' . PHP_EOL;
}
```

Key points about IP Enrichment:

- **Separate API key** — your Leadfeeder token does not work with the Discover API
- **`X-API-KEY` authentication** — the Discover API uses a different auth header than the main API
- **Returns raw arrays** — `lookup()` returns `?array`, not an entity object
- **404 = null** — when no company is found for the IP, `lookup()` returns `null` without raising an error
- **Independent rate limit** — 60 requests per minute (configurable), tracked in its own sliding window
- **No connection required** — `IpEnrichClient` does not need a `Leadfeeder` connection instance

---

Configuration
-------------

[](#configuration)

The SDK ships with a `default.leadfeederapi.config.json` file that contains all default values. You can override any setting by loading a custom configuration file or by calling `Configuration::set()`at runtime.

### Default Configuration

[](#default-configuration)

By default, the SDK connects to `https://api.leadfeeder.com`, disables caching and logging, and enables rate limiting with a 100-requests-per-minute general limit and 5-per-minute for exports.

### Custom Configuration File

[](#custom-configuration-file)

Create a `leadfeederapi.config.json` file in your project and load it at bootstrap. Only the keys you wish to override need to be present — values are merged recursively with the defaults.

```
{
    "devMode": true,
    "enabled": {
        "cache": true,
        "logging": true
    },
    "rateLimit": {
        "perMinute": 80
    },
    "error": {
        "triggerPhpErrors": true
    }
}
```

### Loading Configuration

[](#loading-configuration)

```
use Jcolombo\LeadfeederApiPhp\Configuration;

// Load a configuration file, merging with defaults (path to file or directory)
Configuration::overload('/path/to/your/project');

// Or load a file at an explicit path
Configuration::load('/path/to/leadfeederapi.config.json');

// Read a configuration value
$timeout = Configuration::get('connection.timeout');         // 30
$devMode = Configuration::get('devMode', false);            // false (with default)

// Override a single value at runtime
Configuration::set('devMode', true);
Configuration::set('rateLimit.perMinute', 60);
```

`overload()` looks for a file named `leadfeederapi.config.json` in the given directory (or uses the path directly if it points to a file). If the file does not exist, it silently returns. `load()` requires the file to exist and throws on invalid JSON.

### Configuration Options

[](#configuration-options)

KeyTypeDefaultDescription`connection.url`string`https://api.leadfeeder.com`Base API URL`connection.timeout`int`30`HTTP request timeout in seconds`connection.verify`bool`true`SSL certificate verification`ipEnrich.url`string`https://api.lf-discover.com`Discover API base URL`ipEnrich.rateLimit.perMinute`int`60`IP Enrichment rate limit`enabled.cache`bool`false`Enable response caching`enabled.logging`bool`false`Enable request logging`rateLimit.enabled`bool`true`Enable rate limiting`rateLimit.perMinute`int`100`General requests per minute`rateLimit.export.perMinute`int`5`Export creation requests per minute`rateLimit.minDelayMs`int`200`Minimum milliseconds between requests`rateLimit.safetyBuffer`int`1`Subtract from perMinute before throttling`rateLimit.maxRetries`int`3`Max 429 retry attempts`rateLimit.retryDelayMs`int`2000`Initial retry delay in milliseconds`devMode`bool`false`Enable development warnings`log.connections`bool`false`Log new connection creation`log.requests`bool`true`Log each HTTP request`error.enabled`bool`true`Enable error handling`error.triggerPhpErrors`bool`false`Trigger native PHP errors`error.handlers.notice`array`["log"]`Handlers for notice-level errors`error.handlers.warn`array`["log"]`Handlers for warning-level errors`error.handlers.fatal`array`["log", "echo"]`Handlers for fatal errors---

Caching
-------

[](#caching)

The SDK includes a built-in file-based response cache for GET requests. Caching is disabled by default and must be explicitly enabled. Once enabled, successful GET responses are serialized and stored on disk; subsequent identical requests are served from cache within the configured lifespan. POST requests (such as export creation) automatically invalidate related cache entries via `ScrubCache`.

### Option A: Enable via Configuration

[](#option-a-enable-via-configuration)

```
use Jcolombo\LeadfeederApiPhp\Configuration;

Configuration::set('enabled.cache', true);
```

### Option B: PHP Constant

[](#option-b-php-constant)

Define the `LFAPI_REQUEST_CACHE_PATH` constant before making any requests. The SDK looks for this constant to determine the cache directory. Both the constant and the `enabled.cache` config key must be set for caching to activate.

```
define('LFAPI_REQUEST_CACHE_PATH', '/tmp/my-app-cache');

use Jcolombo\LeadfeederApiPhp\Configuration;

Configuration::set('enabled.cache', true);
```

Cache files are written to a `lfapi-cache/` subdirectory inside the configured path. The default lifespan is 300 seconds (5 minutes). Files older than the lifespan are deleted on the next access attempt.

### Cache Behavior Summary

[](#cache-behavior-summary)

- Only `GET` requests are cached
- `POST` requests (export create) trigger cache invalidation for the related URL scope
- Cached responses are stored as serialized `RequestResponse` objects
- Cache hits return a new `RequestResponse` with the `fromCacheKey` property populated

### Custom Cache Backend

[](#custom-cache-backend)

If you need Redis, Memcached, or any other storage layer, register three callables with `Cache::registerCacheMethods()`:

```
use Jcolombo\LeadfeederApiPhp\Cache\Cache;
use Jcolombo\LeadfeederApiPhp\Utility\RequestResponse;

Cache::registerCacheMethods(
    read: function (string $key): ?RequestResponse {
        $data = redis()->get('lfapi:' . $key);
        return $data !== false ? unserialize($data) : null;
    },
    write: function (string $key, RequestResponse $response): void {
        redis()->setex('lfapi:' . $key, 300, serialize($response));
    },
    clear: function (?string $key): void {
        if ($key !== null) {
            redis()->del('lfapi:' . $key);
        } else {
            // Clear all lfapi:* keys
            foreach (redis()->keys('lfapi:*') as $k) {
                redis()->del($k);
            }
        }
    }
);
```

---

Rate Limiting
-------------

[](#rate-limiting)

The SDK implements multi-scope rate limiting with sliding window tracking. Each scope maintains an independent timestamp log, allowing fine-grained control over different request categories without one scope blocking another.

### Four Rate Limit Scopes

[](#four-rate-limit-scopes)

Scope KeyApplies ToDefault Limit`token:{hash}`All requests using a specific token (no account set)100/min`account:{id}`All requests once `setAccount()` is called100/min`export``ExportManager::create()` POST requests only5/min`ipenrich:{hash}``IpEnrichClient::lookup()` requests60/min### How Rate Limiting Works

[](#how-rate-limiting-works)

Before each request, `RateLimiter::waitIfNeeded()` runs a three-stage check:

1. Prune all timestamps older than 60 seconds from the sliding window
2. If the number of recent requests has reached `perMinute - safetyBuffer`, sleep until the oldest timestamp in the window ages out of the 60-second window
3. If the time elapsed since the last request is less than `minDelayMs`, sleep the remaining gap

On a 429 response from the API, the SDK retries up to `maxRetries` times (default 3) with exponential backoff starting at `retryDelayMs` (default 2,000ms). After exhausting all retries, a `FATAL` error is raised.

Rate limiting can be disabled entirely for testing or high-trust environments:

```
use Jcolombo\LeadfeederApiPhp\Configuration;

Configuration::set('rateLimit.enabled', false);
```

---

Error Handling
--------------

[](#error-handling)

The SDK uses a three-level severity system for all internal error conditions. Error behavior is fully configurable — you can choose whether errors are logged, echoed, or trigger native PHP errors, independently per severity level.

### Severity Levels

[](#severity-levels)

LevelEnum CaseDefault HandlersTypical Cause`notice``ErrorSeverity::NOTICE``log`Informational — non-critical conditions`warn``ErrorSeverity::WARN``log`Potential problem — missing date range in devMode`fatal``ErrorSeverity::FATAL``log`, `echo`Unrecoverable — rate limit exhausted, 402, 5xx### Customizing Error Behavior

[](#customizing-error-behavior)

```
use Jcolombo\LeadfeederApiPhp\Configuration;

// Silence all fatal errors (use with caution)
Configuration::set('error.handlers.fatal', []);

// Add echo output to warn-level errors
Configuration::set('error.handlers.warn', ['log', 'echo']);

// Trigger native PHP errors (useful for frameworks with custom error handlers)
Configuration::set('error.triggerPhpErrors', true);

// Disable error handling entirely
Configuration::set('error.enabled', false);
```

### Checking Response Success

[](#checking-response-success)

Every request returns a `RequestResponse` object. Always check `$response->success` before processing the body. The entity and collection classes handle this internally, but when you need raw access:

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

// Access to the raw response is available indirectly; entity fetch methods return
// the entity itself on success. For most usage, the fluent resource API is sufficient.
// The RequestResponse is available inside the Leadfeeder::execute() pipeline.
```

In normal SDK usage — through resource `fetch()`, `list()->fetch()`, or `ExportManager` — errors are surfaced automatically through the configured error handlers. You do not need to unwrap response objects manually unless you are extending the SDK.

---

Working with Properties
-----------------------

[](#working-with-properties)

Every resource entity exposes its properties through magic `__get` / `__set` accessors and the explicit `get()` / `set()` methods. Properties are automatically coerced to their defined types upon hydration.

### Magic Property Access

[](#magic-property-access)

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$lead = Lead::new($lf)->fetch('abc123');

// Magic accessor (preferred for reading)
echo $lead->name;
echo $lead->employee_count;  // integer
echo $lead->first_visit_date;  // date string

// Explicit method (preferred when the property name is dynamic)
echo $lead->get('website_url');
```

### Complex Type Examples

[](#complex-type-examples)

Some properties are typed as complex structures rather than scalar values.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$lead = Lead::new($lf)->fetch('abc123');

// 'tags' is typed as 'array' — a flat array of strings
$tags = $lead->tags;
if (is_array($tags)) {
    echo implode(', ', $tags) . PHP_EOL;
}

// 'industries' is typed as 'array:object' — array of associative arrays
$industries = $lead->industries;
if (is_array($industries)) {
    foreach ($industries as $industry) {
        echo $industry['name'] ?? '' . PHP_EOL;
    }
}

// 'employees_range' is typed as 'object' — a single associative array
$range = $lead->employees_range;
if (is_array($range)) {
    echo $range['min'] . ' - ' . $range['max'] . PHP_EOL;
}
```

### Property Types

[](#property-types)

The SDK's type system covers all Leadfeeder data shapes:

TypePHP RepresentationExample Properties`text``string``name`, `website_url`, `id``integer``int``employee_count`, `quality`, `visits``date``string` (YYYY-MM-DD)`first_visit_date`, `last_visit_date``datetime``string` (ISO 8601)`started_at``array``array` (flat)`tags`, `ga_client_ids``object``array` (assoc)`employees_range``array:object``array` of `array``industries`, `visit_route``html``string` (raw HTML)`script_html``enum:*``string` (validated)`subscription`, `website_tracking_status`### Serialization

[](#serialization)

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$lead = Lead::new($lf)->fetch('abc123');

// Convert single entity to array or JSON
$array = $lead->toArray();
$json  = $lead->toJson();

// Collections implement JsonSerializable — json_encode works directly
$leads = Lead::list($lf)
    ->dateRange('2024-01-01', '2024-01-31')
    ->fetch();

$jsonString = json_encode($leads);

// flatten() extracts a single property from all entities in the collection
$names = $leads->flatten('name');  // array of all lead names

// raw() returns the keyed array of entity objects (keyed by entity ID)
$raw = $leads->raw();
```

---

Advanced Usage
--------------

[](#advanced-usage)

### Multiple Connections

[](#multiple-connections)

The `Leadfeeder::connect()` factory is a singleton keyed by token. If you need to work with multiple API tokens simultaneously, call `connect()` with each distinct token — each returns its own independent connection with its own rate limit state.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Account;

// Two separate connections for two different API users
$lf1 = Leadfeeder::connect('token-for-client-a');
$lf2 = Leadfeeder::connect('token-for-client-b');

$lf1->setAccount('account-id-a');
$lf2->setAccount('account-id-b');

// Each connection operates independently
$accountsA = Account::list($lf1)->fetch();
$accountsB = Account::list($lf2)->fetch();

// Disconnect a specific token when done
Leadfeeder::disconnect('token-for-client-a');

// Or disconnect everything
Leadfeeder::disconnect();
```

### Combining Web Visitor Leads with IP Enrichment

[](#combining-web-visitor-leads-with-ip-enrichment)

A common pattern is to retrieve leads from the web visitor feed and then enrich any leads that have a known IP address with additional company data from the Discover API.

```
use Jcolombo\LeadfeederApiPhp\Leadfeeder;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead;
use Jcolombo\LeadfeederApiPhp\Entity\Resource\Visit;

$lf = Leadfeeder::connect('your-api-token-here');
$lf->setAccount('your-account-id');

$enrichClient = Leadfeeder::connectIpEnrich('your-discover-api-key');

// Fetch leads for the past week
$leads = Lead::list($lf)
    ->dateRange('2024-01-22', '2024-01-28')
    ->has('status', 'new')
    ->fetch();

foreach ($leads as $lead) {
    // Fetch the most recent visit for this lead to get its IP context
    $visits = Visit::list($lf)
        ->dateRange('2024-01-22', '2024-01-28')
        ->forLead($lead->id)
        ->pageSize(1)
        ->fetch();

    foreach ($visits as $visit) {
        // Enrich with Discover API using visitor IP (if available from visit context)
        $company = $enrichClient->lookup('203.0.113.' . rand(1, 254));
        if ($company !== null) {
            echo $lead->name . ' enriched with: ' . $company['name'] . PHP_EOL;
        }
    }
}
```

---

Running Tests
-------------

[](#running-tests)

The SDK ships with a custom zero-dependency test runner. No PHPUnit or other test framework is required. The test suite makes live API calls and requires valid credentials configured in the `testing` section of the config.

### Running the Suite

[](#running-the-suite)

```
# Run all tests
composer test

# Dry run — show what would execute without making API calls
composer test:dry-run

# Verbose output — show each assertion result
composer test:verbose
```

### CLI Options

[](#cli-options)

The test runner script accepts the following options directly:

```
# Dry run mode
./tests/validate --dry-run

# Verbose mode
./tests/validate --verbose

# Only run tests for a specific resource
./tests/validate --resource=lead

# Combine options
./tests/validate --verbose --resource=visit
```

CLI OptionEffect`--dry-run`Parse and display the test plan without executing any requests`--verbose`Print each individual assertion result as it runs`--resource=`Run only the test group for the named resource (e.g. `lead`, `visit`, `account`)### Test Credentials

[](#test-credentials)

Supply your API credentials via the `testing` block in a local `leadfeederapi.config.json` file. This file should never be committed to version control.

```
{
    "testing": {
        "api_key": "your-test-token",
        "ip_enrich_api_key": "your-discover-key",
        "account_id": "your-account-id"
    }
}
```

Then load the file before running tests, or place it in the project root where `Configuration::overload()` will pick it up automatically.

---

Contributing
------------

[](#contributing)

Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request. The document covers branching conventions, the PR workflow, coding standards (PSR-12, PHP 8.1 minimum, `strict_types`), and the changelog maintenance requirement.

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE) for details.

---

Credits
-------

[](#credits)

Developed and maintained by [Joel Colombo](mailto:jc-dev@360psg.com) at [360 PSG, Inc.](https://360psg.com)

This package is independently developed and is not affiliated with or endorsed by [Leadfeeder](https://www.leadfeeder.com) or Dealfront.

---

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.

###  Health Score

31

—

LowBetter than 66% of packages

Maintenance85

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity28

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

80d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/35b4a899dcb0f25a0b114361713f56db6544facda0fc982386ede3d8ef2cc6ec?d=identicon)[jcolombo](/maintainers/jcolombo)

---

Top Contributors

[![jcolombo](https://avatars.githubusercontent.com/u/1483186?v=4)](https://github.com/jcolombo "jcolombo (4 commits)")

---

Tags

phpapisdkrestcrmlead generationb2bleadfeederlead feederwebsite visitors

### Embed Badge

![Health badge](/badges/jcolombo-leadfeeder-api-php/health.svg)

```
[![Health](https://phpackages.com/badges/jcolombo-leadfeeder-api-php/health.svg)](https://phpackages.com/packages/jcolombo-leadfeeder-api-php)
```

###  Alternatives

[xeroapi/xero-php-oauth2

Xero official PHP SDK for oAuth2 generated with OpenAPI spec 3

1054.6M18](/packages/xeroapi-xero-php-oauth2)[onesignal/onesignal-php-api

A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com

34199.5k2](/packages/onesignal-onesignal-php-api)[zenditplatform/zendit-php-sdk

PHP client for Zendit API

1194.4k](/packages/zenditplatform-zendit-php-sdk)[huaweicloud/huaweicloud-sdk-php

Huawei Cloud SDK for PHP

1830.2k2](/packages/huaweicloud-huaweicloud-sdk-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
