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.
- Quick Start
- PHP and Dependency Requirements
- Biggest Conceptual Changes
- Breaking Changes by Area
- New Features and Additions
- Testing Your Migration
- Upgrade to PHP 8.2 or newer.
- Upgrade the package:
composer require workos/workos-php:^5- Replace direct class instantiation with a
WorkOSclient:
use WorkOS\WorkOS;
$workos = new WorkOS(
apiKey: getenv('WORKOS_API_KEY'),
clientId: getenv('WORKOS_CLIENT_ID'),
);- Update renamed APIs and methods.
- Re-run your tests and verify auth, SSO, invitations, and webhook flows end-to-end.
v4 supported PHP 7.3+. The new SDK requires PHP 8.2 or newer.
guzzlehttp/guzzle:^7.0is now required.paragonie/halitewas upgraded from^4.0to^5.1.ext-curlis 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.
Instead 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.
If you were customizing transport internals with:
Client::setRequestClient(...)Client::requestClient()RequestClientInterfaceCurlRequestClient
switch to the new Guzzle-based runtime. The supported customization points are:
new WorkOS(..., handler: $handlerStack)- per-request
RequestOptions
These 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.
Before:
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.
Before:
[$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->organizations
In v5, use:
$result->data$result->listMetadata$result->hasMore()
These v4 behaviors are no longer part of the resource model:
BaseWorkOSResourceconstructFromResponse()- mutable dynamic properties
- the
rawresponse bag on every resource
If 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:
- timestamps are often
DateTimeImmutableinstead of strings - enums are used for many option and state fields
- list responses sometimes return typed wrappers like
RoleListorListModel
Common 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:
AuthenticationExceptionAuthorizationExceptionBadRequestExceptionConflictExceptionConnectionExceptionNotFoundExceptionRateLimitExceededExceptionServerExceptionTimeoutExceptionUnprocessableEntityException
These exceptions now expose request metadata like statusCode, requestId, and for rate limits, retryAfter.
Before:
$sso = new \WorkOS\SSO();After:
$sso = $workos->sso();In v4, SSO::getAuthorizationUrl(...) returned a string and implicitly used WorkOS::getClientId().
In v5 it still returns a string, but:
- requires an instantiated client with
clientId - requires
redirectUri
Before:
$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.
Before:
$profile = $sso->getProfileAndToken($code);After:
$result = $workos->sso()->getProfileAndToken(
clientId: 'client_...',
clientSecret: 'sk_test_...',
code: $code,
grantType: 'authorization_code',
);In 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}"],
),
);Before:
$userManagement = new \WorkOS\UserManagement();After:
$userManagement = $workos->userManagement();These 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()-> usecreatePasswordReset()sendMagicAuthCode()-> usecreateMagicAuth()
userManagement()->getAuthorizationUrl() and userManagement()->getLogoutUrl() still build URLs locally and return strings.
Notable differences:
getAuthorizationUrl()now requiresredirectUriand an instantiated client withclientIdstateis now a string, not an array that the SDK JSON-encodes for you
Before:
$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,
);| v4 | v5 |
|---|---|
listGroups() |
directorySync()->listDirectoryGroups() |
getGroup() |
directorySync()->getDirectoryGroup() |
listUsers() |
directorySync()->listDirectoryUsers() |
getUser() |
directorySync()->getDirectoryUser() |
The API also moved from direct construction to $workos->directorySync().
Before:
$mfa = new \WorkOS\MFA();After:
$mfa = $workos->multiFactorAuth();Use verifyChallenge():
$result = $workos->multiFactorAuth()->verifyChallenge(
id: $authenticationChallengeId,
code: '123456',
);| v4 | v5 |
|---|---|
enrollAuthFactor() |
multiFactorAuth()->createUserAuthFactors() |
listAuthFactors() |
multiFactorAuth()->listUserAuthFactors() |
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.
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().
If you used:
Organizations::listOrganizationFeatureFlags()
switch to:
$workos->featureFlags()->listOrganizationFeatureFlags(...)
Before:
$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.
| 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.
The 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:
connectionis no longer an argument- the parameter order changed completely
- the method now returns an array instead of a
PasswordlessSessionresource
Before:
$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 payloads
Before:
$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.- expanded
vault()support for object CRUD, data keys, and local encryption helpers. RequestOptionsfor per-request headers, idempotency keys, base URL overrides, timeout overrides, and retry overrides.- automatic retries for
429and common5xxresponses. PaginatedResponse::autoPagingIterator()for iterating across all pages.
After migrating, verify at least the following:
- PHP runtime is 8.2+ everywhere the SDK executes.
- All direct
new WorkOS\...class construction has been replaced with aWorkOSclient. - Any list-response destructuring has been updated to
PaginatedResponse. - Any code that accessed resource
rawdata or mutated resource objects has been updated. - Any code that relied on
WorkOS::getApiKey()/getClientId()throwing during bootstrap has been updated. - SSO and User Management URL-generation code has been updated for the new request/response shape.
- Session-cookie code now uses
SessionManager, and any success/failure type checks were updated for the new array return shape. - Deprecated User Management and MFA method names have been replaced, including auth methods that no longer accept leading credential arguments.
- Webhook verification paths were updated to exception-based handling.
- Integration tests for SSO, AuthKit, invitations, password reset, and webhooks still pass.