PHPackages                             clockster/sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. clockster/sdk

ActiveLibrary[API Development](/categories/api)

clockster/sdk
=============

Official PHP SDK for the Clockster Company API

v0.1.0(yesterday)01↓50%MITPHPPHP ^8.3CI passing

Since Aug 16Pushed yesterdayCompare

[ Source](https://github.com/clockster/sdk-php)[ Packagist](https://packagist.org/packages/clockster/sdk)[ Docs](https://github.com/clockster/sdk-php)[ RSS](/packages/clockster-sdk/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (4)Used By (0)

sdk-php
=======

[](#sdk-php)

Official PHP SDK for the [Clockster Company API](https://api.clockster.com/openapi/v3.json).

Server-to-server client for a company's employees, structure, schedules, attendance, tasks and documents. Generated from the API's OpenAPI document. No dependencies beyond curl and JSON.

```
composer require clockster/sdk
```

Requires PHP 8.3 or newer.

Quickstart
----------

[](#quickstart)

One token authenticates one company. Create it under Settings → API in the web application.

```
use Clockster\Client;

$clockster = new Client(getenv('CLOCKSTER_TOKEN'));

$me = $clockster->me();

$locations = $clockster->locations->upsert([
    'items' => [['external_id' => 'HQ', 'title' => 'Head office']],
]);

$clockster->users->upsert([
    'users' => [[
        'external_id' => 'HR-1',
        'first_name' => 'Aisulu',
        'role' => 'employee',
        'location_id' => $locations['data'][0]['id'],
    ]],
]);

$timesheets = $clockster->timesheets->list(dateFrom: '2026-08-01', dateTo: '2026-08-31');
```

A method answers the parsed body, so rows are `$answer['data']`. Nothing is validated on the way in: the answer is the JSON as it arrived, and a field we add tomorrow reaches your code today.

Filters are named arguments, in the order the documentation lists them — pass the ones you want:

```
$clockster->users->list(perPage: 100, status: 'active', include: ['location', 'department']);
```

The methods are named after the operations, so the API documentation is the reference for both: `GET /users` is `$clockster->users->list(...)`, `POST /users/upsert` is `$clockster->users->upsert(...)`. The TypeScript, Python and Go clients use the same names.

Absent, null and set
--------------------

[](#absent-null-and-set)

A key that was not asked for is absent, never null: `null` means the value is known to be empty, and an absent key means you did not ask. Arrays say both without ceremony:

```
$clockster->users->upsert(['users' => [[
    'external_id' => 'HR-1',
    'first_name' => 'Aisulu',
    'role' => 'employee',
    'location_id' => 3,
    'position_id' => null,   // clears the stored position
    // department_id is not there at all, so the stored department stays.
]]]);
```

The same holds when reading: `array_key_exists('department', $user)` asks whether you requested it with `include`, where `$user['department'] === null` says the employee has none.

Types
-----

[](#types)

Every request body, query and answer is described by a PHPStan array shape in `Clockster\Generated\Shapes`, and the generated methods carry them:

```
/** @return UsersListResponse */
public function list(?int $perPage = null, /* … */): array
```

Run PHPStan or Psalm and a misspelled key or a missing required one is an error before it is a `422`. Without a static analyser they are documentation, and your editor still reads them for completion. Nothing is enforced at run time: the shapes describe what the document says, not something this package confirmed.

Paging
------

[](#paging)

Thirteen listings page on a cursor, and each has a `…All()` beside it that walks them, yielding one row at a time:

```
foreach ($clockster->users->listAll(perPage: 100, include: ['location']) as $user) {
    echo $user['external_id'] ?? $user['id'], PHP_EOL;
}
```

It is a Generator, so a page is fetched only when the loop asks for the next row, and leaving the loop stops the walk. A refused page is thrown where it was refused, so half a listing is never mistaken for the whole of one. A cursor belongs to the filters it was issued under: change them and walk again.

Refusals
--------

[](#refusals)

A refusal is thrown, never returned. `code()` is what to branch on: it names the reason and does not change, where `getMessage()` is prose and may. `requestId()` identifies the call in our logs.

```
use Clockster\Exception\ApiException;
use Clockster\Exception\RateLimitException;
use Clockster\Exception\ValidationException;

try {
    $clockster->users->upsert(['users' => $people]);
} catch (ValidationException $refused) {
    report($refused->code(), $refused->errors(), $refused->requestId());
} catch (RateLimitException $refused) {
    sleep($refused->retryAfter() ?? 60);
} catch (ApiException $refused) {
    report($refused->status(), $refused->code());
}
```

One class per status worth catching: `AuthenticationException` (401), `ForbiddenException` (403), `NotFoundException` (404), `ConflictException` (409), `ValidationException` (422), `RateLimitException` (429) and `ServerException` (5xx). Another company's id answers `NotFoundException` rather than `ForbiddenException` — you cannot learn that it exists.

A call that got no answer at all is a `TransportException` instead, and it is the one case worth retrying blind: the request may have been applied.

Retries and idempotency
-----------------------

[](#retries-and-idempotency)

Retry a 429 and a 5xx; do not retry a 4xx. A keyed write converges on what you meant rather than doubling anything, so a timed-out upsert is safe to send again. Four writes have no key of your own to match a second attempt against — a rota, a webhook endpoint, a rotated secret, a delivery sent again — and those take a key instead:

```
$clockster->schedules->create($body, idempotencyKey: $attempt);
```

Uploading a file
----------------

[](#uploading-a-file)

One operation carries bytes rather than JSON:

```
$stored = $clockster->files->upload(
    file: (string) file_get_contents('agreement.pdf'),
    filename: 'agreement.pdf',
    name: 'agreement',
);
```

Webhooks
--------

[](#webhooks)

Verifying a delivery is the only way to the event it carries, so there is no path that acts on one that was not verified:

```
use Clockster\Exception\WebhookVerificationException;
use Clockster\Webhooks;

try {
    $event = Webhooks::verifyGlobals(getenv('CLOCKSTER_WEBHOOK_SECRET'));
} catch (WebhookVerificationException $refused) {
    http_response_code(400);

    exit;
}

http_response_code(200);
```

In a framework, hand the raw body and the two headers to `Webhooks::verify()` — Laravel's `$request->getContent()` rather than anything re-encoded, since re-serialising a parsed object does not reproduce the signed bytes. Answer 2xx quickly and do the work afterwards — a timeout is retried — and deduplicate on `$event['id']`, since the same event may arrive twice.

Another HTTP client
-------------------

[](#another-http-client)

Calls go out over curl unless you say otherwise. Anything implementing `Clockster\Http\Transport`is accepted, and `Psr18Transport` is one for PSR-18 clients:

```
use Clockster\Http\Psr18Transport;

$clockster = new Client($token, transport: new Psr18Transport(
    $guzzle,          // Psr\Http\Client\ClientInterface
    $requestFactory,  // Psr\Http\Message\RequestFactoryInterface
    $streamFactory,   // Psr\Http\Message\StreamFactoryInterface
));
```

That class is the only thing here that needs anything installed — `psr/http-client` and `psr/http-factory` — and nothing loads it unless you name it.

Configuration
-------------

[](#configuration)

```
$clockster = new Client(
    token: getenv('CLOCKSTER_TOKEN'),
    baseUrl: 'https://demo.clockster.com',  // a demo stand instead of production
    timeout: 60.0,                          // seconds, applied to each request
    userAgent: 'acme-hr/1.4',               // names your integration in our request log
);
```

Requests carry `clockster-php/` unless `userAgent` says otherwise. The token is read per request, so rotating it does not require a new client.

Dates, times and numbers
------------------------

[](#dates-times-and-numbers)

Instants, dates and clock times are strings, in the shapes the document states, rather than `DateTimeImmutable`: a date carries no zone and a clock time is read in the timezone stated beside it, and converting either would decide something this package does not know. Durations are seconds. Decimal amounts are JSON numbers rounded to two places; do not accumulate them in binary floating point.

Generated from the document
---------------------------

[](#generated-from-the-document)

`src/Generated` is written by `scripts/generate.php` from `openapi/company-v3.json`, and committed — an API change appears in review as the lines of the client it moves. To refresh:

```
composer spec generate check
```

A nightly job compares the committed document with the published one, so drift is noticed here rather than by you.

Examples
--------

[](#examples)

Two whole integrations live in [examples](examples): a roster sync in, a timesheet export out.

Versions
--------

[](#versions)

This package follows its own semver, unrelated to the version of the API and to the other SDKs. A new API version would be a major release of this package rather than a second package.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/aa434fb35979c99ea4db20de86e6e5f343d2e1427b3751b46f790b3de06f6966?d=identicon)[apervikov](/maintainers/apervikov)

---

Top Contributors

[![apervikov](https://avatars.githubusercontent.com/u/54171774?v=4)](https://github.com/apervikov "apervikov (5 commits)")

---

Tags

api-clientattendanceclocksterhropenapipayrollphpsdkworkforce-managementsdkopenapihrapi clientattendancepayrollworkforce managementclockster

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/clockster-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/clockster-sdk/health.svg)](https://phpackages.com/packages/clockster-sdk)
```

###  Alternatives

[crowdin/crowdin-api-client

PHP client library for Crowdin API v2

611.7M5](/packages/crowdin-crowdin-api-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
