hyvor / sdk
Official PHP SDK for HYVOR products
Requires
- php: >=8.4
- ext-json: *
- php-http/discovery: ^1.20
- phpdocumentor/reflection-docblock: ^6.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0
- symfony/property-access: ^8.1
- symfony/property-info: ^8.1
- symfony/serializer: ^8.1
Requires (Dev)
- guzzlehttp/guzzle: ^7.15
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^10.5
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 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'), );
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$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.
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
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.