companyenrich / sdk
Official PHP SDK for the CompanyEnrich API: company and people enrichment, search, lookalikes, and work emails.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The official PHP client for the CompanyEnrich API: company and people enrichment, filter, semantic and lookalike search, work emails, and bulk jobs.
It requires PHP 8.1 or newer with the curl and json extensions, and has no Composer dependencies.
Install
composer require companyenrich/sdk
Quick start
Create an API key in your dashboard and keep it secret.
<?php require 'vendor/autoload.php'; use CompanyEnrich\Client; // Reads COMPANYENRICH_API_KEY from the environment, or pass new Client('your-api-key'). $client = new Client(); $company = $client->companies->enrich('stripe.com'); echo $company['name'] ?? '', ' ', $company['employees'] ?? '', PHP_EOL;
Responses are plain associative arrays with the API's field names. Optional fields can be absent or null, so read them with ??.
Arguments can be passed by name, as in these examples. Arrays you pass, such as filter, use the API's own field names.
Companies
// Add paid data to a lookup. Each expansion costs extra credits. $detailed = $client->companies->enrich('stripe.com', expand: ['technologies', 'workforce']); // Up to 50 domains at once. Check each item's status. $batch = $client->companies->enrichBatch(['stripe.com', 'adyen.com']); foreach ($batch['items'] as $item) { if ($item['status'] === 'found') { echo $item['domain'], ' ', $item['company']['name'] ?? '', PHP_EOL; } } // Filter search $page = $client->companies->search( filter: ['country' => 'US', 'employees' => ['51-200', '201-500'], 'categories' => 'saas'], pageSize: 25, ); echo count($page['items']), ' ', $page['nextCursor'] ?? 'last page', PHP_EOL; // Free count of the same query $total = $client->companies->count(filter: ['country' => 'US']); echo $total['count'], PHP_EOL;
People
$results = $client->people->search( filter: ['domain' => 'stripe.com', 'seniority' => ['c-suite', 'founder']], pageSize: 10, ); $person = $client->people->enrich('jane@stripe.com'); $lookup = $client->people->findEmail($person['id'], domain: 'stripe.com'); if ($lookup['status'] === 'found') { echo $lookup['email'], ' ', $lookup['certainty'] ?? '', PHP_EOL; }
Semantic and lookalike search
Semantic search finds companies or people from a plain-language description. Lookalike search finds companies similar to ones you already know. A company search can use either mode but not both, and either can be combined with filter. searchAll accepts the same arguments as search.
| Search | Credits per result |
|---|---|
| Companies by filters | 1 |
| Companies by description | 2 |
| Lookalike companies | 5 |
| People, in any mode | 2 |
Paid expansions cost extra, and an empty result costs one result's base price. To try a query for free, pass the same arguments to companies->preview() or people->preview(), which need a plan with previews.
Companies by description
By default, the API reads attributes such as location and size from your query and applies them. interpretation shows what it inferred.
$results = $client->companies->search( semantic: ['query' => 'saas companies in Turkey'], pageSize: 25, ); foreach ($results['items'] as $company) { echo $company['domain'], ' ', $company['description'] ?? '', PHP_EOL; } print_r($results['interpretation']['filter'] ?? []); // Inferred filters, such as the country print_r($results['interpretation']['boost'] ?? []); // Inferred ranking boosts, such as the category
Filters you pass take priority over inferred values of the same kind. Set 'autoFilter' => false to turn inference off, or set weight (0 to 1) to control how much semantic similarity affects ranking.
$filtered = $client->companies->search( semantic: ['query' => 'AI tools for hospitals', 'autoFilter' => false], filter: ['country' => 'US', 'employees' => ['51-200', '201-500']], pageSize: 25, );
Lookalike companies
Pass 1 to 10 seed domains. Use minScore (0 to 1) to require a minimum similarity.
$similar = $client->companies->search( lookalike: ['domains' => ['stripe.com', 'adyen.com']], filter: ['country' => 'US', 'employees' => ['51-200', '201-500']], pageSize: 25, ); foreach ($similar['items'] as $company) { echo $company['domain'], ' ', $similar['scores'][$company['id']] ?? '', PHP_EOL; // Scores are not probabilities }
Keep results in the order returned rather than sorting them by score.
People by description
The API turns your query into person filters, such as role, seniority and country, plus a description of their current companies. interpretation shows both. A query it can't infer any filters from is rejected with an error, so include something concrete like a role or location.
$people = $client->people->search( semantic: ['query' => 'marketing leaders at Series B fintech startups in London'], pageSize: 25, ); foreach ($people['items'] as $person) { echo $person['name'] ?? '', ' ', $person['position'] ?? '', PHP_EOL; } print_r($people['interpretation'] ?? []); // The inferred filters and company description
People at matching companies
Use company to find people by the kind of company they work at. These searches cover people at the 1,000 best-matching companies.
// Executives at companies matching a description $executives = $client->people->search( filter: ['seniority' => ['c-suite', 'vp']], company: ['semantic' => ['query' => 'AI tools for hospitals']], pageSize: 25, ); // Executives at companies similar to your best customers $similarTeams = $client->people->search( filter: ['seniority' => ['c-suite', 'vp']], company: ['lookalike' => ['domains' => ['stripe.com', 'adyen.com']]], pageSize: 25, );
Pagination
search() returns one page. The *All methods return a generator that requests pages as you consume them.
foreach ($client->companies->searchAll(filter: ['country' => 'TR'], pageSize: 100) as $company) { echo $company['domain'], PHP_EOL; }
These are available as companies->searchAll(), people->searchAll(), batches->listAll(), and geo->searchCountriesAll(), geo->searchStatesAll() and geo->searchCitiesAll().
Search credits are charged for every result on a fetched page, even if you stop iterating partway through it. Set pageSize close to the number of results you plan to use.
Bulk jobs
Bulk enrichment and search exports run in the background.
$job = $client->companies->bulkEnrichment->create(['stripe.com', 'adyen.com']); $client->batches->waitForCompletion( $job['id'], onProgress: fn (array $batch) => print($batch['progress']['succeeded'] . '/' . $batch['progress']['total'] . PHP_EOL), ); foreach ($client->companies->bulkEnrichment->stream($job['id']) as $record) { if ($record['success']) { echo $record['domain'], ' ', $record['company']['name'] ?? '', PHP_EOL; } }
companies->bulkEnrichment, companies->searchExport and people->searchExport share the same methods:
| Method | Returns |
|---|---|
create(...) |
The new batch's status |
stream($id) |
A generator of records, best for large jobs |
results($id) |
One JSON document with every record |
csv($id) |
The CSV as a string |
csvChunks($id) |
A generator of raw bytes, for writing to a file |
Errors
Failed requests throw a subclass of CompanyEnrich\Exception\ApiException, which carries status, title, detail, errors (field-level validation messages) and traceId (include it when contacting support).
use CompanyEnrich\Exception\ApiException; use CompanyEnrich\Exception\NotFoundException; use CompanyEnrich\Exception\PaymentRequiredException; try { $client->companies->enrich('example.invalid'); } catch (NotFoundException) { echo 'No company found', PHP_EOL; } catch (PaymentRequiredException) { echo 'Out of credits, or not included in this plan', PHP_EOL; } catch (ApiException $error) { echo $error->status, ' ', $error->detail, ' ', $error->traceId, PHP_EOL; }
| Status | Class |
|---|---|
| 400 | BadRequestException |
| 401 | AuthenticationException |
| 402 | PaymentRequiredException |
| 403 | PermissionDeniedException |
| 404 | NotFoundException |
| 422 | ValidationException |
| 429 | RateLimitException |
| 5xx | ServerException |
| Other | ApiException |
ApiConnectionException and ApiTimeoutException cover requests that never received a complete response. Everything above extends CompanyEnrichException.
Retries and timeouts
Failed requests are retried up to twice with exponential backoff.
| Failure | GET and free POST endpoints | Other POST endpoints |
|---|---|---|
| 429 rate limit | Retried | Retried |
| 408, 5xx, connection error, timeout | Retried | Not retried |
POST requests that consume credits or start a job are not repeated after an ambiguous failure, because the first attempt may already have been charged or created a batch. A download that fails partway through is never restarted, so you won't receive duplicate records.
Configuration
$configured = new Client('your-api-key', timeout: 30.0, maxRetries: 4); // Every method accepts per-request options. $configured->me(options: ['timeout' => 5.0, 'maxRetries' => 0]);
| Option | Default | Description |
|---|---|---|
apiKey |
COMPANYENRICH_API_KEY |
Your API key |
baseUrl |
https://api.companyenrich.com |
API origin, for proxies |
timeout |
120.0 |
Timeout in seconds. null disables it. For downloads, it applies when no data arrives |
maxRetries |
2 |
Retries after the first attempt |
defaultHeaders |
none | Headers added to every request |
API reference
| SDK method | Endpoint |
|---|---|
companies->get($id) |
GET /v2/companies/{companyId} |
companies->enrich($domain) |
GET /v2/companies/enrich |
companies->enrichByProperties(...) |
POST /v2/companies/enrich |
companies->enrichBatch($domains) |
POST /v2/companies/enrich/batch |
companies->autocomplete($query) |
GET /v2/companies/autocomplete |
companies->workforce(...) |
GET /v2/companies/workforce |
companies->search(...) |
POST /v2/companies/search |
companies->count(...) |
POST /v2/companies/search/count |
companies->preview(...) |
POST /v2/companies/search/preview |
companies->bulkEnrichment |
/v2/companies/enrich/bulk |
companies->searchExport |
/v2/companies/search/export |
people->get($id) |
GET /v2/people/{personId} |
people->enrich($email) |
POST /v2/people/enrich |
people->findEmail($id) |
GET /v2/people/{personId}/email |
people->search(...) |
POST /v2/people/search |
people->count(...) |
POST /v2/people/search/count |
people->preview(...) |
POST /v2/people/search/preview |
people->searchExport |
/v2/people/search/export |
batches->list(...) |
GET /v2/batches |
batches->get($id) |
GET /v2/batches/{id} |
batches->cancel($id) |
POST /v2/batches/{id}/cancel |
batches->rawResults($id) |
GET /v2/batches/{id}/results |
batches->waitForCompletion($id) |
Polls GET /v2/batches/{id} |
geo->regions() |
GET /v2/geo/regions |
geo->country($code) |
GET /v2/geo/countries/{countryCode} |
geo->searchCountries(...) |
POST /v2/geo/countries |
geo->searchStates(...) |
POST /v2/geo/states |
geo->searchCities(...) |
POST /v2/geo/cities |
industries->list() |
GET /v2/industries |
keywords->autocomplete($query) |
GET /v2/keywords/autocomplete |
technologies->autocomplete($query) |
GET /v2/technologies/autocomplete |
positions->autocomplete($query) |
GET /v2/positions/autocomplete |
me() |
GET /v2/me |
See the API documentation for parameters and credit costs.
Development
composer update vendor/bin/phpunit vendor/bin/phpstan analyse
To release, bump VERSION in src/Version.php and push to main. The release workflow runs the checks, creates the git tag, and tells Packagist about it if the version is new.
License
MIT