workos/workos-php
The official WorkOS PHP library gives PHP applications convenient access to the WorkOS API. Install via Composer, configure WORKOS_API_KEY and WORKOS_CLIENT_ID, and integrate SSO, Directory Sync, Admin Portal, Magic Link, and more.
^9.4.0 to ^9.5.0, one day after the last assessment. The container bindings, generated model/enum conventions, and Eloquent-friendly response objects remain unchanged; the integration surface is the same, but the v9.5 spec regeneration removes classes we may already have referenced, so re-verification of every touched call site is mandatory rather than assumed stable.feat(generated)! breaking commit (PR #452). This confirms the planning assumption that generated-code churn can break at any version increment — contract tests against real payloads matter more than pinning.AddDataIntegrationsUpsertApiKeyRequest, AddDataIntegrationsUpsertClientCredentialsRequest, and their ConnectionOwner enums are gone. These are named classes, so any use statement, type hint, constructor call, or test factory referencing them is a fatal error at class-load time, not a runtime 500. This directly repudiates part of the prior assessment's "Pipes parameters are additive" guidance: the parameters were additive, the class names were not. The surviving types appear to be DataIntegrationsUpsertApiKeyRequest / DataIntegrationsUpsertApiKeyRequest2 / ReauthorizeDataIntegrationsUpsertApiKeyRequest.api-key, client-credentials, org-scoped and user-scoped connected accounts) changed their idempotency-key contract. This is an architecture-level concern, not cosmetic — retry semantics belong in the HTTP client wrapper (Http::workos() / retry() config), so the change must be absorbed centrally or every call site inherits duplicate-write risk.AccountSelectionRequiredError model still stands from v9.4 as a first-class domain exception; v9.5 adds ConnectedAccountWriteResponseWarning, a second non-exception "soft failure" surface that should map to a warning channel rather than a success flash.DirectorySyncRateLimitError model: A typed 429 for POST /directories/{id}/sync. This fits Laravel's job/queue lifecycle better than a raw exception — it implies explicit backoff and a Release strategy for the new directory sync endpoint.POST /portal/revoke_link + RevokeLink model + external_user_id on GenerateLink: This closes a real gap — Admin Portal links were previously generate-only, meaning revocation required out-of-band steps. external_user_id also lets us skip a WorkOS user-id lookup in the generate path, reducing fan-out.UserRoleAssignmentSourceGroup + group on UserRoleAssignmentSource: Extends the authorization source taxonomy from v9.4. Fits Laravel RBAC/tenancy mapping, but changes the shape of a model we already planned to snapshot-test.invocable_as: New enum-backed capability surface (AgentBlueprintInvocableAs, plus create/update request variants). Additive and roadmap-gated; no action unless Agents is in scope.^9.4 → ^9.5. Composer-compatible; no new Guzzle/Symfony conflicts signalled. But the PHP/Laravel floor question from the prior assessment remains unanswered and now applies to ^9.5.// REMOVED in v9.5 — fatal if referenced anywhere:
// AddDataIntegrationsUpsertApiKeyRequest
// AddDataIntegrationsUpsertClientCredentialsRequest
// AddDataIntegrationsUpsertApiKeyRequestConnectionOwner
// AddDataIntegrationsUpsertClientCredentialsRequestConnectionOwner
Grep the codebase, tests, factories, and any IDE-generated stubs before bumping. Because v9.5 ships a day after v9.4, it is highly unlikely any production code references these — the realistic exposure is an in-flight upgrade branch cut against ^9.4.// config/services.php
'workos' => [
// ...
'idempotency' => [
'enabled' => env('WORKOS_IDEMPOTENCY', true),
'header' => 'Idempotency-Key',
'scope' => ['pipes.api-key', 'pipes.client-credentials',
'pipes.connected-accounts.org', 'pipes.connected-accounts.user'],
],
],
Generate the key per logical operation (not per HTTP attempt), persist it alongside the queued job, and log it. Without this, retry/queue replay silently creates duplicate connected accounts.} catch (DirectorySyncRateLimitError $e) {
DirectorySyncJob::dispatch($directoryId)->delay(now()->addSeconds($e->retryAfter ?? 60));
}
ConnectedAccountWriteResponseWarning: Pipes writes can now succeed with warnings (state_reason also appears on ConnectedAccount). Rendering these as success is a support-ticket generator — surface them in the UI and log them with the connected-account ID.$link = app(WorkOS::class)->adminPortal()->generateLink(
intent: $intent,
externalUserId: $user->workos_external_id, // new, avoids a lookup
);
// $link->link is opaque — see prior format-change discipline.
app(WorkOS::class)->adminPortal()->revokeLink($linkId);
Note POST /portal/generate_link changed its errors — re-verify any renderable() mapping on Admin Portal generation.changed errors appears on ~35 endpoints spanning audit_logs, authorization (FGA, ~19 endpoints), feature_flags, organizations, organization_membership, admin_portal, and pipes. Any hand-rolled error mapping in Http::workos() or a Handler::renderable() block is a candidate for silent misclassification (e.g. a 400 becoming a 500).account_display_name / account_identifier: Additive across create/upsert/reauthorize paths, but they are user-supplied identity fields — they must be validated and length-capped on our side, and they should be treated as display data, never as keys.secret/uri, PortalLinkResponse.link, passwordless URLs) are unchanged and unresolved until Phase 3 completes.changed errors in this release means our error taxonomy can rot without a single code change on our side. Mitigation is a contract test suite that asserts our mapped error classes against live upstream responses.tries > 1 or retry() configured on Pipes write endpoints is now an unguarded duplicate-creation path. Audit retry config before the bump.ConnectedAccountWriteResponseWarning introduces a third outcome (success / success-with-warning / failure). Two-state handling in code and support runbooks will misclassify it.UserRoleAssignmentSourceGroup (invalidates existing UserRoleAssignmentSource snapshots), ConnectedAccountWriteResponse/...Warning, RevokeLink, DirectorySyncResponse/...RateLimitError, and invocable_as enums.use American spelling and add agent spelling rule may have renamed identifiers or docs fields. Low probability of API impact, but a cheap grep for authoris*/organis* in our own wrapper code is warranted — our naming may now diverge from the SDK's.AddDataIntegrationsUpsertApiKeyRequest* or its ConnectionOwner enum? (Confirm zero before bumping.)tries, Http::retry) can now double-write, and what is the dedupe key per operation?exp claim?secret/uri, portal links, reset/invitation URLs?POST /directories/{id}/sync in scope? If yes, who owns the backoff/DirectorySyncRateLimitError handling and the polling cadence?revoke_link is worth adopting purely as a support-cost reduction.^9.5 require, and does that force a platform upgrade?AccountSelectionRequiredError, warning handling) and defer the identity fields.workos/workos-php:^9.5. No new bindings required; extend the v9.4 session-auth defaults and add idempotency configuration centrally rather than per call site.
$this->app->afterResolving(WorkOS::class, function ($workos) {
$workos->setSessionAuthDefaults([
'issuer' => config('services.workos.issuer'),
]);
});
'workos' => [
'issuer' => env('WORKOS_ISSUER'),
'auth_max_age' => env('WORKOS_AUTH_MAX_AGE', 3600),
'cors_origins' => env('WORKOS_CORS_ORIGINS', []),
'validate_exp' => true,
// v9.5
'idempotency' => [
'enabled' => env('WORKOS_IDEMPOTENCY', true),
'header' => 'Idempotency-Key',
],
'directory_sync' => [
'retry_after' => env('WORKOS_DS_RETRY_AFTER', 60),
],
],
WorkOSIdempotency::for(string $operation, string $modelKey): string that persists the key with the job/log record. This converts a vendor-contract change into one file.$response = app(WorkOS::class)->pipes()->...;
if (! empty($response->warnings)) {
Log::warning('workos.pipes.warning', [
'connected_account' => $response->id,
'state_reason' => $response->state_reason,
'warnings' => $response->warnings,
]);
}
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (AccountSelectionRequiredError $e) {
return redirect()->route('integrations.select-account', ['slug' => $e->integrationSlug]);
});
$exceptions->render(function (DirectorySyncRateLimitError $e) {
return response()->json(['error' => 'rate_limited'], 429)
->header('Retry-After', $e->retryAfter ?? 60);
});
});
text columns, generous sizing. The new external_user_id and account_identifier fields are our inputs — validate and cap those explicitly.Phase 0 — Reconnaissance (blocking, now overdue)
exp, persisted/cached WorkOS URLs, regex validation on WorkOS fields, TOTP rendering, snapshot tests.Http::retry, queue tries, backoff) on the four idempotency-affected Pipes POSTs.Phase 1 — Dependency and platform
^9.5 (supersedes the ^9.4 target; do not stage 9.4 then 9.5 — one bump).Phase 2 — Session/auth hardening (highest risk first) — unchanged from v9.4 plan
WORKOS_ISSUER in staging; enable issuer check; observe rejection rates.exp; update fixtures.Phase 3 — Format-change audit — unchanged from v9.4 plan
Phase 4 — Error-contract reconciliation (new; promoted ahead of capability work)
POST /portal/generate_link error handling specifically, since we already use that path.Phase 5 — Idempotency and duplicate-write protection (new)
Phase 6 — Capability adoption (roadmap-gated, optional)
AccountSelectionRequiredError handling, ConnectedAccountWriteResponseWarning logging, DirectorySyncRateLimitError handler (even if directory sync is not yet used — it is a few lines and prevents a 500 later).POST /portal/revoke_link + external_user_id on generate. Justifiable without a Pipes roadmap, because it removes manual support steps.account_display_name/account_identifier/established_by_user_id/state_reason, Agents invocable_as, AuthKit OAuth resources.UserRoleAssignmentSourceGroup if authorization source grouping is near-term; otherwise add the enum to the deferred-tracking list so snapshots do not silently rot.Phase 7 — Verification and cutover
UserRoleAssignmentSourceGroup, ConnectedAccountWriteResponse(+Warning), RevokeLink, DirectorySyncResponse, DirectorySyncRateLimitError, invocable_as enums.main to ^9.5; retain a deployable ^8 branch for emergency rollback for one release cycle.^9.5 and remains unconfirmed. Projects pinned to ^8 for Laravel 8.x support stay on ^8 and accept the gap in session exp enforcement, or upgrade the platform.text sizing for WorkOS-returned codes/URLs/secrets as previously flagged; (b) new nullable columns if we persist account_display_name, account_identifier, established_by_user_id, or state_reason for Pipes connected accounts.POST /directories/{id}/sync.expires is still independent of WorkOS's mandatory JWT exp — two clocks, document it.^9.4 documented; treat any existing retry policy on those endpoints as unverified.exp enforcement, URL encoding, idempotency contract) are not config-flaggable. Rollback means reverting the SDK version — keep the ^8 branch deployable, and note that rolling back from 9.5 to 9.4 does not restore the old idempotency contract, since that is server-side.exp, issuer mismatch, unhandled AccountSelectionRequiredError) now needs three more entries — duplicate connected account (idempotency), connected account created with warnings / state_reason (partial success), and directory sync rate-limited (429). Each needs a distinguishable internal error code, not an upstream message.revoke_link converts an out-of-band support action into a product action. Low effort, measurable ticket reduction.state_reason values are an enum or open strings; (c) whether the American-spelling chore renamed any identifiers.external_user_id on GenerateLink removes a lookup from the Admin Portal path — a small but real reduction in per-request fan-out.DirectorySyncRateLimitError confirms WorkOS enforces limits we must respect per-endpoint. Directory syncs are inherently bursty; schedule them off-peak and stagger per-directory.organization_id filters plus org-scoped data-integration endpoints mean list calls are safer but more varied. Index coverage on tenancy columns still applies.| Mode | Trigger | Blast radius | Mitigation |
|---|---|---|---|
| Fatal class-not-found | References to removed AddDataIntegrationsUpsert*Request / ConnectionOwner enums |
Build-time / any code path touching Pipes upsert | Grep before bumping; confine SDK types to the wrapper |
| Duplicate connected accounts | Idempotency contract changed; retries/queue replay on 4 Pipes POSTs | Data integrity, per-tenant, hard to unwind | Central idempotency helper; disable auto-retry on those writes until wired |
| Silent partial success | ConnectedAccountWriteResponseWarning / state_reason treated as success |
Users think integrations are healthy | Log + surface warnings; three-state runbook |
| Misclassified errors | ~35 endpoints changed error contracts; stale renderable() mapping |
400s rendered as 500s, or vice versa | Normalized taxonomy + contract tests; prioritize portal/generate_link |
| Mass logout | Tokens without exp now rejected (v9.4, unresolved) |
All authenticated users on upgrade | Inventory + backfill/invalidate before cutover; staged rollout |
| Auth loop | Issuer misconfigured; 401 → redirect → 401 | Subset by environment | Staging-first enablement; distinguishable code; config kill switch |
| Broken MFA enrollment | TOTP secret/uri format changed (v9.4, unresolved) |
New MFA enrollees only | Re-verify enrollment UI; treat values as opaque |
| Broken deep links | Passwordless URL double-encoded (v9.4, unresolved) | New link generation only | Remove app-side urlencode; end-to-end link test |
| Unhandled 500s | AccountSelectionRequiredError uncaught |
Pipes users | Named exception + render() before Pipes adoption |
| Retry storm | Directory sync 429 unhandled; Http::retry keeps hammering |
Directory sync + downstream jobs | Catch DirectorySyncRateLimitError; backoff per Retry-After |
| Truncated values | Widened formats hitting varchar(255) |
Silent data loss | Column audit; text where WorkOS data is stored |
| Rollback friction | Behavioural changes not config-flaggable; idempotency is server-side | Recovery time | Keep ^8 branch deployable; note 9.5→9.4 does not restore old idempotency |
^9.5 directly (recommended — one bump instead of two, and the v9.4 security fixes close a real validation gap) versus deferring both.revoke_link + external_user_id now as a support-cost reduction, independent of the Pipes roadmap.How can I help you explore Laravel packages today?