Skip to Content
SimulatorsAnthropic

Simulators

Anthropic simulator

anthropic-twin is a local, stateful simulator of the Claude API on api.anthropic.com — Messages and token counting, models, files, message batches with their JSONL results, skills with their versions and the Managed Agents beta: agents, environments, sessions with their events and resources, vaults with their credentials and memory stores with their memories and versions. It answers the API’s shapes without calling a model: a Messages reply is an assistant message with a stop reason and usage, and its text is the twin’s own, so test the code around the model, not the model’s answers. Nothing reaches Anthropic and no tokens are billed.

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 anthropic-twin -d --port 0 --scenario anthropic_skill_library    # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version

Expect "service_hosts":["api.anthropic.com"]. 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).

Send a message, then create a skill and read it back:

A=(-H 'x-api-key: sk-ant-placeholder' -H 'anthropic-version: 2023-06-01')
curl -s "${A[@]}" -H 'content-type: application/json' localhost:8080/v1/messages \
  -d '{"model":"claude-haiku-4-5-20251001","max_tokens":64,"messages":[{"role":"user","content":"Say hello"}]}'
# -> {…,"role":"assistant","stop_reason":"end_turn",…,"type":"message","usage":{…}}

mkdir -p release-notes && printf -- '---\nname: release-notes\ndescription: Drafts release notes.\n---\nWrite notes.\n' > release-notes/SKILL.md
curl -s "${A[@]}" localhost:8080/v1/skills -F 'display_title=Release notes' \
  -F 'files[]=@release-notes/SKILL.md;filename=release-notes/SKILL.md'
# -> {…,"display_name":"Release notes","id":"skill_01J00000000000000000000001",…}
curl -s "${A[@]}" localhost:8080/v1/skills/skill_01J00000000000000000000001
# -> {…,"display_name":"Release notes",…,"type":"skill",…}

Point your client at it

Anthropic’s SDKs build every URL on https://api.anthropic.com: set the base URL to the twin, and nothing else changes. A request left unpointed goes to the real Claude API, and is billed. The twin reads the key from x-api-key or Authorization: Bearer, whichever your SDK release sends.

Python (anthropic):

import anthropic

client = anthropic.Anthropic(base_url="http://localhost:8080", api_key="sk-ant-placeholder")
client.messages.create(model="claude-haiku-4-5-20251001", max_tokens=64,
                       messages=[{"role": "user", "content": "Say hello"}])

TypeScript (@anthropic-ai/sdk):

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ baseURL: "http://localhost:8080", apiKey: "sk-ant-placeholder" });
await client.skills.list();

The SDKs’ beta methods (client.beta.agents, client.beta.sessions and the rest) reach the twin too, with the ?beta=true they send.

Plans

anthropic-twin is on Pro and Enterprise, not Free (plans); on a Free key, rystic pull anthropic-twin and a start are refused with product_not_in_plan. Both plans serve all 73 Claude API routes, every control route and all five scenarios. Without --scenario the twin boots an empty account — the API’s 14 models and no skill, file, batch or agent — and POST /_rystic/reset wipes it back to that.

Auth

The twin takes any API key, in x-api-key or as Authorization: Bearer; no Anthropic account or key is involved, and the same key in both headers reads as one. A request with neither is refused 401 authentication_error before any record is looked up; a file upload without one answers 404 not_found_error instead. The Managed Agents routes take anthropic-beta: managed-agents-2026-04-01, and a memory-store request without it answers 404 not_found_error, as on the API. Every request also carries anthropic-version, as the SDKs send it: the twin accepts 2023-01-01, 2023-06-01 and 2026-10-21 and answers them alike, and a missing, empty or unknown version answers 400 invalid_request_error.

Coverage

The twin serves 73 operations. Messages: create, answered from a model the account’s model table lists (any other model answers 404 not_found_error “model: ”) and stopping at max_tokens with stop_reason max_tokens, and count_tokens; an alias such as claude-sonnet-4-5 names its dated model on both. A body that is not JSON, a missing model, messages or max_tokens and an empty messages list answer the API’s 400 invalid_request_error, and Claude Opus 4.7 and every newer model refuse a temperature other than 1.0, any top_k and a top_p below 0.99, as on the API. Models: list (newest first) and read by id or alias, each with its capabilities in the API’s shape. Files: multipart upload with an optional expires_in_seconds, list, read, delete and download — a file you upload is not downloadable, and its /content answers 400 file_not_downloadable, as on the API. Message batches: create (in_progress, expiring 24 hours on), list, read, cancel (the batch ends at once, every request canceled), results of an ended batch as application/x-jsonl and delete of an ended batch. Skills: create from files[] parts with a SKILL.md at their root, list, read, delete and versions created, listed, read and deleted, each new version becoming latest_version_id. Managed Agents: agents created, read, updated into new versions and archived; environments, vaults and their credentials and memory stores, each created, read, updated, archived and deleted; a memory store’s memories written, read and deleted, each change kept as a memory version; sessions created from an agent and an environment, their events sent and listed, their resources managed. Every error but the streaming refusal is the API’s envelope, {"type":"error","error":{"type","message"}}.

73 routes, counted from the simulator’s literal registrations.

ResourceOperations
Agent (5)GET, POST /v1/agents; GET, POST /v1/agents/{agent_id}; POST /v1/agents/{agent_id}/archive
Agent Version (1)GET /v1/agents/{agent_id}/versions
Environment (6)GET, POST /v1/environments; GET, POST, DELETE /v1/environments/{environment_id}; POST /v1/environments/{environment_id}/archive
File (5)GET, POST /v1/files; GET, DELETE /v1/files/{file_id}; GET /v1/files/{file_id}/content
Memory (3)POST /v1/memory_stores/{memory_store_id}/memories; POST, DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}
Memory Store (8)GET, POST /v1/memory_stores; GET, POST, DELETE /v1/memory_stores/{memory_store_id}; POST /v1/memory_stores/{memory_store_id}/archive; GET /v1/memory_stores/{memory_store_id}/memories; GET /v1/memory_stores/{memory_store_id}/memories/{memory_id}
Memory Version (2)GET /v1/memory_stores/{memory_store_id}/memory_versions; GET /v1/memory_stores/{memory_store_id}/memory_versions/{memory_version_id}
Message Batch (6)GET, POST /v1/messages/batches; GET, DELETE /v1/messages/batches/{message_batch_id}; POST /v1/messages/batches/{message_batch_id}/cancel; GET /v1/messages/batches/{message_batch_id}/results
Model (4)POST /v1/messages; POST /v1/messages/count_tokens; GET /v1/models; GET /v1/models/{model_id}
Session (6)GET, POST /v1/sessions; GET, POST, DELETE /v1/sessions/{session_id}; POST /v1/sessions/{session_id}/archive
Session Event (2)GET, POST /v1/sessions/{session_id}/events
Session Resource (5)GET, POST /v1/sessions/{session_id}/resources; GET, POST, DELETE /v1/sessions/{session_id}/resources/{resource_id}
Skill (4)GET, POST /v1/skills; GET, DELETE /v1/skills/{skill_id}
Skill Version (4)GET, POST /v1/skills/{skill_id}/versions; GET, DELETE /v1/skills/{skill_id}/versions/{version}
Vault (6)GET, POST /v1/vaults; GET, POST, DELETE /v1/vaults/{vault_id}; POST /v1/vaults/{vault_id}/archive
Vault Credential (6)GET, POST /v1/vaults/{vault_id}/credentials; GET, POST, DELETE /v1/vaults/{vault_id}/credentials/{credential_id}; POST /v1/vaults/{vault_id}/credentials/{credential_id}/archive

Measured against the live Claude API: the newest full-suite round, on 2026-10-31 (stamp 20261031T052431Z), matched 458 of the suite’s 476 tests (96.2%), none inconclusive, against a target of 93%.

Control plane

Every route under /_rystic/ answers on both plans (Plans); routes by plan lists them.

Scenarios

Every scenario pins the clock at 2026-02-10T00:00:00Z and lists the API’s 14 models and claude-opus-4-1-20250805, so a Messages call works in any of them.

  • anthropic_skill_library: three skills with six versions between them — Invoice Parser and Brand Style Guide (custom) and PDF Report Builder (from Anthropic) — so skill and version reads return real records from the first call.
  • anthropic_batch_queue: three message batches, one in progress, one canceling and one ended, with eleven per-request rows; the ended batch’s results read back as JSONL.
  • anthropic_batch_archive: three ended batches over five weeks, one archived, with succeeded, errored and expired results and nothing still running.
  • anthropic_file_vault: four files — a PDF, a CSV and a PNG (each downloadable) and a text upload that expired on 2026-02-01 — so listing, downloading and the expired file’s 404 all have targets.
  • anthropic_fresh_workspace: the models and nothing else, so every collection starts empty.

Seeding state

POST /_rystic/seed takes a register map — state_skill, state_skill_version, state_file, state_message_batch, state_batch_request, state_model_info, state_agent, state_environment, state_session, state_vault, state_memory_store and the rest GET /_rystic/registers lists, each keyed by the record’s id — and merges it into the running account; Scenarios covers seed files, GET /_rystic/registers and the seed gate.

curl -X POST localhost:8080/_rystic/seed -H 'Content-Type: application/json' -d '{
  "state_skill": {"skill_01SeededSkill000001": {"id": "skill_01SeededSkill000001", "type": "skill",
    "display_name": "Seeded", "source": {"type": "custom"}, "latest_version_id": "skillver_01Seeded",
    "created_at": "2026-02-01T00:00:00Z", "updated_at": "2026-02-01T00:00:00Z"}}
}'
# -> 204; GET /v1/skills/skill_01SeededSkill000001 then reads it back

Determinism & reset

Ids are sequences the reset rewinds, so the first skill created after a reset has the same id every time, and the clock starts at a pinned instant — 2026-01-01T00:00:00Z empty, 2026-02-10T00:00:00Z in every scenario — and moves one millisecond for each record written, never on a read and never by wall time. Two identical runs from the same reset answer the same ids and timestamps. Reset and determinism covers reset, the clock and the RNG.

Egress

None. The twin calls no model and makes no outbound call to Anthropic; GET /_rystic/egress is always empty. The metered edition, which an API-key start runs, also reaches Rystic’s lease server to start and to renew its lease while it runs.

Limitations

  • The model is not called: a reply’s text does not follow the prompt, and its usage counts are the twin’s own, so count_tokens need not match a reply’s input_tokens.
  • No streaming: stream: true is refused with 501 and a body of the twin’s own, not answered with server-sent events, so the SDKs’ messages.stream() raises an error against the twin.
  • Nothing runs on its own: a batch you create stays in_progress until you cancel it, and a session records the events you send and answers none.
  • No rate limits and no request-id or anthropic-ratelimit-* response headers. Every id is the twin’s own; treat ids and page cursors as opaque.

Left out on purpose: the Admin API (organizations, users, invites, workspaces, API keys, usage and cost reports), Text Completions, sessions’ event stream and the other routes the list below names.

Known issues.

  • Most error answers carry "request_id": null, where the API always sends a req_… string; read an error’s type and message, not its request_id.
  • A session never reads running: after a user.message event the twin lets you delete it at once, where the API refuses DELETE with 400 until the session is interrupted or finishes; send a user.interrupt event before deleting, as you would against the API.
  • A message batch never ends at its expires_at when you advance the clock past it, and an archived batch still serves its results; don’t rely on batch expiry in a test that moves the clock a day or more.
  • A request carrying two different API keys, one in x-api-key and another as Authorization: Bearer, is answered, where the API refuses it with 401 authentication_error; send one key, or the same key in both headers.
  • A request without an API key is refused 401 authentication_error before its path is checked, where on some routes the API checks the path first: a message batch id not in the API’s format answers 400 invalid_request_error, and a memory path with such ids 404 not_found_error. One that also lacks an anthropic-version value answers 400 here, where the API answers 401.

Not served. 171 of the 244 operations in the published spec are not served.

  • environments, 6 of 14 served: GET /v1/environments/{environment_id}/work/poll?beta=true, GET /v1/environments/{environment_id}/work/stats?beta=true, POST /v1/environments/{environment_id}/work/{work_id}/ack?beta=true, POST /v1/environments/{environment_id}/work/{work_id}/heartbeat?beta=true, POST /v1/environments/{environment_id}/work/{work_id}/stop?beta=true, GET /v1/environments/{environment_id}/work/{work_id}?beta=true, POST /v1/environments/{environment_id}/work/{work_id}?beta=true, GET /v1/environments/{environment_id}/work?beta=true
  • files, 5 of 10 served: GET /v1/files/{file_id}/content?beta=true, GET /v1/files/{file_id}?beta=true, DELETE /v1/files/{file_id}?beta=true, GET /v1/files?beta=true, POST /v1/files?beta=true
  • memory_stores, 13 of 14 served: POST /v1/memory_stores/{memory_store_id}/memory_versions/{memory_version_id}/redact?beta=true
  • messages, 8 of 16 served: POST /v1/messages/batches/{message_batch_id}/cancel?beta=true, GET /v1/messages/batches/{message_batch_id}/results?beta=true, GET /v1/messages/batches/{message_batch_id}?beta=true, DELETE /v1/messages/batches/{message_batch_id}?beta=true, GET /v1/messages/batches?beta=true, POST /v1/messages/batches?beta=true, POST /v1/messages/count_tokens?beta=true, POST /v1/messages?beta=true
  • models, 2 of 4 served: GET /v1/models/{model_id}?beta=true, GET /v1/models?beta=true
  • sessions, 13 of 19 served: GET /v1/sessions/{session_id}/events/stream?beta=true, POST /v1/sessions/{session_id}/threads/{thread_id}/archive?beta=true, GET /v1/sessions/{session_id}/threads/{thread_id}/events?beta=true, GET /v1/sessions/{session_id}/threads/{thread_id}/stream?beta=true, GET /v1/sessions/{session_id}/threads/{thread_id}?beta=true, GET /v1/sessions/{session_id}/threads?beta=true
  • skills, 8 of 17 served: GET /v1/skills/{skill_id}/versions/{version}/content?beta=true, GET /v1/skills/{skill_id}/versions/{version}?beta=true, DELETE /v1/skills/{skill_id}/versions/{version}?beta=true, GET /v1/skills/{skill_id}/versions?beta=true, POST /v1/skills/{skill_id}/versions?beta=true, GET /v1/skills/{skill_id}?beta=true, DELETE /v1/skills/{skill_id}?beta=true, GET /v1/skills?beta=true, … and 1 more
  • vaults, 12 of 13 served: POST /v1/vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate?beta=true
  • Not served at all: complete (1), deployment_runs (2), deployments (8), dreams (5), organizations (100), tunnels (10), user_profiles (5)

Required fields the twin may leave out. The spec requires these in a success response, and the twin’s response may not carry them:

  • GET /v1/vaults/{vault_id}/credentials (200): data/*/auth/mcp_server_url
  • POST /v1/vaults/{vault_id}/credentials/{credential_id}/archive (200): auth/mcp_server_url

Not measured. The fidelity suite runs 476 tests, and 176 documented branches have none. 76 need a credential the measuring account does not hold (401 ×3, 403 ×73). The other 100:

OperationStatusWhy
DELETE /v1/environments/{environment_id}429no request a client sends provokes it
DELETE /v1/files/{file_id}429no request a client sends provokes it
DELETE /v1/memory_stores/{memory_store_id}429no request a client sends provokes it
DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}412no known request reaches it
DELETE /v1/memory_stores/{memory_store_id}/memories/{memory_id}429no request a client sends provokes it
DELETE /v1/messages/batches/{message_batch_id}200no known request reaches it
DELETE /v1/messages/batches/{message_batch_id}429no request a client sends provokes it
DELETE /v1/sessions/{session_id}429no request a client sends provokes it
DELETE /v1/sessions/{session_id}/resources/{resource_id}429no request a client sends provokes it
DELETE /v1/skills/{skill_id}429no request a client sends provokes it
DELETE /v1/skills/{skill_id}/versions/{version}429no request a client sends provokes it
DELETE /v1/vaults/{vault_id}429no request a client sends provokes it
DELETE /v1/vaults/{vault_id}/credentials/{credential_id}429no request a client sends provokes it
GET /v1/agents429no request a client sends provokes it
GET /v1/agents/{agent_id}429no request a client sends provokes it
GET /v1/agents/{agent_id}/versions429no request a client sends provokes it
GET /v1/agents/{agent_id}/versions500no request a client sends provokes it
GET /v1/agents/{agent_id}/versions503no request a client sends provokes it
GET /v1/environments429no request a client sends provokes it
GET /v1/environments/{environment_id}429no request a client sends provokes it
… and 80 more

Not modeled. What the twin’s operations leave out:

  • Documented statuses not reproduced: 404 (3 operations), 408 (9 operations), 409 (9 operations), 412 (9 operations), 413 (9 operations), 431 (9 operations), 499 (9 operations), 500 (7 operations), 501 (9 operations), 503 (9 operations), 504 (9 operations), 529 (2 operations).
  • Generic transport/infrastructure statuses documented with the same ErrorResponse envelope and no caller-provokable trigger: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504, 529. (DELETE /v1/environments/{environment_id})
  • The spec marks x-api-key as required:false while documenting a 401 for missing credentials; whether an anonymous request reaches routing or is refused before the handler is unmeasured. (DELETE /v1/sessions/{session_id})
  • Documented transport failures with no stated trigger condition: 408 deadline exceeded, 409 aborted, 412 failed precondition, 413 out of range, 431 header fields too large, 499 cancelled, 500 internal, 501 unimplemented, 503 unavailable, 504 upstream deadline exceeded. (DELETE /v1/sessions/{session_id}/resources/{resource_id})
  • Whether the backing file, github checkout or memory store referenced by the resource survives the detach. (DELETE /v1/sessions/{session_id}/resources/{resource_id})
  • Documented transport/status contracts with no stated trigger on this operation: 408 deadline exceeded, 409 aborted, 412 failed precondition, 413 out of range, 431 request header fields too large, 499 cancelled, 500 internal, 501 unimplemented, 503 unavailable, 504 upstream deadline exceeded, 529 overloaded. (DELETE /v1/skills/{skill_id})
  • Whether the anthropic-workspace-id header mismatching the credential’s Workspace answers 403 or 404; the parameter description says it must match, without naming the status. (DELETE /v1/skills/{skill_id})
  • Documented transport-level outcomes with no stated trigger: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504, 529 — declared in the spec’s generic response set, with no condition tying them to this operation. (DELETE /v1/skills/{skill_id}/versions/{version})
  • The skills-2025-10-02 beta header itself is not declared as a parameter on this operation, so the epoch addressing form has no modelled request shape. (DELETE /v1/skills/{skill_id}/versions/{version})
  • Whether the literal ‘latest’ is accepted in the version path on DELETE (documented for GET /v1/skills/{skill_id}/versions/{version}); the delete operation’s parameter description names only the version ID and the beta epoch form. (DELETE /v1/skills/{skill_id}/versions/{version})
  • Whether anthropic-workspace-id mismatching the credential’s Workspace answers 403 or 400. (DELETE /v1/vaults/{vault_id})
  • Documented transport/server statuses 408, 409, 412, 413, 431, 499, 500, 501, 503 and 504 share the generic BetaErrorResponse envelope with no stated trigger for this operation and are not modelled as branches. (DELETE /v1/vaults/{vault_id}/credentials/{credential_id})
  • Whether an unrecognised page cursor draws a 400 invalid_request_error or an empty page is not documented. (GET /v1/agents)
  • Generic transport/infrastructure statuses documented for this operation but not modeled as branches: 408, 409, 412, 413, 431, 499, 500, 501, 503, 504. (GET /v1/agents/{agent_id})
  • The documented 408, 409, 412, 413, 431, 499, 501 and 504 outcomes carry no stated trigger for this operation and are left unmodeled. (GET /v1/agents/{agent_id}/versions)
  • Ordering of the returned data array (creation order, name order, or unspecified) is not documented. (GET /v1/environments)
  • Whether the anthropic-beta header is required for this route (and which beta label) is not stated on the operation; the example sends managed-agents-2026-04-01 because the resource’s scope/config variants declare x-anthropic-beta-required for it. (GET /v1/environments)
  • Whether supplying ids[] with entries exceeding 100 after de-duplication yields the same 400 invalid_request_error message as the mutual-exclusivity violation is undocumented; both are folded into one 400 branch. (GET /v1/files)
  • Documented transport/infrastructure statuses 408, 409, 412, 413, 431, 499, 500, 501, 503, 504 and 529 all carry the same ErrorResponse envelope but have no stated caller-reachable trigger on this operation; they are left unmodeled rather than given invented conditions. (GET /v1/files/{file_id}/content)
  • A File with downloadable=false (any file uploaded via POST /v1/files) answers 400 invalid_request_error with error.details.error_code ‘file_not_downloadable’, not 403. (GET /v1/files/{file_id}/content)
  • The ordering/stability contract of the page_… cursor across concurrent creates and archives is not documented. (GET /v1/memory_stores)
  • … and 72 more

Release notes: Changelog

Last updated on