Skip to content

PHP SDK

Zero-dependency PHP SDK for the Synapse API. Published as pyrx/synapse on Packagist. Uses only PHP stdlib (curl, json, openssl).

Requires PHP 8.1+.


Installation

bash
composer require pyrx/synapse

Quick Start

php
<?php
 
require_once 'vendor/autoload.php';
 
use PyrxSynapse\Client;
 
$client = new Client(
apiKey: 'psk_live_your_api_key',
workspaceId: 'your_workspace_id',
);
 
// Track an event
$client->track(
externalId: 'user_123',
eventName: 'purchase_completed',
attributes: [
'order_id' => 'ord_456',
'amount' => 99.99,
'currency' => 'USD',
],
);
 
// Identify a contact
$client->identify(
externalId: 'user_123',
email: '[email protected]',
firstName: 'Jane',
lastName: 'Doe',
properties: ['plan' => 'pro', 'signup_source' => 'website'],
tags: ['paying', 'beta-tester'],
);
 
// Send a transactional email
$client->sendEmail(
templateSlug: 'order-confirmation',
to: [
'user_id' => 'user_123',
'email' => '[email protected]',
'first_name' => 'Jane',
],
attributes: [
'order_id' => 'ord_456',
'items' => [['name' => 'Widget', 'price' => 99.99]],
],
);
Tip

Get your API key and workspace ID from the dashboard at Settings > API Keys.


Configuration

php
$client = new Client(
apiKey: 'psk_live_xxx', // Required. API key from workspace settings.
workspaceId: 'ws_xxx', // Required. Your workspace ID.
baseUrl: 'https://...', // Default: https://synapse-api.pyrx.tech
timeout: 30, // Default: 30 seconds
maxRetries: 3, // Default: 3. Set to 0 to disable retries.
);
ParameterTypeDefaultDescription
apiKeystringrequiredYour Synapse API key (psk_live_* or psk_test_*)
workspaceIdstringrequiredYour workspace identifier
baseUrlstringhttps://synapse-api.pyrx.techAPI base URL
timeoutint30Request timeout in seconds
maxRetriesint3Retry count for 429/5xx errors. Set to 0 to disable.

Environment detection: The SDK detects test or live from your API key prefix (psk_test_* vs psk_live_*).

Retry behavior: The SDK automatically retries on 429, 500, 502, 503, and 504 with exponential backoff and jitter (capped at 30s). On 429, uses the Retry-After header when present. Curl connection errors are also retried. Client errors (400, 401, 403, 404, 422) are never retried.


Track Events

Single Event

php
$result = $client->track(
externalId: 'user_123',
eventName: 'purchase_completed',
attributes: [
'order_id' => 'ord_456',
'amount' => 99.99,
'currency' => 'USD',
],
contact: [
'email' => '[email protected]',
'first_name' => 'Jane',
],
idempotencyKey: 'purchase_ord_456', // optional, prevents duplicate processing
occurredAt: '2026-04-29T10:30:00Z', // optional, defaults to now
);
 
echo $result->eventId; // "evt_8f14e45f-..."
echo $result->status; // "accepted"
ParameterTypeRequiredDescription
externalIdstringYesYour unique user identifier
eventNamestringYesEvent name (e.g., purchase_completed)
attributesarrayNoArbitrary key-value event data
contactarrayNoContact fields to upsert alongside the event
idempotencyKeystringNoPrevents duplicate processing (7-day TTL)
occurredAtstringNoISO 8601 timestamp. Defaults to server time.

Batch Events

Track up to 50 events in a single request.

php
$result = $client->trackBatch(
events: [
['external_id' => 'user_1', 'event_name' => 'page_view', 'attributes' => ['page' => '/pricing']],
['external_id' => 'user_2', 'event_name' => 'page_view', 'attributes' => ['page' => '/docs']],
['external_id' => 'user_1', 'event_name' => 'button_clicked', 'attributes' => ['button' => 'upgrade']],
],
);
 
echo $result->accepted; // 3
echo $result->rejected; // 0

Identify Contacts

Single Contact

Create or update (upsert) a contact by externalId.

php
$contact = $client->identify(
externalId: 'user_123',
email: '[email protected]',
firstName: 'Jane',
lastName: 'Doe',
phone: '+1234567890',
timezone: 'America/New_York',
locale: 'en-US',
properties: ['plan' => 'pro', 'signup_source' => 'website'],
tags: ['paying', 'beta-tester'],
);
 
echo $contact->id; // UUID
echo $contact->externalId; // "user_123"
echo $contact->email; // "[email protected]"

Batch Identify

Upsert up to 1,000 contacts in a single request.

php
$result = $client->identifyBatch(
contacts: [
['external_id' => 'user_1', 'email' => '[email protected]', 'first_name' => 'Alice'],
['external_id' => 'user_2', 'email' => '[email protected]', 'first_name' => 'Bob'],
],
onConflict: 'merge', // "merge" | "skip" | "replace"
);
 
echo $result->total; // 2
echo $result->created; // 1
echo $result->updated; // 1

Send Transactional Email

Send a one-off email using an NLT template, without a flow.

php
$result = $client->sendEmail(
templateSlug: 'otp-verification',
to: [
'user_id' => 'user_123',
'email' => '[email protected]',
'first_name' => 'Jane',
],
attributes: [
'otp_code' => '847293',
'expiry_minutes' => 10,
],
idempotencyKey: 'otp_user_123_' . time(),
);
 
echo $result->status; // "sent" or "suppressed"
echo $result->emailLogId; // "el_8f14e45f-..."
Note

Requires a data-scoped API key (or higher). The template must exist in your workspace.


Contact Management

The $client->contacts sub-client provides full CRUD operations. Requires a management or full scoped API key.

List Contacts

php
$result = $client->contacts->list(
search: 'jane',
page: 1,
perPage: 25,
sortBy: 'created_at',
sortOrder: 'desc',
);
 
echo $result->meta->total; // 142
echo $result->meta->totalPages; // 6
 
foreach ($result->data as $c) {
echo $c->email . ' ' . $c->firstName;
}

Get a Contact

php
$contact = $client->contacts->get('contact_uuid');

Update a Contact

php
$client->contacts->update('user_123', [
'email' => '[email protected]',
'add_tags' => ['vip'],
'remove_tags' => ['trial'],
]);

Delete a Contact

php
$client->contacts->delete('user_123');

Template Management

The $client->templates sub-client manages email templates. Requires a management or full scoped API key.

List Templates

php
$templates = $client->templates->list();

Get a Template

php
$template = $client->templates->get('welcome-email');

Create a Template

php
$template = $client->templates->create([
'name' => 'Welcome Email',
'slug' => 'welcome-email',
'subject' => 'Welcome, [first name of contact]!',
'body_html' => '<h1>Welcome!</h1><p>Thanks for joining.</p>',
'sender_name' => 'PYRX Team',
'from_email' => '[email protected]',
]);

Update a Template

php
$template = $client->templates->update('welcome-email', [
'subject' => 'Welcome aboard, [first name of contact]!',
]);

Preview with Sample Data

php
$preview = $client->templates->preview('welcome-email', [
'contact' => ['first_name' => 'Jane', 'email' => '[email protected]'],
'trigger_event' => ['order_id' => 'ord_123'],
]);
 
echo $preview->subject; // Rendered subject
echo $preview->html; // Rendered HTML
echo $preview->suppressed; // false
echo $preview->suppressedReason; // null

Delete a Template

php
$client->templates->delete('old-template');

Webhook Verification

Verify incoming webhook signatures to ensure requests are authentically from Synapse. This is a static method -- no client instance needed.

php
<?php
 
use PyrxSynapse\Webhooks;
 
// In your webhook endpoint handler:
$payload = file_get_contents('php://input'); // raw request body
$headers = [
'svix-id' => $_SERVER['HTTP_SVIX_ID'],
'svix-timestamp' => $_SERVER['HTTP_SVIX_TIMESTAMP'],
'svix-signature' => $_SERVER['HTTP_SVIX_SIGNATURE'],
];
$secret = getenv('SYNAPSE_WEBHOOK_SECRET'); // e.g. "whsec_..."
 
try {
$event = Webhooks::verify($payload, $headers, $secret);
echo $event['type']; // e.g. "email.delivered"
} catch (\InvalidArgumentException $e) {
// Invalid signature, expired timestamp, or missing headers
http_response_code(400);
echo "Webhook rejected: " . $e->getMessage();
}

The verification checks:

  • All three svix-* headers are present
  • The timestamp is within 5 minutes (replay attack protection)
  • The HMAC-SHA256 signature matches (supports multiple signatures for key rotation)

Error Handling

The SDK provides typed error classes for every failure mode.

php
<?php
 
use PyrxSynapse\Client;
use PyrxSynapse\Errors\SynapseError;
use PyrxSynapse\Errors\SynapseAuthError;
use PyrxSynapse\Errors\SynapseRateLimitError;
use PyrxSynapse\Errors\SynapsePlanLimitError;
use PyrxSynapse\Errors\SynapseValidationError;
 
try {
$client->track(externalId: 'u1', eventName: 'test');
} catch (SynapsePlanLimitError $e) {
echo "Plan limit: {$e->limitType} ({$e->current}/{$e->maximum})";
echo "Current plan: {$e->plan}";
} catch (SynapseRateLimitError $e) {
echo "Rate limited. Retry after {$e->retryAfter}s";
} catch (SynapseValidationError $e) {
foreach ($e->errors as $err) {
echo "{$err['field']}: {$err['message']}";
}
} catch (SynapseAuthError $e) {
echo "Authentication failed: {$e->getMessage()}";
} catch (SynapseError $e) {
echo "API error {$e->status}: {$e->getMessage()}";
}

Error Types

Error ClassHTTP StatusPropertiesWhen
SynapseErrorAnystatus, message, code, requestIdBase class for all API errors
SynapseAuthError401, 403messageInvalid or expired API key, scope mismatch
SynapseValidationError422errors[] with field + messageRequest body validation failed
SynapseRateLimitError429retryAfter (seconds)Rate limit exceeded
SynapsePlanLimitError403limitType, current, maximum, planPlan limit reached

Environment Variables

For production deployments, load credentials from environment variables.

php
<?php
 
use PyrxSynapse\Client;
 
$client = new Client(
apiKey: getenv('SYNAPSE_API_KEY'),
workspaceId: getenv('SYNAPSE_WORKSPACE_ID'),
);
bash
export SYNAPSE_API_KEY=psk_live_a1b2c3d4e5f67890abcdef1234567890
export SYNAPSE_WORKSPACE_ID=your_workspace_id

Full Method Reference

MethodDescriptionRequired Scope
$client->track(...)Track a single eventdata
$client->trackBatch(...)Track up to 50 eventsdata
$client->identify(...)Upsert a single contactdata
$client->identifyBatch(...)Upsert up to 1,000 contactsdata
$client->sendEmail(...)Send a transactional emaildata
$client->contacts->list(...)List contacts with paginationmanagement
$client->contacts->get($id)Get a single contactmanagement
$client->contacts->update($id, $data)Update a contactmanagement
$client->contacts->delete($id)Delete a contactmanagement
$client->templates->list()List all templatesmanagement
$client->templates->get($slug)Get a template by slugmanagement
$client->templates->create($params)Create a templatemanagement
$client->templates->update($slug, $params)Update a templatemanagement
$client->templates->preview($slug, $data)Preview rendered templatemanagement
$client->templates->delete($slug)Delete a templatemanagement
Webhooks::verify($payload, $headers, $secret)Verify webhook signature--

Framework Examples

Laravel

php
<?php
// config/services.php
return [
'synapse' => [
'api_key' => env('SYNAPSE_API_KEY'),
'workspace_id' => env('SYNAPSE_WORKSPACE_ID'),
],
];
php
<?php
// app/Http/Controllers/SignupController.php
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use PyrxSynapse\Client;
 
class SignupController extends Controller
{
public function store(Request $request)
{
$client = new Client(
apiKey: config('services.synapse.api_key'),
workspaceId: config('services.synapse.workspace_id'),
);
 
// Identify the new user
$client->identify(
externalId: (string) $request->user()->id,
email: $request->user()->email,
firstName: $request->user()->first_name,
properties: ['plan' => $request->input('plan')],
tags: ['new-signup'],
);
 
// Track the signup event (triggers flows)
$client->track(
externalId: (string) $request->user()->id,
eventName: 'user_signed_up',
attributes: ['plan' => $request->input('plan'), 'source' => 'web'],
);
 
return response()->json(['success' => true], 201);
}
}

Webhook Endpoint (Laravel)

php
<?php
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use PyrxSynapse\Webhooks;
 
class WebhookController extends Controller
{
public function handle(Request $request)
{
$payload = $request->getContent();
$headers = [
'svix-id' => $request->header('svix-id'),
'svix-timestamp' => $request->header('svix-timestamp'),
'svix-signature' => $request->header('svix-signature'),
];
 
try {
$event = Webhooks::verify(
$payload,
$headers,
config('services.synapse.webhook_secret'),
);
 
// Process the event
logger()->info("Webhook received: {$event['type']}");
return response()->json(['ok' => true]);
} catch (\InvalidArgumentException $e) {
logger()->warning("Webhook rejected: {$e->getMessage()}");
return response()->json(['error' => 'Invalid signature'], 400);
}
}
}