Skip to Content

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):

FreeProEnterprise
CredentialAPI keyAPI keylicense file
Bootsdesign_studioan empty org, or the --scenario you namesame as Pro
POST /_rystic/resetre-applies design_studiowipes to emptywipes to empty
Okta API (all 49 routes)yesyesyes
Control planeversion, routes, reset onlyevery routeevery 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.

  1. A request with no Authorization header answers 403 E0000006. That refusal is not measured against Okta.
  2. GET /api/v1/users/me answers 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.

FamilyRoutes
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}
  1. Bodies are JSON both ways. The user and group lists take q, search and filter, and page with limit and an after cursor in a Link header.
  2. A first DELETE on a user deactivates it (DEPROVISIONED); a second purges it. A plain user list leaves DEPROVISIONED users out; search and filter include them.
  3. A duplicate login answers 400 E0000001; every refusal carries Okta’s errorCode, errorSummary, errorLink, errorId and errorCauses.

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.

RouteWhat it doesPlan (feature)
GET /_rystic/versionhealth check: product, version, model hash, edition, auth_scheme, initial_clockevery plan
GET /_rystic/routesevery Okta API route this twin serves; not the control planeevery plan
POST /_rystic/resetFree: back to design_studio. Pro, Enterprise: wipe every record, rewind the id sequence and the clockevery plan
GET /_rystic/scenariosthe shipped worldsPro, Enterprise (scenarios)
POST /_rystic/scenario{"name":"design_studio"}: reset, then load the worldPro, Enterprise (scenarios)
GET /_rystic/inspectdump the entire statePro, Enterprise (state_view)
GET /_rystic/registersevery seedable register and its fieldsPro, Enterprise (state_view)
GET /_rystic/egressthe outbound effect log; Okta declares none, so it is always emptyPro, Enterprise (state_view)
GET /_rystic/eventslive activity stream (server-sent events) the state UI readsPro, Enterprise (state_view)
POST /_rystic/seedland a register mapPro, Enterprise (state_set)
POST /_rystic/clock{"time":"<RFC3339>"} pins the clock, {"advance_seconds":N} moves itPro, Enterprise (state_set)
POST /_rystic/seed-rng{"seed":N} fixes the RNGPro, Enterprise (state_set)
POST /_rystic/egress-retain{"retain":N} bounds the egress log; 0 = unboundedPro, Enterprise (state_set)
GET, POST /_rystic/networkread or set a network profile: rejected requests and latency (fault injection)Pro, Enterprise (faults)
GET /_rystic/uithe state UIPro, Enterprise (ui)
GET /_rystic/wsopen WebSocket connections; Okta has no WebSocket feed, so nonePro, Enterprise (state_view)
POST /_rystic/ws-dropcut WebSocket connections; there are none to cutPro, Enterprise (faults)
GET /_rystic/activitythe usage counters the CLI reads; any other caller gets 403every 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.

  1. Free: POST /_rystic/reset re-applies design_studio, clock and id sequence included.
  2. Pro, Enterprise: reset wipes to empty and rewinds the sequence and the clock; POST /_rystic/clock and POST /_rystic/seed-rng pin 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

  1. Nothing is sent and nobody signs in: activation and password reset send nothing, activationUrl and resetPasswordUrl name the twin’s own origin and serve nothing, lastLogin never moves, and LOCKED_OUT is reachable only from a seed.
  2. Every change happens in the request that makes it; nothing transitions on its own.
  3. No password policy, and passwordless activation is not modelled: any password is accepted, and a user activated without one lands ACTIVE at once.
  4. Factors are OKTA software TOTP only. Compute codes from the enrollment’s sharedSecret at the twin’s clock, not wall time. Other factor types enroll as PENDING_ACTIVATION; activating or verifying one answers 501.
  5. Applications are bookmark only: another sign-on mode is stored without the credentials Okta would generate.
  6. A design_studio record read as stored (a user, a group, a group’s members) keeps _links on https://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’s Link header, the users list’s _links, activationUrl and resetPasswordUrl, are on the origin you reached the twin on (RYS-1386).
  7. No rate limits: no X-Rate-Limit-* headers, never a 429.
  8. Ids and errorId values 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

  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. (RYS-1384)

  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. (RYS-1065)

  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. (RYS-1135)

  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. (RYS-1177)

  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. (RYS-1384)

Last updated on