captur/events

PHP SDK for the captur.events API

1.0.0 2026-08-02 20:35 UTC

This package is auto-updated.

Last update: 2026-08-02 20:38:15 UTC


README

Official-style PHP client for the captur.events event ingestion API.

Base URL: https://captur.events · Default version: v1
Docs: Introduction · OpenAPI

Install

composer require captur/events

Requires PHP 8.3+.

Authentication

Create a project in the dashboard and copy a test (evt_test_…) or live (evt_live_…) API key.

export CAPTUR_API_KEY=evt_test_...

Every request sends Authorization: Bearer <key>.

Quick start

use Captur\Client;

$client = new Client(apiKey: getenv('CAPTUR_API_KEY'));

// 1. Register an event type (once per project)
$client->eventTypes()->create([
    'name' => 'vehicle.entered',
    'description' => 'Vehicle entered a car park',
]);

// 2. Ingest an event
$event = $client->events()->create([
    'type' => 'vehicle.entered',
    'stream' => 'vehicle:AB12XYZ',
    'data' => [
        'car_park_id' => 'teeside-central',
        'barrier' => 'north',
    ],
]);

echo $event['id'];

Optional client options:

$client = new Client(
    apiKey: getenv('CAPTUR_API_KEY'),
    baseUrl: 'https://captur.events', // default host
    version: 'v1',                    // default API version
);

Events

Single ingest

$event = $client->events()->create([
    'type' => 'vehicle.entered',
    'stream' => 'vehicle:AB12XYZ',
    'occurred_at' => '2026-08-01T08:45:00Z',
    'idempotency_key' => 'entry-AB12XYZ-2026-08-01T08:45',
    'data' => ['car_park_id' => 'teeside-central'],
]);

Batch ingest

POST a JSON array (max 500). Response shape: { "events": [ … ] }.

$result = $client->events()->createMany([
    ['type' => 'vehicle.entered', 'stream' => 'vehicle:AB12XYZ', 'data' => []],
    ['type' => 'vehicle.exited', 'stream' => 'vehicle:AB12XYZ', 'data' => []],
]);

List, get, delete

$client->events()->list([
    'type' => 'vehicle.entered',
    'stream' => 'vehicle:AB12XYZ',
    'from' => '2026-08-01T00:00:00Z',
    'to' => '2026-08-02T00:00:00Z',
]);

$client->events()->get('01KYY7S1ZGJ1DF8XG4X1RFHQ1S');
$client->events()->delete('01KYY7S1ZGJ1DF8XG4X1RFHQ1S');
$client->events()->deleteMany(['01A…', '01B…']);

Pagination (list events)

Use either offset (page) or cursor (cursor / starting_after) — not both.

Offset pagination (page + limit):

$page = 1;

do {
    $result = $client->events()->list([
        'type' => 'vehicle.entered',
        'limit' => 100,
        'page' => $page,
    ]);

    foreach ($result['data'] as $event) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor pagination (preferred for large histories / continuous sync):

$cursor = null;

do {
    $query = ['limit' => 100];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->events()->list($query);

    foreach ($result['data'] as $event) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Or continue after a known event id:

$result = $client->events()->list([
    'limit' => 100,
    'starting_after' => '01J9X2K8M3N4P5Q6R7S8T9V0W1',
]);

Event types

$client->eventTypes()->list();
$client->eventTypes()->get('vehicle.entered');
$client->eventTypes()->update('vehicle.entered', [
    'description' => 'Updated description',
]);
$client->eventTypes()->delete('vehicle.entered');

Pagination (list event types)

Offset:

$page = 1;

do {
    $result = $client->eventTypes()->list([
        'limit' => 50,
        'page' => $page,
    ]);

    foreach ($result['data'] as $eventType) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = ['limit' => 50];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->eventTypes()->list($query);

    foreach ($result['data'] as $eventType) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Streams

$client->streams()->get('vehicle:AB12XYZ');
$client->streams()->delete('vehicle:AB12XYZ');

Pagination (read stream)

Stream reads return events under events (not data).

Offset:

$page = 1;

do {
    $result = $client->streams()->get('vehicle:AB12XYZ', [
        'limit' => 100,
        'page' => $page,
    ]);

    foreach ($result['events'] as $event) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = ['limit' => 100];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->streams()->get('vehicle:AB12XYZ', $query);

    foreach ($result['events'] as $event) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Or continue after a known event id:

$result = $client->streams()->get('vehicle:AB12XYZ', [
    'limit' => 100,
    'starting_after' => '01J9X2K8M3N4P5Q6R7S8T9V0W1',
]);

Session reports

$client->reports()->sessions([
    'start_type' => 'session.started',
    'end_type' => 'session.ended',
    'from' => '2026-08-01T00:00:00Z',
    'to' => '2026-08-02T00:00:00Z',
]);

Pagination (session reports)

Offset:

$page = 1;

do {
    $result = $client->reports()->sessions([
        'start_type' => 'session.started',
        'end_type' => 'session.ended',
        'from' => '2026-08-01T00:00:00Z',
        'to' => '2026-08-02T00:00:00Z',
        'limit' => 50,
        'page' => $page,
    ]);

    foreach ($result['data'] as $session) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = [
        'start_type' => 'session.started',
        'end_type' => 'session.ended',
        'from' => '2026-08-01T00:00:00Z',
        'to' => '2026-08-02T00:00:00Z',
        'limit' => 50,
    ];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->reports()->sessions($query);

    foreach ($result['data'] as $session) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Webhooks

$client->webhooks()->create([
    'url' => 'https://example.com/hooks/captur',
    'event_types' => ['vehicle.entered'],
]);

$client->webhooks()->list();
$client->webhooks()->get('wh_…');
$client->webhooks()->update('wh_…', ['enabled' => false]);
$client->webhooks()->delete('wh_…');

Test inbox helpers: testInbox(), upsertTestInbox(), deleteTestInbox(), sendTestInboxSample().

Pagination (list webhooks)

Offset:

$page = 1;

do {
    $result = $client->webhooks()->list([
        'limit' => 50,
        'page' => $page,
    ]);

    foreach ($result['data'] as $webhook) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = ['limit' => 50];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->webhooks()->list($query);

    foreach ($result['data'] as $webhook) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Alerts

$client->alerts()->list();
$client->alerts()->create(['name' => 'High dwell']);
$client->alerts()->get('al_…');
$client->alerts()->update('al_…', ['name' => 'Updated']);
$client->alerts()->delete('al_…');

Pagination (list alerts)

Offset:

$page = 1;

do {
    $result = $client->alerts()->list([
        'limit' => 50,
        'page' => $page,
    ]);

    foreach ($result['data'] as $alert) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = ['limit' => 50];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->alerts()->list($query);

    foreach ($result['data'] as $alert) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Projections

$client->projections()->list();
$client->projections()->create(['name' => 'occupancy']);
$client->projections()->get('pr_…');
$client->projections()->update('pr_…', ['name' => 'Updated']);
$client->projections()->delete('pr_…');
$client->projections()->replay('pr_…');

Pagination (list projections)

Offset:

$page = 1;

do {
    $result = $client->projections()->list([
        'limit' => 50,
        'page' => $page,
    ]);

    foreach ($result['data'] as $projection) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $page++;
} while ($hasMore);

Cursor:

$cursor = null;

do {
    $query = ['limit' => 50];
    if ($cursor !== null) {
        $query['cursor'] = $cursor;
    }

    $result = $client->projections()->list($query);

    foreach ($result['data'] as $projection) {
        // …
    }

    $hasMore = $result['meta']['has_more'] ?? false;
    $cursor = $result['meta']['next_cursor'] ?? null;
} while ($hasMore && $cursor !== null);

Verify webhook signatures

captur signs deliveries with HMAC-SHA256 over the exact raw body (X-Captur-Signature).

use Captur\Webhook;

$payload = $request->getContent(); // raw body — required
$signature = $request->header('X-Captur-Signature');
$secret = getenv('CAPTUR_WEBHOOK_SECRET');

if (! Webhook::verify($payload, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);

Errors

Non-2xx responses throw typed exceptions under Captur\Exceptions\:

Status Exception
401 AuthenticationException
403 AuthorizationException
404 NotFoundException
409 ConflictException
422 ValidationException
429 RateLimitException
other ApiException
use Captur\Exceptions\CapturException;

try {
    $client->events()->create([/* … */]);
} catch (CapturException $e) {
    echo $e->getMessage(); // human message
    echo $e->code();       // e.g. invalid_api_key
    echo $e->type();       // e.g. authentication_error
    echo $e->status();     // HTTP status
}

Development

composer install
composer tests
composer test:coverage   # requires pcov or xdebug; enforces 100%
composer standards:check

Docker:

docker compose build
docker compose up -d
docker exec php-composer-package composer install
docker exec php-composer-package composer tests

CI runs on push/PR to main: standards check plus tests with a 100% coverage gate.

Links