Skip to Content
SimulatorsOkta changelog

Simulators

Okta changelog

What changed in each Okta simulator release, newest first.

v0.0.5 — 2026-11-06

  1. The docs say what the one org holds. The connect section points the SDK’s org URL at the simulator, and the limitations list the users, groups and apps that belong to the org.

v0.0.4 — 2026-11-05

  1. Reactivating a user moves activated, as Okta does. When POST /api/v1/users/{id}/lifecycle/reactivate returns a RECOVERY user to ACTIVE, the user’s activated now reads the reactivation time, alongside statusChanged and lastUpdated. Before, it kept the value from when the user was created.

  2. Setting the clock never stamps records before the timeline events it fired. When POST /_rystic/clock {"time": ...} fires timeline events due at its target, the next request’s records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

  3. A paced clock keeps wall time, and rystic run --speed 0 stops it. Under RYSTIC_PACE, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. rystic run hands the twin only the pace it resolved, so --speed 0, a scenario without events, --tape and --live keep the clock manual even when your shell exports RYSTIC_PACE.

  4. Swagger UI to read and call the twin from the browser. /_rystic/swagger, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin.

  5. A request with no Authorization header is refused on every route, as Okta refuses it. The twin answers 403 E0000005 (Invalid session) before it reads the path or body, where its routes answered 403 E0000006 or, for an id that does not exist, 404 E0000007. Any non-empty Authorization value is still accepted.

  6. GET /api/v1/users/me answers the user who owns the calling token, as Okta does. It answered 404. A credential bound with POST /_rystic/credentials, or an X-Rystic-Actor, answers that actor; any other answers the scenario’s token owner: a placeholder ACTIVE user the twin now seeds in an otherwise empty org, or Studio Bot, a new Okta Admins integration account, in design_studio. me also answers the token owner on /users/me/groups, /users/me/factors, /users/me/factors/catalog and /users/me/factors/{factorId}. Okta’s Terraform provider now gets past its credential check against the twin.

  7. Every scenario holds Okta’s BUILT_IN Okta Administrators group beside Everyone. GET /api/v1/groups?filter=type eq "BUILT_IN" lists both, as an Okta org does, in an empty scenario, after a reset and in design_studio. Okta Administrators lists no members, is in no user’s groups, and refuses profile and membership writes as Everyone does.

  8. GET /.well-known/okta-organization answers the twin’s org metadata, with no credential, as Okta does. It answered 404, which stopped Okta’s SDKs and Terraform provider at their first call. It answers a placeholder org id, pipeline idx, the org’s settings and the org URL on the origin you reached, and ignores any Authorization header sent with it.

  9. Your Okta org’s own host reaches the twin under rystic exec and --intercept. The twin claimed only subdomain.okta.com, the placeholder in Okta’s spec, so an SDK or the Terraform provider configured with https://<org>.okta.com went to Okta itself. It now claims okta.com and oktapreview.com, which cover every org on them. An org on a custom domain still points its orgUrl at the twin. As for every simulated host, a project with no okta twin running with --intercept now refuses calls to those domains rather than sending them to Okta.

  10. Assigning an app to Everyone assigns it to every user, links are absolute, and EMEA and government orgs reach the twin. PUT /api/v1/apps/{appId}/groups/{everyoneId} assigned no one in an org with no stored memberships. Now every user gets a GROUP app user whose _links.group names Everyone, as in Okta, and so does a user you create afterwards, STAGED included. Unassigning Everyone removes those app users, reactivating a user brings them back, and leaving or deleting another group no longer drops an app Everyone still assigns. Every user, factor and app answer now gives its _links hrefs as absolute URLs on the origin your client reached, where /users/me, POST /users and a few others gave a bare path. The twin also claims okta-emea.com, okta-gov.com and okta.mil beside okta.com and oktapreview.com, so a client configured with an EMEA, US government or DoD org URL reaches it under rystic exec and --intercept.

  11. The docs page lists the clients known to work. A new Clients known to work section names each official client checked against this simulator, the release checked, the ways it connects that work and fail, the day it was checked, and any known gap that covers a failure.

  12. The Python SDK snippet turns off the HTTPS check in a way okta 3.x honors. The docs passed "testing": {"disableHttpsCheck": True} to OktaClient, which okta 3.x ignores, so the client refused the twin’s http:// org URL with “Your Okta URL must start with ‘https’”. Set OKTA_TESTING_TESTINGDISABLEHTTPSCHECK=true in the environment instead; the client code keeps only the org URL and token.

  13. The first calls of the Okta Node.js and Python SDKs and the Terraform provider are now measured against Okta. The calls @okta/okta-sdk-nodejs 8.2.0, okta 3.4.7 and okta/okta 7.0.0 send first are now among the simulator’s tests, which compare them with real Okta in every full round: GET /api/v1/users?limit=1, GET /api/v1/users/me and GET /api/v1/groups?q=Everyone. The twin’s behavior does not change, and its /users/me answer does not yet carry Okta’s _links.changePassword, which these tests do not compare.

  14. A state dump seeds back as it was, and the next record gets a fresh id. GET /_rystic/inspect posted to POST /_rystic/seed after a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds. GET /_rystic/inspect?view=sequences lists every id counter, and a world document’s new sequences key restores them, so ids the simulator renders from a counter continue where the dump left off.

  15. A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

  16. The call log shows what each call asked for and whether it succeeded. Each GET /_rystic/calls entry now carries args, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), and ok, false for an answer that is not 2xx. A path the simulator does not serve is marked unknown_route: true. ?tail=N returns only the last N calls, and dropped counts the calls the log no longer keeps.

  17. The docs are one page, with a page per feature. The archive ships README.md in place of HOWTO.md, with the same sections in the same order as every Rystic simulator’s page, and CHANGELOG.md beside it. User lifecycle and passwords, software TOTP factors and bookmark applications each have their own page under docs/features/, and every example on these pages runs as written against a fresh simulator.

  18. The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.

  19. Scenario descriptions use customer terms. The Scenarios table and --list-scenarios describe each scenario in American spelling, with no internal references.

  20. The Scenarios table is easier to scan. Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.

  21. Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.

  22. Known clients name their registry. The table at the end of Connect your client names each client’s package registry beside it, such as npm, PyPI or GitHub Releases.

  23. Limitations is shorter to read. It opens with the known issues and one sentence counting the rest. The detailed lists follow, and the website folds them behind “Show all limitations”.

  24. The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.

  25. The docs read more plainly, mark where the simulator differs from Okta and keep only the simulator’s own facts. Sentences are shorter, and a “Differs from Okta” line sits beside the behavior it describes, such as the token checks the simulator skips. Rules with several outcomes, such as which user GET /api/v1/users/me returns or what a lifecycle route returns for sendEmail, are tables. The seeding and reset sections keep the registers, ids, clock and what reset clears. They link the shared pages for the rest.

v0.0.3 — 2026-10-12

  1. A world can carry its own timeline. A --seed file, or a complete world document posted to POST /_rystic/seed, may add credentials, binding presented credentials to actors, and events: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level credentials or events key the twin used to ignore now takes effect. RYSTIC_PACE=<speed> plays the clock, and the timeline, in real time, and an event’s at may be +<duration>. /_rystic/inject, --inject and RYSTIC_INJECT now need state_set instead of scenarios.

  2. A timeline call can hand what it minted to later events. A call event may add bind, {"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as ${name} in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a ${name} no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

  3. design_studio carries a timeline. As the clock moves past 2026-03-01T00:00:00Z, the admin Noor Haddad adds Hannah Kim to Content and retitles her, suspends the freelancer Ava Chen and removes her from Freelancers, then creates and activates Lena Okafor and adds her to Freelancers, each a call on the twin’s own API under Noor’s bound token. A test that advances the clock past +1h now sees these changes, and Lena takes the first user ID the twin mints.

  4. A clock jump fires each timeline event at its own time. When one POST /_rystic/clock or paced tick passes several events, each fires with the clock at its at, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

  5. A clock jump stamps each timeline event at its exact instant. When a clock move passes timeline events, the records each one makes carry exactly its at, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

  6. A timeline call can carry raw bytes, XML or a JSON array. A call event may send body_text or body_base64 instead of body; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and ${name} fills in body_text. A body may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as /InitiateMultipartUploadResult/UploadId.

v0.0.2 — 2026-10-11

  1. A group lookup’s error ID comes from its own range. An error from GET /api/v1/groups/{groupId} carries an errorId such as oae6f3c83c1-0474-49d0-8000-000000000001 rather than oae00000001-0000-4000-8001-000000000001. Ids still count up in their last digits and restart on reset; a test that pinned an old errorId needs the new one.

  2. The test fixtures seed transitioningToStatus: null. The four okta_* fixtures under sim/testdata/scenarios/ seeded an empty string for a user with no transition in progress, which PUT /api/v1/users/{id} and GET /api/v1/groups/{groupId}/users echoed back. They now seed null, as the twin’s own lifecycle operations write it.

  3. Offboarding a user removes their app access, as Okta does. Deactivating a user, by POST /api/v1/users/{id}/lifecycle/deactivate or a first DELETE, and purging one remove their app assignments, direct and group-derived: GET /api/v1/apps/{appId}/users/{userId} answers 404 E0000007 and the app’s user list leaves them out. A group assigned to an app afterwards gives a deactivated member no access, adding a deactivated user to a group is refused 403 E0000038, and activating the user again restores their group-derived access but not a direct assignment. Each was measured against a live Okta org.

  4. Deleting a group, an app or a user removes what names it, as Okta does. After DELETE /api/v1/groups/{groupId}, GET /api/v1/apps/{appId}/groups/{groupId} answers 404 E0000007, and the app access members held only through that group goes with it. A direct assignment, or access held through another assigned group, stays. Deleting an app removes its user and group assignments, and purging a user removes their enrolled factors. The group behavior was measured against a live Okta org.

  5. A JSON Patch on an app’s group assignment is applied. PATCH /api/v1/apps/{appId}/groups/{groupId} with an array such as [{"op":"replace","path":"/priority","value":9}] now changes the assignment: the answer and a later GET carry the replaced priority, profile or profile attribute, and an empty array changes nothing. Before, the twin answered 200 and left the assignment as it was.

  6. A user write must carry the profile properties Okta requires. POST /api/v1/users and PUT /api/v1/users/{id} refuse a profile that omits, nulls or blanks login, firstName, lastName or email with 400 E0000001 and one "<property>: The field cannot be left blank" cause per such property, plus Okta’s "login: Username must be in the form of an email address" when a create has no login. POST /api/v1/users/{id} may still leave them out, but refuses nulling or blanking one. Before, the twin created a user without firstName, lastName or email, replaced one without firstName or lastName, and let a partial update null either name.

  7. Every link in design_studio leads back to the twin. Seeded users’ and groups’ _links, on every route that answers them, are on the origin you reached the twin on, as the links the twin builds already were. They named a host on Okta’s own domain, so following one left the twin; a test that resolved a link’s path against its org URL can now follow the link as it is. The four okta_* test fixtures under sim/testdata/scenarios/ moved the same way.

  8. Listed users no longer carry realmId. GET /api/v1/users answers each user without it, as Okta does and as GET /api/v1/users/{id} already did. Before, a user given a realmId by a write or a seed showed it in the list and not in its own read. Measured against Okta’s reference org, whose lists and reads carry no realmId.

  9. The scripts in this archive are MIT-licensed. check.sh and the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them. LICENSE.txt no longer covers them; it still governs the twin itself.

v0.0.1 — 2026-10-01

  1. A local Okta Management API. okta-twin serves 49 routes of Okta’s Management API — users and their lifecycle, groups and memberships, bookmark applications with their user and group assignments, password operations and OKTA software-TOTP factors — plus the /_rystic/ control plane for reset, seeding and the clock. It accepts any non-empty Authorization header and never calls Okta, so no activation email is sent and no real directory changes.

  2. Okta’s user lifecycle, two-step delete included. Users move through STAGED, ACTIVE, SUSPENDED and DEPROVISIONED by the lifecycle routes. A first DELETE deactivates a user and a second purges it; a plain GET /api/v1/users leaves DEPROVISIONED users out while search and filter include them; and a profile update on a DEPROVISIONED user answers 403 E0000038, as an org with deactivated-profile edits turned off does.

  3. Password operations and software-TOTP factors. Reset, change and expire a password, reactivate a user and reset their factors; list a user’s factor catalog, and enroll, read, activate, verify and delete an OKTA token:software:totp factor with codes computed from its shared secret. Other factor types enroll but do not activate, and OAuth/OIDC and browser sign-in are not served.

  4. One shipped world, design_studio. Website Design Contractors’ Okta org: 36 users (33 ACTIVE, three DEPROVISIONED ex-staff) in ten groups, Everyone included, each profile carrying title, department and manager. Free boots it and POST /_rystic/reset returns to it; Pro and Enterprise boot an empty org unless --scenario design_studio names it.

  5. Measured against a live Okta org. The 2026-10-01 full-suite round matched 143 of 143 probes against an Okta Integrator Free Plan org. HOWTO.md states what the suite leaves out and the differences this release ships with.

Last updated on