jcolombo / leadfeeder-api-php
PHP SDK for the Leadfeeder website visitor tracking and lead generation API
Requires
- php: >=8.1
- ext-json: *
- adbario/php-dot-notation: ^3.3
- guzzlehttp/guzzle: ^7.8
This package is auto-updated.
Last update: 2026-04-08 21:03:33 UTC
README
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: https://www.leadfeeder.com API Documentation: https://docs.leadfeeder.com/api/
Stability Notice: This package is in active development (v0.x-alpha). The API surface may change before v1.0. Pin to
^0.xin production.
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
- PHP 8.1 or higher
- A Leadfeeder account with API access
- Your Leadfeeder API token (from Settings → Personal → API Tokens)
- Composer
- (Optional) A Leadfeeder Discover API key for IP enrichment
Installation
composer require jcolombo/leadfeeder-api-php
The package is published on Packagist 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
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
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
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
The SDK models six Leadfeeder API entities. Each resource class lives under
Jcolombo\LeadfeederApiPhp\Entity\Resource\.
| Resource | Class | Scope | Pattern | Notes |
|---|---|---|---|---|
| Account | Account |
Token | list(), fetch($id) |
Lists all accounts for the token |
| Lead | Lead |
Account | list(), fetch($id) |
Date range required for list |
| Visit | Visit |
Account | list(), fetch($id) |
Date range required for list |
| CustomFeed | CustomFeed |
Account | list(), fetch($id) |
Read-only feed definitions |
| Location | Location |
Include-only | Resolved via include | Cannot be fetched or listed directly |
| WebsiteTrackingScript | 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
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
devModeis enabled in configuration, the SDK emits aWARN-level error if you callfetch()orfetchAll()on a Lead or Visit collection without first callingdateRange(). It does not throw an exception, so the request still proceeds — but the API will likely return an error response.
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
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
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()
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 whenlinks.nextis 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
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)
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:
| Resource | Field | Operator |
|---|---|---|
| Lead | 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
devModeis enabled, callingwhere()with a field that is not in the resource'sWHERE_OPERATIONStable emits aWARN-level error. This helps catch typos and unsupported filters during development.
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:
| Operator | Meaning |
|---|---|
= |
Equal (default) |
!= |
Not equal |
> |
Greater than |
>= |
Greater than or equal |
< |
Less than |
<= |
Less than or equal |
like |
Case-insensitive substring match |
The like operator uses PHP's str_contains() after lowercasing both sides. It is only valid
for string properties; numeric properties use the comparison operators.
Relationship Includes
The Leadfeeder API supports sideloading related entities via the include query parameter. The
SDK maps these to PHP entity objects automatically through its JSON:API parser. Currently, the
only supported relationship include is locations on both Lead and Visit responses.
Including Locations on Leads
use Jcolombo\LeadfeederApiPhp\Leadfeeder; use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead; $lf = Leadfeeder::connect('your-api-token-here'); $lf->setAccount('your-account-id'); // Request location data to be included alongside each lead $leads = Lead::list($lf) ->dateRange('2024-01-01', '2024-01-31') ->include('locations') ->fetch(); foreach ($leads as $lead) { $location = $lead->getIncluded('location'); if ($location !== null) { echo $lead->name . ' is from ' . $location->city . ', ' . $location->country . PHP_EOL; } }
Including Locations on Visits
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') ->include('locations') ->fetch(); foreach ($visits as $visit) { $location = $visit->getIncluded('location'); if ($location !== null) { echo $visit->id . ' originated from ' . $location->country_code . PHP_EOL; } }
The Location entity is include-only — it has no standalone API path and cannot be fetched or
listed directly. Calling Location::fetch() or Location::list() will throw a
RuntimeException. Included Location objects expose the following properties: id, country,
country_code, region, region_code, city, state_code.
Custom Feed Scoping
Custom Feeds are saved filter configurations in Leadfeeder that segment your leads into named
groups. You can list all custom feeds to discover their IDs, then use forFeed() on a Lead
collection to retrieve leads scoped to that feed. This sets a parent context that prefixes the
API request path with custom-feeds/{feedId}/.
Listing Custom Feeds
use Jcolombo\LeadfeederApiPhp\Leadfeeder; use Jcolombo\LeadfeederApiPhp\Entity\Resource\CustomFeed; $lf = Leadfeeder::connect('your-api-token-here'); $lf->setAccount('your-account-id'); $feeds = CustomFeed::list($lf)->fetch(); foreach ($feeds as $feed) { echo $feed->id . ' — ' . $feed->name . PHP_EOL; }
Fetching Leads for a Specific Feed
use Jcolombo\LeadfeederApiPhp\Leadfeeder; use Jcolombo\LeadfeederApiPhp\Entity\Resource\Lead; $lf = Leadfeeder::connect('your-api-token-here'); $lf->setAccount('your-account-id'); // forFeed() sets the request path to: custom-feeds/{feedId}/leads $leads = Lead::list($lf) ->dateRange('2024-01-01', '2024-01-31') ->forFeed('your-feed-id') ->fetch(); foreach ($leads as $lead) { echo $lead->name . PHP_EOL; }
forFeed() and the server-side where('custom_feed_id', ...) filter are complementary
approaches to the same goal. forFeed() uses a nested URL path; where() passes the feed ID
as a query parameter. In most cases forFeed() is the cleaner choice.
Lead-Scoped Visits
You can retrieve all visits attributed to a specific lead by calling forLead() on a Visit
collection. This sets the parent context to leads/{leadId}/, scoping the API path to
accounts/{accountId}/leads/{leadId}/visits.
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-01-01', '2024-01-31') ->forLead('lead-id-here') ->fetch(); foreach ($visits as $visit) { echo 'Visit: ' . $visit->started_at . PHP_EOL; // visit_route is an array:object — each item describes a page in the visit path $route = $visit->visit_route; if (is_array($route)) { foreach ($route as $step) { echo ' -> ' . ($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)
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 onWebsiteTrackingScriptis disabled and throws:RuntimeException: WebsiteTrackingScript is a singleton entity and cannot be listed.
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
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
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
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
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-KEYauthentication — 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()returnsnullwithout raising an error - Independent rate limit — 60 requests per minute (configurable), tracked in its own sliding window
- No connection required —
IpEnrichClientdoes not need aLeadfeederconnection instance
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
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
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
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
| Key | Type | Default | Description |
|---|---|---|---|
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
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
use Jcolombo\LeadfeederApiPhp\Configuration; Configuration::set('enabled.cache', true);
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
- Only
GETrequests are cached POSTrequests (export create) trigger cache invalidation for the related URL scope- Cached responses are stored as serialized
RequestResponseobjects - Cache hits return a new
RequestResponsewith thefromCacheKeyproperty populated
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
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
| Scope Key | Applies To | Default Limit |
|---|---|---|
token:{hash} |
All requests using a specific token (no account set) | 100/min |
account:{id} |
All requests once setAccount() is called |
100/min |
export |
ExportManager::create() POST requests only |
5/min |
ipenrich:{hash} |
IpEnrichClient::lookup() requests |
60/min |
How Rate Limiting Works
Before each request, RateLimiter::waitIfNeeded() runs a three-stage check:
- Prune all timestamps older than 60 seconds from the sliding window
- 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 - 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
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
| Level | Enum Case | Default Handlers | Typical 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
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
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
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
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
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
The SDK's type system covers all Leadfeeder data shapes:
| Type | PHP Representation | Example 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
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
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
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
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
# 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
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 Option | Effect |
|---|---|
--dry-run |
Parse and display the test plan without executing any requests |
--verbose |
Print each individual assertion result as it runs |
--resource=<name> |
Run only the test group for the named resource (e.g. lead, visit, account) |
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
Contributions are welcome. Please read 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
MIT — see LICENSE for details.
Credits
Developed and maintained by Joel Colombo at 360 PSG, Inc.
This package is independently developed and is not affiliated with or endorsed by Leadfeeder or Dealfront.
Changelog
See CHANGELOG.md for a detailed history of changes.