hyvor/sdk

Official PHP SDK for HYVOR products

Maintainers

Package info

github.com/hyvor/sdk-php

pkg:composer/hyvor/sdk

Transparency log

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

0.0.2 2026-07-18 16:05 UTC

This package is auto-updated.

Last update: 2026-07-18 16:08:01 UTC


README

Official PHP SDK for HYVOR Products.

Install

composer require hyvor/sdk

Requires PHP >= 8.4. The SDK talks HTTP through PSR-18 / PSR-17 and does not ship its own HTTP client. If your project already has one installed (Guzzle, Symfony HttpClient, Nyholm, etc.), it's discovered automatically via php-http/discovery - no extra wiring needed.

Usage

use Hyvor\Sdk\HyvorClient;
use Hyvor\Sdk\Talk\Dto\Website\CreateWebsiteRequest;
use Hyvor\Sdk\Talk\Dto\Comment\ListCommentsRequest;

// org-level access, via a cloud API key
$client = new HyvorClient(
    cloudApiKey: 'your-cloud-api-key', // or tokenProvider: new SomeTokenProviderInterface()
);

// GET /api/console/v1/{id}/website — resource-level access to a specific website.
// The cloud API key/token provider must have access to this website.
$website = $client->talk->website($websiteId)->get();

// POST /api/console/v1/websites — org-level endpoint, not scoped to one website
$website = $client->talk->websites->create(
    new CreateWebsiteRequest(name: 'My Blog', domain: 'blog.example.com')
);

// every Console API resource hangs off the website client, e.g.:
$comments = $client->talk->website($websiteId)->comments->list(new ListCommentsRequest(limit: 10));
$pages = $client->talk->website($websiteId)->pages->list();
$moderators = $client->talk->website($websiteId)->moderators->list();

$client->talk->website($websiteId) exposes: comments, reactions, ratings, pages, users, analytics, moderators, emailDomain, rules, emailLogs, ips, domains, badges, sso, jobs, webhooks, integrations (->slack), and media — matching the Console API one-to-one, plus get()/update() for the website itself.

Hyvor Post

use Hyvor\Sdk\Post\Dto\Issue\ListIssuesRequest;
use Hyvor\Sdk\Post\Dto\Newsletter\UpdateNewsletterRequest;

$client = new HyvorClient(cloudApiKey: 'your-cloud-api-key');

// resource-level access to a specific newsletter (the cloud API key/token
// provider must have access to it)
$newsletter = $client->post->newsletter($newsletterId)->get();
$newsletter = $client->post->newsletter($newsletterId)->update(
    new UpdateNewsletterRequest(name: 'My Newsletter'),
);

$issues = $client->post->newsletter($newsletterId)->issues->list(new ListIssuesRequest(limit: 10));
$subscribers = $client->post->newsletter($newsletterId)->subscribers->list();

$client->post->newsletter($newsletterId) exposes: issues, lists, subscribers, subscriberMetadataDefinitions, sendingProfiles, templates, users, invites, media, and exports — matching the Console API one-to-one, plus get()/update() for the newsletter itself.

Unlike Talk, Post has no org-level endpoints (there's no API to create a newsletter), so PostClient only exposes newsletter(). Also unlike Talk, Post's Console API doesn't embed the newsletter's ID in the URL — every request instead carries an X-Newsletter-Id header, which is how an org-level cloud API key (otherwise valid for every newsletter the org can access) resolves to one specific newsletter.

Resource-level API keys

Resource-level API keys are generated in the Console of each product and are scoped to a single resource (e.g. one website). They can be used without any client-level auth:

$client = new HyvorClient();

$website = $client->talk->website($websiteId, 'your-product-api-key')->get();

// org-level endpoints (like $client->talk->websites->create()) are not supported
// this way, since resource-level API keys are scoped to a single resource.

Configuration

$client = new HyvorClient(
    cloudApiKey: '...',                          // or tokenProvider: ...
    cloudInstance: 'https://hyvor.com',          // default
    logger: $psrLogger,                          // PSR-3 logger, default NullLogger
    httpClient: $psr18Client,                    // Psr\Http\Client\ClientInterface, default: auto-discovered
    requestFactory: $psr17Factory,               // Psr\Http\Message\RequestFactoryInterface, default: auto-discovered
    streamFactory: $psr17Factory,                // Psr\Http\Message\StreamFactoryInterface, default: auto-discovered
    retryMaxAttempts: 3,
    retryBackoffFactor: 2.0,
);

Authentication

HyvorClient accepts at most one of (both are optional — see resource-level API keys above):

  • cloudApiKey — a Cloud API key created at https://hyvor.com/account/org/api-keys. The SDK exchanges it for a short-lived JWT internally (and refreshes it as needed).
  • tokenProvider — a Hyvor\Sdk\Auth\TokenProviderInterface implementation for full control over how the bearer token is obtained. Hyvor\Sdk\Auth\StaticTokenProvider is included for the common case of using a single, pre-issued token (e.g. a JWT generated by an internal integration):
use Hyvor\Sdk\Auth\StaticTokenProvider;

$client = new HyvorClient(
    tokenProvider: new StaticTokenProvider('your-jwt'),
);

Request options

Per-request overrides (retries, extra headers):

use Hyvor\Sdk\RequestOptions;

$client->talk->websites->create(
    new CreateWebsiteRequest(name: 'My Blog', domain: 'blog.example.com'),
    new RequestOptions(retryMaxAttempts: 1),
);

Acting as a specific moderator

By default, the Console API is authenticated as the website owner. To act as a different moderator (see the Console API's "User Authentication" docs), set X-AUTH-USER-EMAIL or X-AUTH-USER-SSO-ID — either as a default for every call made through a WebsiteClient and its sub-resources:

$website = $client->talk->website($websiteId, headers: ['X-AUTH-USER-EMAIL' => 'mod@example.com']);
$website->comments->reply($commentId, new ReplyToCommentRequest(body: 'Thanks!'));

or per call, via RequestOptions::$headers (overrides the client-level default for that call):

$website->comments->reply(
    $commentId,
    new ReplyToCommentRequest(body: 'Thanks!'),
    new RequestOptions(headers: ['X-AUTH-USER-EMAIL' => 'mod@example.com']),
);

Errors

All API errors extend Hyvor\Sdk\Exceptions\HyvorApiException:

  • ValidationFailedException (422) — has $errors (field => messages)
  • RateLimitException (429) — has $retryAfterSeconds
  • AuthenticationException (401/403)
  • NotFoundException (404)
  • ServerErrorException (5xx)
  • NetworkException — request could not be sent (connection/timeout, from the underlying PSR-18 client)
  • ApiException — fallback for other error statuses

Requests to RateLimitException and ServerErrorException-triggering statuses are retried automatically with exponential backoff before the exception is thrown.

Development

See DEV.md at the repo root for running tests (with or without Docker).

Notes

  • Talk requests are sent directly to the product's own instance, derived from cloudInstance by prefixing its host with the product name — e.g. cloudInstance: 'https://hyvor.com' (the default) resolves to https://talk.hyvor.com. Both org-level endpoints (like $client->talk->websites->create()) and resource-level ones (everything under $client->talk->website($id)) go through this same per-product instance.
  • A cloudApiKey is exchanged for a short-lived JWT via POST {cloudInstance}/api/cloud/token, then sent as Authorization: Bearer <jwt> on Talk requests. A resource-level API key (passed to $client->talk->website($id, $apiKey)) is sent the same way, as a bearer token, without any token exchange — even though the public Console API docs only document X-API-KEY auth for resource-level keys.

DTO fields mirror the publicly documented Talk Console API one-to-one, except: request DTOs treat a null property as "omit this field" (so partial updates and list filters don't need every property set) rather than sending a literal JSON null — the one documented exception is VoteOnCommentRequest::$type, where null is itself a meaningful instruction (remove the vote), so it's always sent verbatim.

  • Post's Console API paths (unlike Talk's) don't embed any resource ID — GET /issues, not GET /{newsletterId}/issues. $client->post->newsletter($id) instead sends X-Newsletter-Id: $id as a default header on every request made through it and its sub-resources, which is what lets an org-level cloud API key (otherwise valid for every newsletter the org can access) resolve to one specific newsletter. DTO fields otherwise mirror the publicly documented Post Console API one-to-one, with the same null-means-omit convention as Talk.