ory/client
A PHP client for integrating Ory identity and access APIs into Laravel applications, with generated SDK classes for services such as Kratos and Hydra. Useful for teams adopting Ory’s hosted or self-managed auth, though adoption and maintenance may be limited.
ory/client is an OpenAPI-generated PHP SDK for the Ory stack (Kratos, Hydra, Keto, Oathkeeper, and Ory Network). It is not a Laravel package — there is no service provider, no config file, no facade. What you get is a set of *Api classes plus a Configuration object, and you wire it up yourself.
Install:
composer require ory/client
Look first at these three things in vendor/ory/client/lib:
Configuration.php — host, access token, and the constructor that most API classes take.Api/IdentityApi.php (Kratos admin), Api/FrontendApi.php (Kratos public), Api/OAuth2Api.php (Hydra admin), Api/RelationshipApi.php / Api/PermissionApi.php (Keto).Model/Session.php, Model/Identity.php, Model/CreateIdentityBody.php — the payloads you will actually be touching.The first realistic use case in a Laravel app is verifying a Kratos session on an incoming request and pulling the identity out of it. That single call justifies the dependency:
use Ory\Client\Api\FrontendApi;
use Ory\Client\Configuration;
$config = (new Configuration())->setHost(config('services.ory.public_url'));
$session = (new FrontendApi($config))->toSession(
null, // xSessionToken — usually null
$request->header('Cookie') // forward the browser cookie verbatim
);
if (! $session->getActive()) {
abort(401);
}
$identityId = $session->getIdentity()->getId();
$traits = $session->getIdentity()->getTraits(); // array from the schema
The second realistic use case is the admin API for programmatic identity management — provisioning users from a Laravel command or job, listing identities for an admin panel, soft-deleting on account closure. That is where the SDK earns its keep versus hand-rolling HTTP calls.
Bind one configured client per service in a service provider. Don't new API classes inline; the configuration is mutable state and you will eventually need a second host or a longer timeout.
// config/services.php
'ory' => [
'public_url' => env('ORY_PUBLIC_URL'), // http://127.0.0.1:4433 or https://{slug}.projects.oryapis.com
'admin_url' => env('ORY_ADMIN_URL'), // http://127.0.0.1:4434 or the same Ory Network host
'api_key' => env('ORY_API_KEY'), // only for Ory Network / secured Hydra
'timeout' => env('ORY_TIMEOUT', 5),
],
// AppServiceProvider::register()
$this->app->singleton('ory.admin', fn () => (new Configuration())
->setHost(config('ory.admin_url'))
->setAccessToken(config('ory.api_key')));
$this->app->singleton(IdentityApi::class, fn ($app) => new IdentityApi($app['ory.admin']));
$this->app->singleton(FrontendApi::class, fn ($app) => new FrontendApi($app['ory.public']));
Then inject IdentityApi into controllers, commands, and jobs normally.
Separate public from admin hosts deliberately. In self-hosted Kratos these are two ports (4433 public, 4434 admin) and they expose different endpoints for the same resources. FrontendApi is for browser-facing self-service flows and session checks; IdentityApi is for CRUD. Calling admin endpoints on the public port gives you a 404 that looks like "the SDK is broken" but is really a routing mistake. Keep two named bindings (ory.public, ory.admin) so you cannot mix them up by accident.
Wrap the admin API in your own thin repository interface. The generated classes are not mockable in any pleasant way, and they change shape on regeneration. A three-method interface (findById, create, deactivate) that delegates to IdentityApi lets you fake it in tests, swap to raw Http:: calls later, and keep Ory's CreateIdentityBody out of your domain layer.
final class OryIdentityRepository implements IdentityRepository
{
public function __construct(private IdentityApi $api) {}
public function create(string $email, array $traits, string $schemaId): string
{
$body = (new CreateIdentityBody())
->setSchemaId($schemaId)
->setTraits(['email' => $email] + $traits);
return $this->api->createIdentity($body)->getId();
}
public function all(int $page = 1, int $perPage = 100): array
{
return $this->api->listIdentities($perPage, $page);
}
}
Use the *WithHttpInfo variants when you need headers. List endpoints return a bare array; the total count lives in X-Total-Count, which the plain method throws away. This is the one place you must use the verbose variant:
[$identities, $status, $headers] = $api->listIdentitiesWithHttpInfo(50, 1);
$total = (int) ($headers['X-Total-Count'][0] ?? 0);
Forward cookies, not bearer tokens, for session checks. toSession() accepts an X-Session-Token header (for SPAs / API clients) or a raw Cookie header (for server-rendered pages). In Laravel, $request->header('Cookie') is the correct thing to pass — do not try to reconstruct the session from $request->session(), which is a completely unrelated Laravel session.
Cache the session check per request. toSession() is a network call; add it to a middleware and stash the result on $request->attributes so policies and controllers reuse it:
class VerifyOrySession
{
public function handle($request, Closure $next)
{
$session = $this->frontend->toSession(null, $request->header('Cookie'));
if (! $session->getActive()) {
return response()->json(['message' => 'Unauthenticated'], 401);
}
$request->attributes->set('ory_session', $session);
$request->setUserResolver(fn () => new OryUser($session->getIdentity()));
return $next($request);
}
}
Keto relationships and Hydra introspection follow the same shape. If you are using Keto for authorization, RelationshipApi::createRelationship() takes a Relationship model built from a SubjectSet or subject id — the string-tuple form (document:1#viewer@user:abc) is parsed server-side by OPL, so if you find yourself constructing objects for every tuple, consider using checkOplSyntax() to validate batches at deploy time instead.
The package is generated, not hand-maintained. Version bumps are machine-made and can rename parameters or change nullability without a human ever thinking about backwards compatibility. Pin an exact version ("ory/client": "1.22.*"), read the diff before bumping, and expect to re-run your integration tests.
Error bodies are not deserialized. A failed call throws Ory\Client\ApiException; getResponseBody() hands you a raw JSON string. If you want Ory's structured error (error.message, error.reason, and for self-service flows the full ui.nodes tree), decode it yourself:
try {
$api->createIdentity($body);
} catch (ApiException $e) {
$payload = json_decode($e->getResponseBody(), true);
// $payload['error']['message'], $payload['error']['reason']
}
Registration and login validation failures are not thrown as clean exceptions from a dedicated method — self-service flows are meant to be driven by Ory's hosted UI, and if you call updateLoginFlow directly you will be digging ui.nodes out of this JSON.
setAccessToken() is a no-op if you forget to pass the Configuration. new IdentityApi() silently constructs its own default configuration pointing at http://localhost. Symptoms: connection refused, or 401 against the wrong host. Always pass the config as the first constructor argument.
Trailing slashes in setHost() produce //admin/identities. Some servers tolerate it; some proxies don't. Strip the trailing slash in your config.
Kratos self-hosted admin has no auth by default; Ory Network requires a bearer token. Don't assume your local setup is representative — a setAccessToken() that works locally may be masking a missing .env entry in staging. Assert config('ory.api_key') is present when the host is *.projects.oryapis.com.
CSRF tokens are required on the frontend API for state-changing browser flows. If you are calling FrontendApi mutating endpoints directly rather than redirecting to Ory's UI, include the X-CSRF-Token. This only applies to the public API; the admin API has no CSRF.
Timeouts default to whatever Guzzle defaults to, which is usually "forever." Pass a pre-configured Guzzle client as the second constructor argument (check the generated signature — it changed between openapi-generator versions) or call setTimeout() on the API instance. A hanging identity check in middleware will take your whole app down.
Testing: mock your wrapper, not the generated classes. If you must mock at the SDK boundary, use Guzzle's MockHandler in the injected HTTP client — trying to subclass IdentityApi is fragile because the generated constructors are not stable.
Given 7 stars and 0 dependents, ask whether you need the SDK at all. For a handful of endpoints — session check, GET /admin/identities/{id} — Laravel's Http client against the same REST API is arguably simpler, fully testable with Http::fake(), and removes a generated-code dependency you did not choose. The SDK becomes worth it when you are touching a dozen endpoints or the large model graph (Hydra clients, Keto relationship objects), where hand-writing DTOs is the bigger cost.
listIdentities() argument order is positional and long (perPage, page, ids, ...). It is easy to pass page where perPage goes. Use named arguments — the generated signatures are stable enough for it, and it makes call sites reviewable.
How can I help you explore Laravel packages today?