onetracepro / onetrace-php
PHP client for the OneTrace.pro customer data platform API: events, profiles, products, recommendations, segments, journeys and campaigns.
Requires
- php: ^7.4 || ^8.0
- ext-curl: *
- ext-json: *
Requires (Dev)
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Suggests
- psr/http-client-implementation: Send requests through your own PSR-18 client (Guzzle, Symfony HttpClient) instead of cURL
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-07 03:25:27 UTC
README
Server-side integration with OneTrace.pro, the customer data platform: send events and orders, update profiles and consents, sync the product catalog, get recommendations, manage segments, journeys and campaigns — the whole public API v1, without writing HTTP requests by hand.
- PHP 7.4–8.5, no required dependencies besides
ext-curlandext-json; any PSR-18 client can be used instead of cURL. - Safe retries: network errors,
429and5xxare retried with backoff; events are deduplicated bymessageId, creating and launching resources carries anIdempotency-Key. - Batching buffer for events, cursor pagination helpers, typed exceptions.
Installation
composer require onetracepro/onetrace-php
Quick start
use OneTrace\Client; $onetrace = new Client('https://cdp.onetrace.pro', [ 'write_key' => getenv('ONETRACE_WRITE_KEY'), // cdp_wk_…: events, recommendations 'secret_key' => getenv('ONETRACE_SECRET_KEY'), // cdp_sk_…: everything else, server only ]); $onetrace->events()->track([ 'userId' => (string) $user->id, 'event' => 'order_completed', 'messageId' => 'order-' . $order->number, // a repeat within 24 hours is ignored 'properties' => [ 'order_id' => $order->number, 'amount' => $order->total, 'products' => [['product_id' => 'SKU-1', 'quantity' => 2, 'price' => 4990]], ], ]);
The first argument is the address of your account: https://cdp.onetrace.pro, or the domain of your white-label brand. Keys are created in the project under API keys. A write key is enough for events; a secret key gets only the permissions chosen when it was created. Keep keys in environment variables and never send a secret key to browsers.
Options
| Option | Default | |
|---|---|---|
write_key |
— | cdp_wk_…: events, recommendations, widgets, Web Push |
secret_key |
— | cdp_sk_…: all methods (used for events too when there is no write key) |
timeout |
10 |
seconds per request (built-in cURL transport) |
connect_timeout |
5 |
seconds to connect (built-in cURL transport) |
max_retries |
3 |
retries of network errors, 429 and 5xx; 0 disables |
retry_delay |
0.5 |
first backoff delay in seconds, doubled on every retry (up to 8 s); Retry-After wins |
user_agent |
— | appended to the User-Agent, e.g. my-shop/2.1 |
language |
en |
language of error messages from the API: en, ru, de, es, fr, it, pt, tr, uz, zh |
http_client, request_factory, stream_factory |
— | send through a PSR-18 client instead of cURL |
transport |
cURL | your own OneTrace\Http\Transport (tests, custom networking) |
Events
$events = $onetrace->events(); // A visitor became a known customer: link the browser's anonymous id to the user. $events->identify([ 'userId' => (string) $user->id, 'anonymousId' => $_COOKIE['cdp_aid'] ?? null, // set by the website tracker 'traits' => ['email' => $user->email, 'phone' => '+4915112345678', 'first_name' => 'Anna'], ]); $events->track(['userId' => '42', 'event' => 'subscription_renewed', 'properties' => ['plan' => 'pro']]); $events->page(['anonymousId' => $anonymousId, 'name' => 'Checkout', 'properties' => ['url' => $url]]); $events->alias(['previousId' => $oldUserId, 'userId' => '42']);
Every message needs userId or anonymousId (alias needs userId and previousId, track needs event). The library adds messageId, timestamp (or formats a DateTimeInterface you pass) and context.library. Traits email, phone, telegram_chat_id and viber_id identify the profile; other traits become profile attributes; null deletes a trait. Calls return ['accepted' => 1, 'duplicates' => 0]: events are processed asynchronously.
Many events — batch() splits them into requests of up to 500 messages and 1 MB:
$onetrace->events()->batch([ ['type' => 'identify', 'userId' => '42', 'traits' => ['plan' => 'pro']], ['type' => 'track', 'userId' => '42', 'event' => 'plan_changed'], ]);
Buffer — queue events during a request or a job and send them in batches:
$buffer = $onetrace->events()->buffer(100); // sends every 100 events foreach ($orders as $order) { $buffer->track(['userId' => $order->userId, 'event' => 'order_shipped', 'properties' => ['order_id' => $order->number]]); } $buffer->flush(); // the rest; also sent automatically when the buffer is destroyed
In long-running workers call flush() yourself. A failed flush() throws and keeps the messages; flushing again resends them with the same messageIds. Errors of the automatic flush at the end of the script are passed to the callback buffer(100, function (Throwable $e, array $lost) { … }), or reported as a PHP warning.
Profiles
use OneTrace\Identity; $profile = $onetrace->profiles()->get('email', 'anna@example.com'); // traits, identities, first/last seen $onetrace->profiles()->updateConsent('user_id', '42', 'email', 'unsubscribed', 'news'); foreach ($onetrace->profiles()->iterateEvents('user_id', '42', ['name' => 'order_completed']) as $event) { // newest first, all pages } $link = $onetrace->profiles()->telegramLink(Identity::userId(42)); // ['url' => 'https://t.me/…', …] $onetrace->profiles()->delete('user_id', '42'); // GDPR erasure
Identity types: user_id, anonymous_id, email, phone, telegram_chat_id, web_push. Personal data in responses is masked unless the key has the profiles.pii permission.
Products and recommendations
$onetrace->products()->upsert( [['id' => 'SKU-1', 'name' => 'Sneakers', 'price' => 4990, 'currency' => 'EUR', 'url' => 'https://shop.example/sku-1', 'image' => 'https://shop.example/sku-1.jpg', 'category_ids' => ['shoes'], 'available' => true]], [['id' => 'shoes', 'name' => 'Shoes']] ); $onetrace->products()->delete(['SKU-2']); $recommendations = $onetrace->recommendations()->get('viewed_with', ['item' => 'SKU-1', 'limit' => 4]); // personal, popular, trending, viewed_with, bought_with, similar, recently_viewed
Product ids are the same ids the website sends in events (product_id). Up to 1000 products per call.
Segments, journeys and campaigns
$segment = $onetrace->segments()->create(['name' => 'VIP', 'type' => 'static']); $onetrace->segments()->addMembers($segment['id'], [Identity::email('anna@example.com'), Identity::userId(42)]); $onetrace->segments()->membership($segment['id'], 'user_id', '42'); // ['member' => true, …] // Start a journey with the "API" trigger for one customer; the key enrolls only once. $onetrace->journeys()->enroll(7, Identity::userId(42), ['order_id' => 'A-1001'], 'order-A-1001'); $onetrace->journeys()->pause(7); $campaign = $onetrace->campaigns()->get(3); $onetrace->campaigns()->schedule(3); $report = $onetrace->campaigns()->report(3);
Segment rules, journey graphs and campaign settings use the same JSON as the API reference.
Pagination
Lists return a OneTrace\Page (iterable, countable, getNextCursor()); iterate…() methods walk all pages lazily:
$page = $onetrace->segments()->list(['limit' => 50]); $next = $onetrace->segments()->list(['cursor' => $page->getNextCursor()]); foreach ($onetrace->segments()->iterateMembers(12, ['limit' => 200]) as $member) { // … }
Errors
All exceptions implement OneTrace\Exception\OneTraceException.
| Exception | When |
|---|---|
ValidationException (422) |
invalid request; getErrors() returns messages by field |
AuthenticationException (401) |
missing, invalid or revoked key |
PaymentRequiredException (402) |
the feature is not in the plan (getReason() = feature_unavailable, getFeature()) or the project is read-only after the trial or the payment grace period (subscription_expired) |
PermissionException (403) |
the key lacks a permission, the site domain is not allowed or the account is suspended |
NotFoundException (404) |
not found in the key's project |
ConflictException (409) |
the current state does not allow the action |
RateLimitException (429) |
rate limit or monthly event quota; getRetryAfter() |
ServerException (5xx) |
temporary server error |
ApiException |
any other error status; base class of the above, getStatusCode(), getPayload() |
TransportException |
no response: DNS, connection, TLS, timeout |
ConfigurationException |
a method needs a key the client was not given |
Network errors, 429, 5xx (and 409 of a request with an idempotency key) are retried before the exception is thrown. Invalid arguments (a track without event, an unknown identity type) throw InvalidArgumentException before any request.
try { $onetrace->segments()->create(['name' => '']); } catch (OneTrace\Exception\ValidationException $e) { $e->getErrors(); // ['name' => ['The name field is required.']] }
Idempotency
Retries never duplicate data: events carry a messageId (pass your own, such as an order number, to make resending from your side safe too); creating segments, journeys and campaigns, adding segment members, enrolling and launching send an Idempotency-Key — generated per call and reused on retries, or your own as the last argument:
$onetrace->segments()->create(['name' => 'VIP', 'type' => 'static'], 'segment-vip');
PSR-18 clients
$factory = new Nyholm\Psr7\Factory\Psr17Factory(); $onetrace = new OneTrace\Client('https://cdp.onetrace.pro', [ 'secret_key' => getenv('ONETRACE_SECRET_KEY'), 'http_client' => new GuzzleHttp\Client(['timeout' => 10]), 'request_factory' => $factory, 'stream_factory' => $factory, ]);
Testing your code
Pass a transport implementing OneTrace\Http\Transport to record requests and return prepared responses instead of calling the API.
Development
composer install
composer check # PHPStan and PHPUnit
The test suite checks that every operation of the API specification has a method (tests/OperationsTest.php); ONETRACE_SPEC_URL=https://cdp.onetrace.pro/api/v1/openapi.json vendor/bin/phpunit --filter OperationsTest runs it against the live platform.
License
MIT, see LICENSE.