workos/workos-php
Official WorkOS PHP SDK for interacting with the WorkOS API. Includes support for Single Sign-On, Directory Sync, Admin Portal, and Magic Link features. Configure via WORKOS_API_KEY and WORKOS_CLIENT_ID environment variables.
This guide covers the changes required to migrate from the v4 PHP SDK to the next major release of workos/workos-php.
The biggest change is architectural: the SDK is now centered around an instantiated WorkOS client with typed request/response models, lazy client methods like sso() and userManagement(), and a Guzzle-based HTTP runtime.
composer require workos/workos-php:^5
WorkOS client:use WorkOS\WorkOS;
$workos = new WorkOS(
apiKey: getenv('WORKOS_API_KEY'),
clientId: getenv('WORKOS_CLIENT_ID'),
);
v4 supported PHP 7.3+. The new SDK requires PHP 8.2 or newer.
guzzlehttp/guzzle:^7.0 is now required.paragonie/halite was upgraded from ^4.0 to ^5.1.ext-curl is now declared as ^8.2.If your app or deployment environment was pinned to older PHP or extension versions, upgrade those first.
Before:
use WorkOS\WorkOS;
use WorkOS\UserManagement;
WorkOS::setApiKey('sk_test_...');
WorkOS::setClientId('client_...');
$userManagement = new UserManagement();
$user = $userManagement->createUser('user@example.com');
After:
use WorkOS\WorkOS;
$workos = new WorkOS(
apiKey: 'sk_test_...',
clientId: 'client_...',
);
$user = $workos->userManagement()->createUsers(
email: 'user@example.com',
);
WorkOS::setApiKey() and WorkOS::setClientId() still exist as defaults, but the intended integration style is now an instantiated client.
Static getters are no longer the primary configuration path. In v4, WorkOS::getApiKey() and WorkOS::getClientId() loaded environment variables and threw when unset; in v5, they are nullable compatibility shims, and credential validation happens when an operation requires them.
WorkOS clientInstead of instantiating new SSO(), new UserManagement(), new MFA(), and so on, you now call lazy client methods:
$workos->sso()$workos->userManagement()$workos->multiFactorAuth()$workos->directorySync()$workos->organizations()$workos->authorization()$workos->adminPortal()$workos->auditLogs()$workos->featureFlags()$workos->webhooks()Resources are now typed readonly models with fromArray() / toArray() methods. Timestamps are commonly hydrated into DateTimeImmutable, and many option values now use enums instead of free-form strings.
If you previously relied on mutable resource objects, dynamic properties, or BaseWorkOSResource, review that code carefully.
Many generated methods now have longer signatures with optional parameters near the front. Positional argument code that compiled in v4 will often call the wrong parameter in v5.
Prefer named arguments:
$workos->organizations()->listOrganizations(
after: 'org_123',
limit: 25,
);
Most APIs now live behind the WorkOS client and share an internal HttpClient, so code like this should be removed:
new \WorkOS\SSO();
new \WorkOS\UserManagement();
new \WorkOS\Organizations();
new \WorkOS\MFA();
new \WorkOS\Portal();
new \WorkOS\RBAC();
Use the WorkOS client methods instead.
It is not enough to replace new \WorkOS\SSO() with $workos->sso(). If your code imported or type-hinted old top-level service classes such as WorkOS\UserManagement, WorkOS\Portal, or WorkOS\RBAC, update those references to the client-accessed services instead.
Client, RequestClientInterface, and CurlRequestClient were removedIf you were customizing transport internals with:
Client::setRequestClient(...)Client::requestClient()RequestClientInterfaceCurlRequestClientswitch to the new Guzzle-based runtime. The supported customization points are:
new WorkOS(..., handler: $handlerStack)RequestOptionsWorkOS configuration methods were removedThese v4 methods are gone:
WorkOS::getApiBaseURL()WorkOS::setApiBaseUrl()WorkOS::setIdentifier()WorkOS::getIdentifier()WorkOS::setVersion()WorkOS::getVersion()Configure baseUrl, timeout, maxRetries, and handler via the WorkOS constructor instead.
WorkOS::getApiKey() and WorkOS::getClientId() no longer validate configurationBefore:
use WorkOS\WorkOS;
WorkOS::setApiKey(getenv('WORKOS_API_KEY'));
WorkOS::setClientId(getenv('WORKOS_CLIENT_ID'));
$clientId = WorkOS::getClientId();
$apiKey = WorkOS::getApiKey();
After:
use WorkOS\WorkOS;
$workos = new WorkOS(
apiKey: getenv('WORKOS_API_KEY'),
clientId: getenv('WORKOS_CLIENT_ID'),
);
In v4, the getters loaded env vars and threw ConfigurationException when missing. In v5, they only return the current static shim value. If you used them as bootstrap-time validation, move that validation to new WorkOS(...) or to the first API call that requires credentials.
Resource\PaginatedResource was replaced with WorkOS\PaginatedResponseBefore:
[$before, $after, $users] = $userManagement->listUsers();
After:
$page = $workos->userManagement()->listUsers();
$users = $page->data;
$after = $page->listMetadata['after'] ?? null;
PaginatedResponse also adds auto-pagination helpers:
foreach ($page->autoPagingIterator() as $user) {
// ...
}
Auto-pagination only follows after cursors. If your integration previously relied on reverse pagination with before, keep fetching those pages manually.
v4 list responses supported access patterns like:
[$before, $after, $items] = $result$result->users$result->organizationsIn v5, use:
$result->data$result->listMetadata$result->hasMore()BaseWorkOSResource is goneThese v4 behaviors are no longer part of the resource model:
BaseWorkOSResourceconstructFromResponse()raw response bag on every resourceIf you previously accessed $resource->raw, mutated resource fields, or extended resource base classes, migrate to typed properties plus toArray().
Examples of behavior changes you may notice:
DateTimeImmutable instead of stringsRoleList or ListModelCommon constant-to-enum migrations include:
use WorkOS\Resource\EventsOrder;
$workos->organizations()->listOrganizations(
order: EventsOrder::Asc,
);
use WorkOS\Resource\ConnectionsConnectionType;
$workos->sso()->listConnections(
connectionType: ConnectionsConnectionType::OktaSAML,
);
The new runtime maps HTTP failures to explicit exception classes such as:
AuthenticationExceptionAuthorizationExceptionBadRequestExceptionConflictExceptionConnectionExceptionNotFoundExceptionRateLimitExceededExceptionServerExceptionTimeoutExceptionUnprocessableEntityExceptionThese exceptions now expose request metadata like statusCode, requestId, and for rate limits, retryAfter.
SSO is now accessed through the clientBefore:
$sso = new \WorkOS\SSO();
After:
$sso = $workos->sso();
getAuthorizationUrl() still builds a URL locally but the API changedIn v4, SSO::getAuthorizationUrl(...) returned a string and implicitly used WorkOS::getClientId().
In v5 it still returns a string, but:
clientIdredirectUriBefore:
$url = $sso->getAuthorizationUrl(
domain: 'example.com',
redirectUri: 'https://example.com/callback',
state: ['return_to' => '/dashboard'],
);
After:
$url = $workos->sso()->getAuthorizationUrl(
redirectUri: 'https://example.com/callback',
domain: 'example.com',
state: json_encode(['return_to' => '/dashboard']),
);
state is now a string parameter. If you used array state in v4, encode it yourself.
client_id now comes from the instantiated WorkOS client, and the SDK always sends response_type=code for you.
getProfileAndToken() now requires explicit credentialsBefore:
$profile = $sso->getProfileAndToken($code);
After:
$result = $workos->sso()->getProfileAndToken(
clientId: 'client_...',
clientSecret: 'sk_test_...',
code: $code,
grantType: 'authorization_code',
);
getProfile($accessToken) changed shapeIn v4, getProfile() accepted the access token directly.
In v5, the method signature no longer takes an access token argument. Based on the current API surface, pass the token via RequestOptions headers:
use WorkOS\RequestOptions;
$profile = $workos->sso()->getProfile(
options: new RequestOptions(
extraHeaders: ['Authorization' => "Bearer {$accessToken}"],
),
);
UserManagement is now accessed through the clientBefore:
$userManagement = new \WorkOS\UserManagement();
After:
$userManagement = $workos->userManagement();
UserManagementThese v4 methods are no longer on UserManagement:
getJwksUrl()authenticateWithSessionCookie()loadSealedSession()getSessionFromCookie()Use SessionManager instead:
use WorkOS\SessionManager;
$url = SessionManager::getJwksUrl('client_...');
$result = $workos->sessionManager()->authenticate(
sessionData: $_COOKIE['wos-session'] ?? '',
cookiePassword: $cookiePassword,
clientId: 'client_...',
);
The old new UserManagement($encryptor) customization point was also removed.
Before:
$result = $userManagement->authenticateWithSessionCookie(
$sealedSession,
$cookiePassword,
);
After:
$result = $workos->sessionManager()->authenticate(
sessionData: $sealedSession,
cookiePassword: $cookiePassword,
clientId: 'client_...',
);
The new method returns an associative array such as ['authenticated' => true, ...] or ['authenticated' => false, 'reason' => 'invalid_jwt']. If your code checked for SessionAuthenticationSuccessResponse or SessionAuthenticationFailureResponse, update that logic.
Before:
$result = $userManagement->authenticateWithPassword(
'client_...',
'user@example.com',
'secret',
);
After:
$result = $workos->userManagement()->authenticateWithPassword(
email: 'user@example.com',
password: 'secret',
);
The same change applies to methods like authenticateWithCode() and authenticateWithRefreshToken(): remove leading credential arguments and ensure the WorkOS client was instantiated with apiKey and clientId.
| v4 | v5 |
|---|---|
createUser() |
userManagement()->createUsers() |
createOrganizationMembership() |
userManagement()->createOrganizationMemberships() |
sendInvitation() |
userManagement()->createInvitations() |
findInvitationByToken() |
userManagement()->getByToken() |
authenticateWithSelectedOrganization() |
userManagement()->authenticateWithOrganizationSelection() |
verifyEmail() |
userManagement()->confirmEmailVerification() |
resetPassword() |
userManagement()->confirmPasswordReset() |
listSessions() |
userManagement()->listUserSessions() |
These methods existed in v4 but should be treated as removed in v5:
sendPasswordResetEmail() -> use createPasswordReset()sendMagicAuthCode() -> use createMagicAuth()userManagement()->getAuthorizationUrl() and userManagement()->getLogoutUrl() still build URLs locally and return strings.
Notable differences:
getAuthorizationUrl() now requires redirectUri and an instantiated client with clientIdstate is now a string, not an array that the SDK JSON-encodes for youBefore:
$url = $userManagement->getAuthorizationUrl(
'https://example.com/callback',
['return_to' => '/dashboard'],
WorkOS\UserManagement::AUTHORIZATION_PROVIDER_AUTHKIT,
);
After:
$url = $workos->userManagement()->getAuthorizationUrl(
redirectUri: 'https://example.com/callback',
state: json_encode(['return_to' => '/dashboard']),
provider: \WorkOS\Resource\UserManagementAuthenticationProvider::Authkit,
);
DirectorySync method names are more explicit| v4 | v5 |
|---|---|
listGroups() |
directorySync()->listDirectoryGroups() |
getGroup() |
directorySync()->getDirectoryGroup() |
listUsers() |
directorySync()->listDirectoryUsers() |
getUser() |
directorySync()->getDirectoryUser() |
The API also moved from direct construction to $workos->directorySync().
MFA became multiFactorAuth()Before:
$mfa = new \WorkOS\MFA();
After:
$mfa = $workos->multiFactorAuth();
verifyFactor() is goneUse verifyChallenge():
$result = $workos->multiFactorAuth()->verifyChallenge(
id: $authenticationChallengeId,
code: '123456',
);
UserManagement| v4 | v5 |
|---|---|
enrollAuthFactor() |
multiFactorAuth()->createUserAuthFactors() |
listAuthFactors() |
multiFactorAuth()->listUserAuthFactors() |
Portal became adminPortal()Before:
$portal = new \WorkOS\Portal();
$link = $portal->generateLink('org_123', 'sso');
After:
$response = $workos->adminPortal()->generateLink(
organization: 'org_123',
intent: \WorkOS\Resource\GenerateLinkIntent::SSO,
);
$link = $response->link;
intent_options is also now supported.
RBAC was replaced by authorization()Before:
$rbac = new \WorkOS\RBAC();
After:
$authorization = $workos->authorization();
The environment-role method names were renamed:
| v4 | v5 |
|---|---|
createEnvironmentRole() |
authorization()->createRoles() |
listEnvironmentRoles() |
authorization()->listRoles() |
getEnvironmentRole() |
authorization()->getRole() |
updateEnvironmentRole() |
authorization()->updateRole() |
setEnvironmentRolePermissions() |
authorization()->updateRolePermissions() |
addEnvironmentRolePermission() |
authorization()->createRolePermissions() |
Organization-role APIs also moved from Organizations / RBAC into authorization().
OrganizationsIf you used:
Organizations::listOrganizationFeatureFlags()switch to:
$workos->featureFlags()->listOrganizationFeatureFlags(...)RequestOptionsBefore:
$organizations = new \WorkOS\Organizations();
$organization = $organizations->createOrganization('Acme', null, null, 'idemp_123');
After:
use WorkOS\RequestOptions;
$organization = $workos->organizations()->createOrganizations(
name: 'Acme',
options: new RequestOptions(
idempotencyKey: 'idemp_123',
),
);
The same pattern applies anywhere the new runtime uses RequestOptions.
If you rely on retries for write requests, set an explicit idempotency key yourself. The new runtime retries retryable responses, but it does not auto-generate idempotency keys for POST requests.
AuditLogs is now accessed through the client, and several methods were renamed| v4 | v5 |
|---|---|
createEvent() |
auditLogs()->createEvents() |
createExport() |
auditLogs()->createExports() |
createSchema() |
auditLogs()->createActionSchemas() |
schemaExists() |
removed |
There is no direct schemaExists() helper in v5. Call listActionSchemas() and handle NotFoundException if you need equivalent behavior.
Passwordless::createSession() changed signatureThe old positional signature was:
createSession($email, $redirectUri, $state, $type, $connection, $expiresIn)
The new signature is:
$workos->passwordless()->createSession(
email: 'user@example.com',
type: 'MagicLink',
redirectUri: 'https://example.com/callback',
state: '...',
expiresIn: 900,
);
Notable changes:
connection is no longer an argumentPasswordlessSession resourcesendSession() now takes a session ID stringBefore:
$session = $passwordless->createSession(...);
$passwordless->sendSession($session);
After:
$session = $workos->passwordless()->createSession(...);
$workos->passwordless()->sendSession($session['id']);
Widgets::getToken() was renamed to widgets()->createToken(), and the return type is now WidgetSessionTokenResponse.
The Vault API was expanded and renamed around "objects" instead of "vault objects".
| v4 | v5 |
|---|---|
getVaultObject() |
vault()->readObject() |
listVaultObjects() |
vault()->listObjects() |
Additional Vault capabilities were added in v5, including object version listing, object creation/update/delete, data-key APIs, and local encrypt/decrypt helpers.
v4 used a single Webhook helper for verification.
In v5:
$workos->webhooks() manages webhook endpoints$workos->webhookVerification() verifies webhook payloadsBefore:
$webhook = new \WorkOS\Webhook();
$result = $webhook->verifyHeader($sigHeader, $payload, $secret, 180);
if ($result !== 'pass') {
// handle string error
}
After:
$event = $workos->webhookVerification()->verifyEvent(
eventBody: $payload,
eventSignature: $sigHeader,
secret: $secret,
);
If verification fails, verifyHeader() / verifyEvent() throw InvalidArgumentException.
These are not migration blockers, but they are new capabilities in the v5 SDK:
apiKeys() for organization API key management and validation.connect() for Connect applications and OAuth completion.events() for event listing.featureFlags() for feature flag retrieval and targeting.organizationDomains() for standalone organization domain operations.pipes() for data integration flows.radar() for attempts and list management.actions() for WorkOS Actions signature verification and signed responses.sessionManager() for sealing, unsealing, session-cookie auth, JWKS helpers, and refresh flows.pkce() for PKCE verifier/challenge generation and AuthKit/SSO PKCE flows.vault() support for object CRUD, data keys, and local encryption helpers.RequestOptions for per-request headers, idempotency keys, base URL overrides, timeout overrides, and retry overrides.429 and common 5xx responses.PaginatedResponse::autoPagingIterator() for iterating across all pages.After migrating, verify at least the following:
new WorkOS\... class construction ha...How can I help you explore Laravel packages today?