fradeet/hypothesis-is-sdk

Maintainers

Package info

github.com/fradeet/hypothesis-is-sdk

pkg:composer/fradeet/hypothesis-is-sdk

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.9 2026-08-22 11:10 UTC

This package is auto-updated.

Last update: 2026-08-22 11:28:33 UTC


README

A vibe-coding project.

A PHP SDK for the Hypothesis API, built on Saloon 4.x. It organizes annotation, group, membership, profile, and user endpoints into resources and uses typed payloads, queries, and DTOs for requests and responses.

For endpoint-specific examples and API behavior, see the complete usage guide.

Requirements

  • PHP 8.4 or later
  • Composer
  • A Hypothesis API token or authority client credentials, depending on the endpoint

Installation

composer require fradeet/hypothesis-is-sdk

Creating a Client

Personal API Token

Use a Bearer token for most operations performed as the current user:

<?php

use Fradeet\HypothesisIsSdk\Hypothesis;

$hypothesis = Hypothesis::withToken($_ENV['HYPOTHESIS_TOKEN']);

Authority Client Credentials

Authority-level endpoints, such as user management, use HTTP Basic Authentication:

$hypothesis = Hypothesis::withClientCredentials(
    clientId: $_ENV['HYPOTHESIS_CLIENT_ID'],
    clientSecret: $_ENV['HYPOTHESIS_CLIENT_SECRET'],
);

To send requests on behalf of a user, provide an X-Forwarded-User value:

$hypothesis = Hypothesis::withClientCredentials(
    clientId: $_ENV['HYPOTHESIS_CLIENT_ID'],
    clientSecret: $_ENV['HYPOTHESIS_CLIENT_SECRET'],
    forwardedUser: 'acct:alice@example.com',
);

Anonymous Access and Self-Hosted Instances

// Anonymous access to public endpoints
$hypothesis = new Hypothesis();

// Self-hosted Hypothesis instance
$hypothesis = Hypothesis::withToken(
    token: $_ENV['HYPOTHESIS_TOKEN'],
    baseUrl: 'https://hypothesis.example.com/api',
);

The client connects to https://hypothes.is/api by default and automatically sends the Hypothesis v1 Accept header.

Quick Start

Create and Retrieve an Annotation

use Fradeet\HypothesisIsSdk\Payloads\CreateAnnotation;

$annotation = $hypothesis->annotations()->create(new CreateAnnotation(
    uri: 'https://example.com/article',
    text: 'This is an annotation',
    tags: ['php', 'saloon'],
    group: 'group-id',
));

echo $annotation->id;
echo $annotation->text;

$sameAnnotation = $hypothesis->annotations()->get($annotation->id);

The returned value is a Fradeet\HypothesisIsSdk\Data\Annotation. Date fields are converted to DateTimeImmutable instances.

Search Annotations

use Fradeet\HypothesisIsSdk\Enums\SearchOrder;
use Fradeet\HypothesisIsSdk\Enums\SearchSort;
use Fradeet\HypothesisIsSdk\Queries\SearchAnnotations;

$result = $hypothesis->annotations()->search(new SearchAnnotations(
    limit: 50,
    sort: SearchSort::Updated,
    order: SearchOrder::Descending,
    uri: 'https://example.com/article',
    groups: ['group-a', 'group-b'],
    tags: ['php', 'sdk'],
    text: 'keyword',
));

echo $result->total;

foreach ($result->rows as $annotation) {
    echo $annotation->text;
}

SearchAnnotations also supports offset, searchAfter, url, uriParts, wildcardUri, user, tag, any, quote, and references.

Update, Delete, and Moderate Annotations

use Fradeet\HypothesisIsSdk\Enums\ModerationStatus;
use Fradeet\HypothesisIsSdk\Payloads\UpdateAnnotation;

$updated = $hypothesis->annotations()->update(
    id: $annotation->id,
    annotation: new UpdateAnnotation(
        text: 'Updated content',
        tags: ['updated'],
    ),
);

$hypothesis->annotations()->flag($annotation->id);
$hypothesis->annotations()->hide($annotation->id);
$hypothesis->annotations()->show($annotation->id);

$moderated = $hypothesis->annotations()->moderate(
    id: $annotation->id,
    status: ModerationStatus::Approved,
);

$receipt = $hypothesis->annotations()->delete($annotation->id);
echo $receipt->deleted ? 'deleted' : 'not deleted';

UpdateAnnotation only sends fields that were explicitly provided. Values such as text: '' and tags: [] are preserved, allowing existing values to be cleared.

Groups

use Fradeet\HypothesisIsSdk\Payloads\CreateGroup;
use Fradeet\HypothesisIsSdk\Payloads\UpdateGroup;
use Fradeet\HypothesisIsSdk\Queries\ListGroups;

$groups = $hypothesis->groups()->all(new ListGroups(
    authority: 'example.com',
    documentUri: 'https://example.com/article',
    expand: ['organization', 'scopes'],
));

$group = $hypothesis->groups()->create(new CreateGroup(
    name: 'SDK Group',
    description: 'Created through the SDK',
));

$group = $hypothesis->groups()->get(
    id: $group->id,
    expand: ['organization', 'scopes'],
);

$group = $hypothesis->groups()->update(
    id: $group->id,
    group: new UpdateGroup(description: 'Updated description'),
);

Retrieve annotations from a group:

use Fradeet\HypothesisIsSdk\Enums\ModerationStatus;
use Fradeet\HypothesisIsSdk\Queries\ListGroupAnnotations;

$page = $hypothesis->groups()->annotations(
    id: $group->id,
    query: new ListGroupAnnotations(
        pageSize: 50,
        moderationStatus: ModerationStatus::Approved,
    ),
);

foreach ($page->data as $annotation) {
    echo $annotation->id;
}

To retrieve the next page, pass its timestamp cursor as pageAfter: new DateTimeImmutable(...).

Group Memberships

use Fradeet\HypothesisIsSdk\Payloads\ChangeMembership;
use Fradeet\HypothesisIsSdk\Queries\ListMemberships;

$membership = $hypothesis->memberships()->add(
    groupId: $group->id,
    userId: 'acct:alice@example.com',
    membership: new ChangeMembership(roles: ['member']),
);

$membership = $hypothesis->memberships()->get(
    groupId: $group->id,
    userId: 'acct:alice@example.com',
);

$membership = $hypothesis->memberships()->update(
    groupId: $group->id,
    userId: 'acct:alice@example.com',
    membership: new ChangeMembership(roles: ['moderator']),
);

$page = $hypothesis->memberships()->all(
    groupId: $group->id,
    query: new ListMemberships(pageNumber: 1, pageSize: 25),
);

$hypothesis->memberships()->remove(
    groupId: $group->id,
    userId: 'acct:alice@example.com',
);

Without pagination parameters, all() supports the legacy Hypothesis response and returns list<Membership>. When pageNumber is provided, it returns a MembershipPage; memberships are available through $page->data, and pagination metadata through $page->meta.

Profile and User Management

$profile = $hypothesis->profile()->get();
$myGroups = $hypothesis->profile()->groups();

User management usually requires authority client credentials:

use Fradeet\HypothesisIsSdk\Payloads\CreateUser;
use Fradeet\HypothesisIsSdk\Payloads\UpdateUser;

$user = $hypothesis->users()->create(new CreateUser(
    authority: 'example.com',
    username: 'alice',
    email: 'alice@example.com',
    displayName: 'Alice',
    password: 'a-secure-password',
));

$user = $hypothesis->users()->get('acct:alice@example.com');

$user = $hypothesis->users()->update(
    username: 'alice',
    user: new UpdateUser(displayName: 'Alice Smith'),
);

Error Handling

The client uses Saloon's AlwaysThrowOnErrors trait, so non-2xx responses throw a HypothesisApiException:

use Fradeet\HypothesisIsSdk\Exceptions\HypothesisApiException;

try {
    $annotation = $hypothesis->annotations()->get('annotation-id');
} catch (HypothesisApiException $exception) {
    echo $exception->statusCode;
    echo $exception->reason ?? 'Hypothesis did not provide an error reason';

    if ($exception->mayBePermissionDenied) {
        // Hypothesis v1 may use 404 to hide inaccessible private resources.
    }

    $response = $exception->getResponse();
}

Using Saloon Directly

Hypothesis is a Saloon Connector. In addition to the resource convenience methods, you can send the SDK's request classes directly when you need access to the raw Response:

use Fradeet\HypothesisIsSdk\Requests\Annotations\GetAnnotationRequest;

$response = $hypothesis->send(new GetAnnotationRequest('annotation-id'));

echo $response->status();
echo $response->body();

$annotation = $response->dtoOrFail();

You can therefore continue to use Saloon features such as authenticators, request middleware, retries, concurrency, logging, and debugging.

Mocking the API in Tests

use Fradeet\HypothesisIsSdk\Hypothesis;
use Saloon\Http\Faking\MockClient;
use Saloon\Http\Faking\MockResponse;

$mockClient = new MockClient([
    MockResponse::make([
        'total' => 0,
        'rows' => [],
    ]),
]);

$hypothesis = Hypothesis::withToken('test-token')
    ->withMockClient($mockClient);

$result = $hypothesis->annotations()->search();

API Overview

Resource Methods
$hypothesis->annotations() create, search, get, update, delete, flag, hide, show, moderate
$hypothesis->groups() all, create, get, update, annotations
$hypothesis->memberships() all, get, add, update, remove
$hypothesis->profile() get, groups
$hypothesis->users() create, get, update

PHP named arguments are recommended for payload and query constructors. List and search methods with optional queries provide defaults, so they can also be called directly:

$result = $hypothesis->annotations()->search();
$groups = $hypothesis->groups()->all();
$memberships = $hypothesis->memberships()->all('group-id');

Development

composer install
vendor/bin/pest
vendor/bin/phpstan analyse --no-progress

License

MIT