jcolombo/leadfeeder-api-php

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

Maintainers

Package info

github.com/jcolombo/leadfeeder-api-php

pkg:composer/jcolombo/leadfeeder-api-php

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0-alpha 2026-04-08 21:01 UTC

This package is auto-updated.

Last update: 2026-04-08 21:03:33 UTC


README

Latest Version PHP Version License GitHub Issues

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.x in 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-PaginationfetchAll() 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 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

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 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

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 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)

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 on WebsiteTrackingScript is 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-KEY authentication — the Discover API uses a different auth header than the main API
  • Returns raw arrayslookup() 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 requiredIpEnrichClient does not need a Leadfeeder connection 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 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

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:

  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

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.