Search by

seventhings / customer-api-php

hra42

PHP SDK for the seventhings API

Package info

github.com/seventhingsCompany/customer-api-php

pkg:composer/seventhings/customer-api-php

Statistics

Installs: 24

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.5.0 2026-10-17 01:12 UTC

This package is auto-updated.

Last update: 2026-10-17 01:15:43 UTC


README

PHP SDK for the seventhings asset tracking API.

Installation

Add the repository to your composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@github.com:SeventhingsCompany/customer-api-php.git"
        }
    ]
}

Then install:

composer require seventhings/customer-api-php

Requirements: PHP ^8.5, Guzzle 7 (only runtime dependency).

Quick Start

use Seventhings\Client;

// Authenticate with credentials (recommended)
$client = Client::withCredentials($baseUrl, $username, $password, $clientId);

// Or use an existing token
$client = Client::withToken($baseUrl, $accessToken);

Rate Limiting

Clients default to 200 requests per minute, evenly spaced (one every 300 ms) without bursts. The first request is immediate and idle time does not accumulate into a burst. Authentication, uploads, and downloads share the same per-client limit; separate client instances have independent limits. Each factory accepts the limit as its last argument:

// Override the limit (requests per minute)
$client = Client::withToken($baseUrl, $accessToken, requestsPerMinute: 100);

// Disable throttling
$client = Client::withCredentials($baseUrl, $username, $password, $clientId, requestsPerMinute: 0);

Throttling blocks the calling PHP process (usleep) and does not automatically retry API rate-limit errors (HTTP 429); see Error Handling for retryAfter().

Usage by Module

Auth

// Health check (unauthenticated)
$client->auth->ping();

// Login
$token = $client->auth->login($username, $password, $clientId);

// Refresh token (uses stored clientId from login)
$refreshed = $client->auth->refresh($token->refreshToken);

// Revoke all tokens
$client->auth->revokeTokens();

Objects

use Seventhings\Models\ListOptions;
use Seventhings\Models\FilterEntry;
use Seventhings\Models\Enums\FilterOperator;
use Seventhings\Models\FileAttachment;

// List objects (one page of raw arrays)
$objects = $client->objects->list();

// Iterate every object across all pages (lazy; default page size 100).
// Each item is a type-safe Fields wrapper over the instance-defined body.
foreach ($client->objects->all() as $obj) {
    echo $obj->uuid(), ' ', $obj->string('inventory_name'), "\n";
    $price = $obj->float('purchasing_price');   // null if absent or non-numeric
}

// Create (field names are instance-specific)
$uuid = $client->objects->create([
    'inventory_name' => 'Laptop #42',
    'barcode' => 'INV-0042',
]);

// Get, patch, delete
$object = $client->objects->get($uuid);

// Look up by scancode, including archived objects. Pass the raw barcode;
// the SDK URL-encodes it as a single path segment.
$object = $client->objects->getByBarcode('INV/0042');

$client->objects->patch($uuid, ['inventory_name' => 'Laptop #43']);
$client->objects->delete($uuid);

// Count
$count = $client->objects->count();

// Archive / unarchive
$client->objects->archive($uuid);
$client->objects->unarchive($uuid);

// File attachments
$client->objects->addFiles($uuid, [new FileAttachment('documents', $fileUuid)]);
$client->objects->removeFiles($uuid, [new FileAttachment('documents', $fileUuid)]);

Rooms

$rooms = $client->rooms->list();
$uuid = $client->rooms->create(['name' => 'Server Room', ...]);
$room = $client->rooms->get($uuid);
$client->rooms->patch($uuid, ['name' => 'Server Room A']);
$count = $client->rooms->count();
$client->rooms->delete($uuid);

Locations

$locations = $client->locations->list();
$uuid = $client->locations->create(['name' => 'Building A']);
$location = $client->locations->get($uuid);
$client->locations->patch($uuid, ['name' => 'Building B']);
$count = $client->locations->count();
$client->locations->delete($uuid);

Room and location list(), get(), and patch() return the API's flat field maps unchanged, including custom fields.

Users

use Seventhings\Models\UserListOptions;

$result = $client->users->list();
// $result->items, $result->total, $result->page, etc.

$user = $client->users->get($uuid);
$user = $client->users->getById($id);

Persons

use Seventhings\Models\PersonListOptions;
use Seventhings\Models\FilterObject;
use Seventhings\Models\Enums\SortDirection;

$result = $client->persons->list();
// $result->items, $result->total, $result->page, etc.

// Sorting uses the deep-object sort[field]=DIR format (a field => direction
// map), not sort_by/order. $result->sort echoes the applied sort back.
$result = $client->persons->list(new PersonListOptions(
    perPage: 25,
    sort: ['id' => SortDirection::Desc],
));

// Each PersonResponse exposes typed convenience props (uuid, email, firstname, …).
// Person fields are template-defined, so any custom field beyond those props is
// still available via the full raw map: $person->fields['custom_key'] or the
// null-safe $person->field('custom_key').
// Responses are flat field maps; ->uuid is read from person_uuid.

$count = $client->persons->count();

$person = $client->persons->get($uuid);
$person = $client->persons->getById($id);

// Create — returns the new person UUID.
// Field keys are template-defined (commonly first_name / last_name).
$uuid = $client->persons->create([
    'email' => 'max@example.com',
    'first_name' => 'Max',
]);

// Update fields — the API returns no body, so re-fetch to read the result.
$client->persons->patch($uuid, ['last_name' => 'Mustermann']);
$person = $client->persons->get($uuid);

$client->persons->delete($uuid);

// Create a platform user from the person matched by a filter
$client->persons->createUser(new FilterObject(
    filter: ['email' => ['eq' => 'max@example.com']],
));

Files

// Upload a file
$fileUuid = $client->files->upload('report.pdf', $fileContents);

// List all files
$files = $client->files->list();

// Get metadata
$meta = $client->files->get($fileUuid);

// Download file data
$data = $client->files->getData($fileUuid);

// Download thumbnail
$thumbnail = $client->files->getThumbnail($fileUuid);

// Thumbnail fitted within 320x240 (aspect ratio preserved). Both dimensions
// must be 1–2000; the API caches new sizes and may reject excess sizes (422).
$small = $client->files->getThumbnail($fileUuid, 320, 240);

Tasks

use Seventhings\Models\CreateTaskRequest;
use Seventhings\Models\UpdateTaskRequest;
use Seventhings\Models\TaskListOptions;
use Seventhings\Models\TaskReferenceInput;
use Seventhings\Models\TimeInterval;
use Seventhings\Models\Enums\TaskStatus;
use Seventhings\Models\Enums\TaskReferenceType;
use Seventhings\Models\Enums\TimeIntervalUnit;

// Create (requires at least one reference and assignee)
$uuid = $client->tasks->create(new CreateTaskRequest(
    title: 'Review inventory',
    deadline: '2026-12-31',
    assignees: [$userUuid],
    references: [new TaskReferenceInput(TaskReferenceType::Asset, $objectUuid)],
    reminders: [new TimeInterval(TimeIntervalUnit::Days, 1)],
    recurringSchedule: null,
));

// Get, update, delete
$task = $client->tasks->get($uuid);
$client->tasks->update($uuid, new UpdateTaskRequest(
    title: 'Updated title',
    deadline: '2026-12-31',
    assignees: [$userUuid],
    references: [new TaskReferenceInput(TaskReferenceType::Asset, $objectUuid)],
    reminders: [new TimeInterval(TimeIntervalUnit::Days, 1)],
    recurringSchedule: null,
));
$client->tasks->delete($uuid);

// Change status
$client->tasks->updateStatus($uuid, TaskStatus::Closed);

// List with filters
$tasks = $client->tasks->list(new TaskListOptions(status: TaskStatus::Open));

reminders: null omits the field, [] explicitly disables reminders, and a zero interval (new TimeInterval(TimeIntervalUnit::Days, 0)) means "on the deadline".

Rentals

use Seventhings\Models\CreateRentalCaseRequest;
use Seventhings\Models\UpdateRentalCaseRequest;
use Seventhings\Models\RentalCaseReferenceInput;
use Seventhings\Models\RentalCaseRenter;
use Seventhings\Models\Enums\RentalCaseReferenceType;
use Seventhings\Models\Enums\RenterType;

$rentals = $client->rentals->list();

// Full-day rental (default when the kind and times are omitted)
$uuid = $client->rentals->create(new CreateRentalCaseRequest(
    title: 'Team event',
    issueDate: '2026-11-01',
    dueDate: '2026-11-03',
    comment: '',
    responsibleUserUuid: $userUuid,
    renter: new RentalCaseRenter(RenterType::Plain, 'Jane Doe'),
    references: [new RentalCaseReferenceInput(RentalCaseReferenceType::Asset, $objectUuid)],
    attachments: [],
));

// Timed rental: any offset is accepted and stored as UTC
$timed = $client->rentals->create(new CreateRentalCaseRequest(
    title: 'Projector',
    issueDate: '2026-11-01',
    dueDate: '2026-11-01',
    comment: '',
    responsibleUserUuid: $userUuid,
    renter: new RentalCaseRenter(RenterType::Plain, 'Jane Doe'),
    references: [new RentalCaseReferenceInput(RentalCaseReferenceType::Asset, $objectUuid)],
    attachments: [],
    isFullDay: false,
    issueAt: '2026-11-01T10:00:00+02:00',
    dueAt: '2026-11-01T12:00:00+02:00',
));

$rental = $client->rentals->get($timed);
$rental->isFullDay; // false
$rental->issueAt;   // "2026-11-01T08:00:00+00:00"

// update() is a full replacement (PUT): send the complete state, not a patch
$client->rentals->update($uuid, new UpdateRentalCaseRequest(/* same fields as create */));
$client->rentals->delete($uuid);

A renter is required by the SDK types: although the API description marks it optional, the current server fails when it is omitted. On update, leaving isFullDay, issueAt and dueAt null keeps the stored kind and moves its times to the given dates.

Catalog Items

Requires the product catalog feature. Object field keys carry a catalog_object_ prefix; catalog metadata such as id does not.

use Seventhings\Models\FilterEntry;
use Seventhings\Models\ListOptions;

$uuid = $client->catalogItems->create(['catalog_object_inventory_name' => 'Office chair']);
$item = $client->catalogItems->get($uuid);   // CatalogItem { uuid, fields: Fields }
$item->fields->string('catalog_object_inventory_name');

$page = $client->catalogItems->list(new ListOptions(
    perPage: 25,
    filters: [FilterEntry::like('catalog_object_inventory_name', 'chair')],
));                                          // items, page, perPage, total, sort
foreach ($client->catalogItems->all() as $item) { /* ... */ }

// PUT replaces all catalog fields; missing fields are emptied. Objects
// previously created from this item are not changed.
$client->catalogItems->update($uuid, ['catalog_object_inventory_name' => 'Desk chair'] + $item->fields->data);

// Deleting leaves objects created from the item intact.
$client->catalogItems->delete($uuid);

Resource History

use Seventhings\Models\HistoryListOptions;

$options = new HistoryListOptions(page: 1, perPage: 25);
$history = $client->objects->history($objectUuid, $options);
// $history->items, ->page, ->perPage, ->total; newest changes first.

foreach ($history->items as $event) {
    echo $event['type']; // asset, task, rental_case, or object_merge
    // Dynamic maps retain properties and merge-specific payloads such as
    // user_id and absorbedObjectData.
}

// Other resources return typed history entries.
$history = $client->rooms->history($roomUuid, $options);
foreach ($history->items as $event) {
    echo $event->roomUuid, ' ', $event->occurredAt, ' ', $event->eventName;
    // Also: ->userUuid, ->description, ->details.
    // details remains a JSON-encoded snapshot string (or an empty string).
}

$client->locations->history($locationUuid, $options); // LocationHistoryEntry
$client->persons->history($personUuid, $options);    // PersonHistoryEntry
$client->tasks->history($taskUuid, $options);        // TaskHistoryEntry
$client->rentals->history($rentalUuid, $options);    // RentalCaseHistoryEntry

Omitting options uses the API defaults: page 1, 50 entries per page. The API caps history pages at 200 entries. Use HistoryListOptions to request subsequent pages; resources without recorded changes return empty items. History does not accept list filters or sorting options.

PDF Reports

use Seventhings\Models\CreateReportRequest;

$templates = $client->reports->listTemplates(); // ReportTemplate[] (uuid, name)

if ($templates !== []) {
    $pdf = $client->reports->create(new CreateReportRequest(
        reportTemplateUuid: $templates[0]->uuid,
        objectUuids: [$objectUuid1, $objectUuid2], // At least one; order is preserved.
    ));
    file_put_contents('inventory.pdf', $pdf);
}

create() returns a binary PDF string. Each call generates a new document; the API does not store it. Failed requests throw the usual ApiException.

Field Definitions

use Seventhings\Models\Enums\AssetTrackingTemplate;
use Seventhings\Models\CreateFieldDefinition;
use Seventhings\Models\FieldDefinitionFieldType;
use Seventhings\Models\FieldValueConstraint;
use Seventhings\Models\FieldAttribute;
use Seventhings\Models\Enums\FieldTypeName;

// List field definitions for a template
// (AssetTrackingTemplate::Asset, ::Room, or ::Person)
$fields = $client->fieldDefinitions->list(AssetTrackingTemplate::Asset);

// Inspect a definition: is it required for this instance, and (for a
// DROPDOWN etc.) what values are allowed?
$fields[0]->isMandatory();                   // bool
$fields[0]->attribute('mandatory');          // raw attribute value or null
$fields[0]->fieldType->allowedValues();      // list<mixed> or null

// Discover instance-required fields before creating a resource. System-managed
// keys (id, uuid, timestamps, ...) are excluded automatically.
$required = $client->fieldDefinitions->mandatoryFieldDefinitions(AssetTrackingTemplate::Person);

// Fail fast: which required keys are missing from a create payload?
$missing = $client->fieldDefinitions->missingMandatoryFields(
    AssetTrackingTemplate::Person,
    ['email' => 'max@example.com'],
);
if ($missing !== []) {
    throw new \InvalidArgumentException('missing required fields: ' . implode(', ', $missing));
}

// Get a specific field definition
$field = $client->fieldDefinitions->get(AssetTrackingTemplate::Asset, $uuid);

// Create a custom field
$uuid = $client->fieldDefinitions->create(AssetTrackingTemplate::Asset, new CreateFieldDefinition(
    fieldType: new FieldDefinitionFieldType(FieldTypeName::Text, [
        new FieldValueConstraint('max_length', 255),
    ]),
    label: 'Serial Number',
    attributes: [
        new FieldAttribute('mandatory', 'no'),
        new FieldAttribute('mutable', 'yes'),
        new FieldAttribute('internal', 'no'),
    ],
    relations: [],
));

CircularityHub

use Seventhings\Models\FilterObject;
use Seventhings\Models\AddObjectEntry;

// Suggest category
$suggestion = $client->circularityHub->suggestCategory(new FilterObject(filter: ['name' => 'Monitor']));

// Items
$items = $client->circularityHub->listItems();
$item = $client->circularityHub->getItem($id);
$client->circularityHub->updateItem($id, ['price' => '25.00']);
$client->circularityHub->deleteItem($id);

// Orders
$orders = $client->circularityHub->listOrders();
$orderId = $client->circularityHub->createOrder([$itemId1, $itemId2]);
$order = $client->circularityHub->getOrder($orderId);

// Add objects to CircularityHub
$client->circularityHub->addObjects([
    $objectUuid => new AddObjectEntry(category: 'Electronics', price: '50.00'),
]);

Error Handling

use Seventhings\Models\ApiException;
use Seventhings\Models\NetworkException;

try {
    $client->objects->get($uuid);
} catch (ApiException $e) {
    echo $e->statusCode; // e.g. 404
    echo $e->status;     // e.g. "Not Found"
    echo $e->body;       // raw response body
    $e->isStatusCode(404); // true

    // Convenience predicates for common statuses:
    if ($e->isNotFound()) { /* 404 */ }
    // also: isUnauthorized() 401, isForbidden() 403, isConflict() 409,
    //       isRateLimited() 429, isServerError() 5xx
    // isFeatureInactive() recognizes the API's explicit inactive-feature 403
    // response; ordinary permission denials return false.
} catch (NetworkException $e) {
    echo $e->getMessage(); // connection error details
}

ApiException::$headers holds the response headers (not included in the message); header($name) returns all values case-insensitively. Use retryAfter() to read Retry-After as delay-seconds or an HTTP date:

if ($e->isRateLimited()) {
    $delay = $e->retryAfter(); // ?int seconds; pass a DateTimeInterface as "now" for tests
    if ($delay !== null) {
        // Requeue the operation no earlier than $delay seconds from now.
    } else {
        // Missing or invalid header: apply your own bounded backoff.
    }
}

HTTP dates are rounded up to whole seconds and past dates return 0. Missing, repeated, invalid, or overflowing values return null. The helper also works on other errors with this header (e.g. HTTP 503). It does not sleep or retry; the SDK sends each operation once.

ListOptions & Filtering

Use ListOptions for paginated, filtered, and sorted requests on Objects, Rooms, Locations, Rentals, and Catalog Items.

use Seventhings\Models\ListOptions;
use Seventhings\Models\FilterEntry;
use Seventhings\Models\Enums\FilterOperator;
use Seventhings\Models\Enums\SortDirection;

$options = new ListOptions(
    page: 1,
    perPage: 25,
    sort: ['updated_at' => SortDirection::Desc],
    filters: [
        // Static constructors avoid spelling out the operator enum:
        FilterEntry::like('inventory_name', 'Laptop'),
        FilterEntry::in('status', 'active', 'reserved'),
        // equivalent to: new FilterEntry('inventory_name', FilterOperator::Like, ['Laptop'])
    ],
);

$objects = $client->objects->list($options);
$count = $client->objects->count($options);

// Or iterate every matching object across all pages:
foreach ($client->objects->all($options) as $obj) { /* ... */ }

all() is available on objects, rooms, locations (yielding Fields), and on persons / users / catalogItems (yielding their typed response objects).

Tasks and Users have their own option classes:

// Tasks
$tasks = $client->tasks->list(new TaskListOptions(
    status: TaskStatus::Open,
    assignee: $userUuid,
));

// Users
$users = $client->users->list(new UserListOptions(
    page: 1,
    perPage: 50,
    sortBy: UserSortBy::Email,
    order: UserSortOrder::Asc,
));

Scope & Limitations

  • No auto-refresh — tokens are not automatically refreshed; call $client->auth->refresh() and $client->setToken() manually.
  • No retry logic — failed requests (including HTTP 429) are not retried automatically; use ApiException::retryAfter() to schedule your own retry.
  • Client-side throttling — 200 requests/minute per client by default; see Rate Limiting.
  • No caching — all calls hit the API directly.
  • CircularityHub uses integer IDs, not UUIDs.
  • History pagination is manual — use HistoryListOptions; list iterators are described above.

Running Tests

# Unit tests (default)
composer test

# Integration tests (requires API credentials)
export SEVENTHINGS_BASE_URL=https://your-instance.7things.de
export SEVENTHINGS_USERNAME=your-user
export SEVENTHINGS_PASSWORD=your-password
export SEVENTHINGS_CLIENT_ID=your-client-id
composer test:integration

License

MIT