Simulators
Okta simulator
okta-twin is a local, stateful simulator of the Okta Management API — users and their lifecycle, groups and memberships, bookmark applications and their assignments, password operations and software-TOTP factors. It never calls Okta: no activation email is sent and no real directory changes.
Quickstart
You need an API key from Account → API keys (how).
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_… # your API key
rystic run okta-twin -d --port 0 --scenario design_studio # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version
Expect "product":"okta-twin" and "version":"0.0.1". An older version means an older install; rystic pull okta-twin upgrades it. --scenario design_studio boots the same world on every plan. The examples below use localhost:8080; use the port rystic run printed.
On Enterprise, rystic login ./rystic-license.json takes the place of the key (license files).
curl -s -H 'Authorization: SSWS placeholder' -H 'Content-Type: application/json' \
'localhost:8080/api/v1/users?activate=false' \
-d '{"profile":{"firstName":"New","lastName":"Hire","email":"new.hire@wdcontractors.example","login":"new.hire@wdcontractors.example"}}'
# -> {...,"id":"00u00000000000000001",...,"status":"STAGED",...}
Point the SDK at it — the org URL only, code unchanged. The twin serves plain HTTP, so turn off the SDK’s HTTPS check for tests:
client = OktaClient({
"orgUrl": "http://localhost:8080",
"token": "placeholder",
"testing": {"disableHttpsCheck": True},
})
Plans
What okta-twin does on each plan (plans):
| Free | Pro | Enterprise | |
|---|---|---|---|
| Credential | API key | API key | license file |
| Boots | design_studio | an empty org, or the --scenario you name | same as Pro |
POST /_rystic/reset | re-applies design_studio | wipes to empty | wipes to empty |
| Okta API (all 49 routes) | yes | yes | yes |
| Control plane | version, routes, reset only | every route | every route |
Free runs 5 hours of simulator time a month. It serves the whole Okta surface against design_studio. Every control route beyond GET /_rystic/version, GET /_rystic/routes and POST /_rystic/reset answers 403, naming the feature it needs (the loopback-only GET /_rystic/activity aside):
{"feature":"state_set","message":"/_rystic/seed needs the \"state_set\" feature, which this plan does not include; upgrade at https://www.rystic.ai/pricing","upgrade_url":"https://www.rystic.ai/pricing"}
Free also refuses these boot inputs, and the twin exits naming the feature: --scenario naming any other world (scenarios), --seed or RYSTIC_SEED (state_set), and RYSTIC_NETWORK (faults). See boot inputs.
Pro opens every control route below. Without --scenario the twin boots an empty org, and POST /_rystic/reset wipes it back to empty; load design_studio with --scenario design_studio or POST /_rystic/scenario.
Enterprise is Pro on a license file instead of an API key.
Auth
Okta takes an API token as Authorization: SSWS <token>. The twin accepts any non-empty Authorization value, SSWS or Bearer, and checks no token shape, admin role, scope or expiry; no Okta org or token is involved.
- A request with no
Authorizationheader answers 403E0000006. That refusal is not measured against Okta. GET /api/v1/users/meanswers 404: the twin has no token owner.
curl -s -H 'Authorization: SSWS placeholder' localhost:8080/api/v1/users
Coverage
49 routes on the Management API. GET /_rystic/routes returns the same list from a running twin.
| Family | Routes |
|---|---|
| Users (7) | GET, POST /api/v1/users; GET, POST, PUT, DELETE /api/v1/users/{id}; GET /api/v1/users/{id}/groups |
| User lifecycle (9) | POST /api/v1/users/{id}/lifecycle/ activate, deactivate, suspend, unsuspend, reactivate, expire_password, expire_password_with_temp_password, reset_password, reset_factors |
| Credentials (1) | POST /api/v1/users/{userId}/credentials/change_password |
| Factors (7) | GET, POST /api/v1/users/{userId}/factors; GET …/factors/catalog; GET, DELETE …/factors/{factorId}; POST …/factors/{factorId}/lifecycle/activate, POST …/factors/{factorId}/verify |
| Groups (8) | GET, POST /api/v1/groups; GET, PUT, DELETE /api/v1/groups/{groupId}; GET …/{groupId}/users; PUT, DELETE …/{groupId}/users/{userId} |
| Applications (7) | GET, POST /api/v1/apps; GET, PUT, DELETE /api/v1/apps/{appId}; POST …/{appId}/lifecycle/activate, …/deactivate |
| App assignments (10) | GET, POST /api/v1/apps/{appId}/users; GET, POST, DELETE …/users/{userId}; GET /api/v1/apps/{appId}/groups; GET, PUT, PATCH, DELETE …/groups/{groupId} |
- Bodies are JSON both ways. The user and group lists take
q,searchandfilter, and page withlimitand anaftercursor in aLinkheader. - A first
DELETEon a user deactivates it (DEPROVISIONED); a second purges it. A plain user list leavesDEPROVISIONEDusers out;searchandfilterinclude them. - A duplicate
loginanswers 400E0000001; every refusal carries Okta’serrorCode,errorSummary,errorLink,errorIdanderrorCauses.
Not modelled: OAuth 2.0 and OIDC, browser and Identity Engine sign-in, sessions, unlock, roles and grants, group rules and owners, linked objects, the System Log, hooks, and every other Management API family. An unserved route answers 404 {"message":"No route in twin for …"}.
Control plane
Every route under /_rystic/, and the plan that reaches it. Routes by plan covers the whole control plane.
| Route | What it does | Plan (feature) |
|---|---|---|
GET /_rystic/version | health check: product, version, model hash, edition, auth_scheme, initial_clock | every plan |
GET /_rystic/routes | every Okta API route this twin serves; not the control plane | every plan |
POST /_rystic/reset | Free: back to design_studio. Pro, Enterprise: wipe every record, rewind the id sequence and the clock | every plan |
GET /_rystic/scenarios | the shipped worlds | Pro, Enterprise (scenarios) |
POST /_rystic/scenario | {"name":"design_studio"}: reset, then load the world | Pro, Enterprise (scenarios) |
GET /_rystic/inspect | dump the entire state | Pro, Enterprise (state_view) |
GET /_rystic/registers | every seedable register and its fields | Pro, Enterprise (state_view) |
GET /_rystic/egress | the outbound effect log; Okta declares none, so it is always empty | Pro, Enterprise (state_view) |
GET /_rystic/events | live activity stream (server-sent events) the state UI reads | Pro, Enterprise (state_view) |
POST /_rystic/seed | land a register map | Pro, Enterprise (state_set) |
POST /_rystic/clock | {"time":"<RFC3339>"} pins the clock, {"advance_seconds":N} moves it | Pro, Enterprise (state_set) |
POST /_rystic/seed-rng | {"seed":N} fixes the RNG | Pro, Enterprise (state_set) |
POST /_rystic/egress-retain | {"retain":N} bounds the egress log; 0 = unbounded | Pro, Enterprise (state_set) |
GET, POST /_rystic/network | read or set a network profile: rejected requests and latency (fault injection) | Pro, Enterprise (faults) |
GET /_rystic/ui | the state UI | Pro, Enterprise (ui) |
GET /_rystic/ws | open WebSocket connections; Okta has no WebSocket feed, so none | Pro, Enterprise (state_view) |
POST /_rystic/ws-drop | cut WebSocket connections; there are none to cut | Pro, Enterprise (faults) |
GET /_rystic/activity | the usage counters the CLI reads; any other caller gets 403 | every plan, loopback callers only |
Scenarios
design_studio is the one shipped world, and the one Free boots: Website Design Contractors’ org on 2026-03-01T00:00:00Z. 36 users on wdcontractors.example — 33 ACTIVE, three DEPROVISIONED ex-staff — in ten groups: the BUILT_IN Everyone group and nine OKTA_GROUPs (Leadership, Development, Design, Content, Project Management, New Business, Operations, Freelancers, Okta Admins). Every profile carries title, department, manager and managerId, so the org chart walks down from the owner. The same staff are in the studio’s Slack workspace. No applications are seeded.
curl -s -g -H 'Authorization: SSWS placeholder' \
'localhost:8080/api/v1/users?search=profile.department%20eq%20%22Design%22'
# -> the eight designers, each profile naming its manager
To list the worlds: GET /_rystic/scenarios (Pro); rystic docs okta-twin scenarios prints the archive’s SCENARIOS.md (the container image does not carry it); --list-scenarios on the installed binary works on every plan without a key:
~/.rystic/twins/okta-twin/0.0.1/okta-twin --list-scenarios
Seeding state
Pro and Enterprise. POST /_rystic/seed takes a register map — state_user, state_group, state_group_membership (keyed <groupId>/<userId>), state_application, state_app_user, state_app_group_assignment, state_user_factor, state_user_password, state_factor_challenge — and lands it directly; GET /_rystic/registers lists every field. A scenario file booted with --seed <file> or RYSTIC_SEED=<file> wraps the same map in seed, beside optional clock, rng_seed and network; a bare register map boots too.
curl -X POST localhost:8080/_rystic/seed -d '{"state_user":{"00useed0000000000001":{"id":"00useed0000000000001","status":"ACTIVE","type":{"id":"otyseed000000000001"},"credentials":{"provider":{"name":"OKTA","type":"OKTA"}},"profile":{"firstName":"Seed","lastName":"User","email":"seed@example.com","login":"seed@example.com"}}}}'
curl -s -H 'Authorization: SSWS x' localhost:8080/api/v1/users/00useed0000000000001
Success returns 204. Only an unknown register or a record that is not a JSON object is refused (400, and an unknown register’s error names every valid one); a record missing required fields, or carrying a misspelled one, is accepted with WARNING — incomplete seed in the log unless RYSTIC_SEED_GATE=reject.
Determinism & reset
Execution is serialized, ids are a sequence (00u00000000000000001, 00g00000000000000001, 0oa00000000000000001) and the clock starts at a pinned instant — 2026-01-01T00:00:00Z empty, 2026-03-01T00:00:00Z in design_studio — and moves one millisecond per request that stamps a time (a write), never by wall time. Two identical runs from the same reset give byte-identical users and groups, down to id, created, lastUpdated and statusChanged.
- Free:
POST /_rystic/resetre-appliesdesign_studio, clock and id sequence included. - Pro, Enterprise: reset wipes to empty and rewinds the sequence and the clock;
POST /_rystic/clockandPOST /_rystic/seed-rngpin the rest.
State is in memory: a restart boots the plan’s world again — design_studio on Free; on Pro and Enterprise whatever --scenario or --seed names, or an empty org.
Egress
None. Okta’s model declares no outbound effects: the twin sends no email and calls no hook, and GET /_rystic/egress is always empty.
Limitations
- Nothing is sent and nobody signs in: activation and password reset send nothing,
activationUrlandresetPasswordUrlname the twin’s own origin and serve nothing,lastLoginnever moves, andLOCKED_OUTis reachable only from a seed. - Every change happens in the request that makes it; nothing transitions on its own.
- No password policy, and passwordless activation is not modelled: any password is accepted, and a user activated without one lands
ACTIVEat once. - Factors are OKTA software TOTP only. Compute codes from the enrollment’s
sharedSecretat the twin’s clock, not wall time. Other factor types enroll asPENDING_ACTIVATION; activating or verifying one answers 501. - Applications are bookmark only: another sign-on mode is stored without the credentials Okta would generate.
- A
design_studiorecord read as stored (a user, a group, a group’s members) keeps_linksonhttps://wdcontractors.okta.com, the org it was captured from: resolve the link’s path against your org URL. Links the twin builds, every list page’sLinkheader, the users list’s_links,activationUrlandresetPasswordUrl, are on the origin you reached the twin on (RYS-1386). - No rate limits: no
X-Rate-Limit-*headers, never a 429. - Ids and
errorIdvalues are the twin’s own sequences, not Okta’s random ids.
Fidelity is scored differentially against a live Okta Integrator Free Plan org with an SSWS token, per response field: the last full-suite round matched 143 of 143 probes on 2026-10-01. The suite sends SSWS requests only, so it shows nothing about invalid credentials, admin privileges, scopes or OAuth; the DEPROVISIONED-user refusal (403 E0000038) is that org’s policy with deactivated-profile edits turned off. Each release states its own number.
The full guide ships in the release as HOWTO.md; rystic docs okta-twin prints it.
Release notes
Newest first.
v0.0.1 — 2026-10-01
-
A local Okta Management API.
okta-twinserves 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-emptyAuthorizationheader and never calls Okta, so no activation email is sent and no real directory changes. (RYS-1384) -
Okta’s user lifecycle, two-step delete included. Users move through
STAGED,ACTIVE,SUSPENDEDandDEPROVISIONEDby the lifecycle routes. A firstDELETEdeactivates a user and a second purges it; a plainGET /api/v1/usersleavesDEPROVISIONEDusers out whilesearchandfilterinclude them; and a profile update on aDEPROVISIONEDuser answers 403E0000038, as an org with deactivated-profile edits turned off does. (RYS-1065) -
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:totpfactor 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. (RYS-1135) -
One shipped world,
design_studio. Website Design Contractors’ Okta org: 36 users (33ACTIVE, threeDEPROVISIONEDex-staff) in ten groups, Everyone included, each profile carrying title, department and manager. Free boots it andPOST /_rystic/resetreturns to it; Pro and Enterprise boot an empty org unless--scenario design_studionames it. (RYS-1177) -
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.mdstates what the suite leaves out and the differences this release ships with. (RYS-1384)