A complete, small PHP integration for a shop that sells a tracker with a monthly tracking subscription. It shows the whole journey: your payment provider confirms the sale → find or create the customer and fleet by your own references → register the device → request the activation (safe to retry) → verify the Fleetalyse webhook → cancel when your customer cancels.
Your retail payment and the Fleetalyse service are separate: a paid retail order does not bypass Fleetalyse credit checks, and cancelling your customer's subscription does not stop the Fleetalyse service until you call the deactivation endpoint.
fleetalyse.php — a tiny client
<?php
declare(strict_types=1);
// Minimal Fleetalyse Partner API client. The key comes from your server's secret store.
final class Fleetalyse
{
public function __construct(private string $key, private string $base = 'https://fleetalyse.co.uk/partner-api/v1') {}
public function get(string $path, array $query = []): array
{
return $this->call('GET', $path . ($query ? '?' . http_build_query($query) : ''));
}
public function post(string $path, array $body, string $idempotencyKey): array
{
return $this->call('POST', $path, $body, $idempotencyKey);
}
private function call(string $method, string $path, ?array $body = null, ?string $idem = null): array
{
for ($attempt = 1; ; $attempt++) {
$ch = curl_init($this->base . $path);
$headers = ['Authorization: Bearer ' . $this->key, 'Accept: application/json'];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$headers[] = 'Idempotency-Key: ' . $idem; // same key on every retry
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HEADER => true]);
$raw = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$json = $raw === false ? null : json_decode(substr($raw, $headerSize), true);
$retryable = $raw === false || $status === 429 || $status >= 500 || ($json['error']['retryable'] ?? false);
if ($status >= 200 && $status < 300) {
return $json ?? [];
}
if (!$retryable || $attempt >= 5) {
throw new RuntimeException(($json['error']['code'] ?? 'HTTP_' . $status) . ' (' . ($json['error']['request_id'] ?? 'no request id') . ')');
}
$retryAfter = preg_match('/^Retry-After:\s*(\d+)/mi', (string)$raw, $m) ? (int)$m[1] : 0;
sleep(max($retryAfter, 2 ** $attempt) + random_int(0, 2));
}
}
}order-paid.php — after your payment provider confirms payment
<?php
declare(strict_types=1);
require __DIR__ . '/fleetalyse.php';
// Called by YOUR payment provider's verified webhook once the retail order is paid.
// $order comes from your database: id, customer id/name, tracker IMEI, plan, lines …
function on_order_paid(array $order): void
{
$api = new Fleetalyse(getenv('FLEETALYSE_KEY'));
// 1. Find or create the Fleetalyse customer by YOUR customer id.
$found = $api->get('/customers', ['external_reference' => 'C' . $order['customer_id']]);
$customer = $found['data'][0] ?? $api->post('/customers', [
'name' => $order['customer_company'],
'external_reference' => 'C' . $order['customer_id'],
], 'customer-C' . $order['customer_id']);
// 2. Find or create the customer's fleet for this plan.
$fleetRef = 'C' . $order['customer_id'] . '-' . $order['plan'];
$found = $api->get('/fleets', ['external_reference' => $fleetRef]);
$fleet = $found['data'][0] ?? $api->post('/fleets', [
'customer_id' => $customer['id'],
'name' => $order['customer_company'] . ' (' . $order['plan'] . ')',
'plan_code' => $order['plan'],
'external_reference' => $fleetRef,
], 'fleet-' . $fleetRef);
foreach ($order['lines'] as $line) {
// 3. Register the tracker (or find it if it is already registered).
$found = $api->get('/devices', ['identifier' => $line['imei']]);
$device = $found['data'][0] ?? $api->post('/devices', [
'identifier_type' => 'imei',
'identifier' => $line['imei'],
'manufacturer' => $line['manufacturer'], // as listed in Supported devices
'model' => $line['model'],
'fleet_id' => $fleet['id'],
'label' => $line['vehicle_label'],
'registration_plate' => $line['registration'],
], 'device-' . $line['imei']);
// 4. Request the activation: 202 + operation. Safe to retry with the same keys.
$operation = $api->post('/activations', [
'external_order_reference' => 'ORDER-' . $order['id'],
'external_request_id' => 'ORDER-' . $order['id'] . '-line-' . $line['id'] . '-activate',
'customer_id' => $customer['id'],
'fleet_id' => $fleet['id'],
'device_id' => $device['id'],
'activate_when' => 'as_soon_as_ready',
], 'order-' . $order['id'] . '-line-' . $line['id'] . '-activation');
save_line_status($line['id'], 'activation_requested', $operation['operation_id'], $operation['service_id']);
}
}fleetalyse-webhook.php — completion and failures
<?php
declare(strict_types=1);
// Point your webhook endpoint here (subscribed to service.* and operation.* events).
$raw = file_get_contents('php://input');
if (!fleetalyse_verify($raw, $_SERVER['HTTP_FLEETALYSE_SIGNATURE'] ?? '', getenv('FLEETALYSE_WEBHOOK_SECRET'))) {
http_response_code(400); // see the verification example on the Webhooks page
exit;
}
$event = json_decode($raw, true);
// At-least-once delivery: process each event id once (UNIQUE index on event_id).
if (!insert_ignore_processed_event($event['id'])) {
http_response_code(200);
exit;
}
http_response_code(204);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // answer within 10 s, then work
}
$data = $event['data'];
switch ($event['type']) {
case 'service.activation_completed':
// Charging started at effective_at; show the tracker as active to your customer.
mark_lines_active($data['external_order_reference'], $data['device_id'], $data['service_id'], $data['effective_at']);
break;
case 'service.activation_failed':
mark_line_failed($data['external_order_reference'], $data['device_id']);
notify_ops('Activation failed for ' . $data['device_id']);
break;
case 'service.deactivation_completed':
// Charging stopped at effective_at.
mark_service_stopped($data['service_id'], $data['effective_at']);
break;
case 'operation.requires_attention':
notify_ops('Fleetalyse operation needs attention: ' . $event['resource']['id']);
break;
}cancel.php — your customer cancels
<?php
declare(strict_types=1);
require __DIR__ . '/fleetalyse.php';
// Your customer cancelled their retail subscription: stop the Fleetalyse service too.
function on_subscription_cancelled(array $line): void
{
$api = new Fleetalyse(getenv('FLEETALYSE_KEY'));
$operation = $api->post('/services/' . $line['fleetalyse_service_id'] . '/deactivations', [
'effective' => 'immediate',
], 'line-' . $line['id'] . '-deactivate');
// 202: requested. Charging stops when service.deactivation_completed arrives.
save_line_status($line['id'], 'deactivation_requested', $operation['operation_id'], $line['fleetalyse_service_id']);
}Why it is safe to retry
- Every write uses an Idempotency-Key derived from your order line, so a retry after a timeout returns the original result.
- Customers and fleets are found by
external_referencebefore creating them. - The activation carries
external_request_id, which stays unique permanently (idempotency keys expire after 30 days). - The webhook handler ignores event ids it has already processed and only moves the order forward.