Official PHP SDK for HYVOR Products.
composer require hyvor/sdkRequires 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.
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.
$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(['name' => 'My Newsletter']);
$issues = $client->post->newsletter($newsletterId)->issues->list(['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 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.$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,
);HyvorClient accepts at most one of (both are optional — see resource-level API keys above):
cloudApiKey— a Cloud API key created athttps://hyvor.com/account/org/api-keys. The SDK exchanges it for a short-lived JWT internally (and refreshes it as needed).tokenProvider— aHyvor\Sdk\Auth\TokenProviderInterfaceimplementation for full control over how the bearer token is obtained.Hyvor\Sdk\Auth\StaticTokenProvideris 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'),
);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),
);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']),
);All API errors extend Hyvor\Sdk\Exceptions\HyvorApiException:
ValidationFailedException(422) — has$errors(field => messages)RateLimitException(429) — has$retryAfterSecondsAuthenticationException(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.
See DEV.md at the repo root for running tests (with or without Docker).
- Talk requests are sent directly to the product's own instance, derived from
cloudInstanceby prefixing its host with the product name — e.g.cloudInstance: 'https://hyvor.com'(the default) resolves tohttps://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
cloudApiKeyis exchanged for a short-lived JWT viaPOST {cloudInstance}/api/cloud/token, then sent asAuthorization: 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 documentX-API-KEYauth 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, notGET /{newsletterId}/issues.$client->post->newsletter($id)instead sendsX-Newsletter-Id: $idas 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.