Skip to main content

Twin

A twin is a hand-written clone of one provider’s API. It reproduces the payloads and the behavior behind them: state machines, pagination, idempotency, authentication, rate limits and error envelopes. Nothing reaches the real provider. Twinbay validates a twin by running the provider’s own SDK against the built image in CI. A twin may be incomplete, and it never invents behavior: an operation outside its fidelity answers with an explicit provider-shaped refusal rather than a plausible response. See not modelled. Browse the twins for what Resend and Linear cover today.

Sandbox

A sandbox is one isolated instance of up to five twins. Each twin inside it has its own hostname, credential, records and request log, and writes made through the provider surface persist as long as that twin keeps running. Each twin runs as a container of its own, created when you ask for it and holding its state in a database inside that container. Nothing is shared: two sandboxes never see each other’s records, and neither do two organizations.
The call returns 202. Twinbay recorded the sandbox and queued provisioning, which is a container per twin plus seeding, so poll GET /sandboxes/{id} until each twin reports ready. A twin that has not reported ready within two minutes is marked failed, and failure says why.

Twin state

Each twin in a sandbox carries a desired_state you set (running or stopped) and a state it reports:
Stopping a twin destroys its container, and the records inside it go with it. Starting it again provisions a fresh container and seeds it from the same scenario, so any write your tests made is gone and the credential you collected no longer opens anything. Collect a new key after every start.
The url is the one thing that survives, because a twin’s hostname belongs to the sandbox rather than to the container behind it. Stop a twin once a suite has finished, and read a start as a new run at the same address.

Scenario

A scenario is the state a sandbox starts with. Ask for it two ways:
  • A prompt. Plain language, up to 4000 characters, seeded into every twin in the sandbox while it provisions. Add a per-twin prompt for a note only one provider needs; Twinbay appends it to the sandbox prompt instead of replacing it.
  • A curated template. Each twin publishes named scenarios, such as hard-bounce on Resend or busy-team on Linear. Pass one as scenario_template when the state you want already has a name.
GET /twins/{slug} lists the scenarios a twin publishes.

Provider-shaped credential

Each twin mints its own key in the provider’s format, such as re_... for Resend or lin_api_... for Linear. Collect it with POST /sandboxes/{id}/twins/{twin_id}/credential. Twinbay returns the key once and refuses a second call, so capture it in the run that created the sandbox. Keys are per twin: the one issued for Resend does nothing against the Linear twin beside it. A key belongs to the container that minted it, so stopping and starting a twin invalidates it and you collect a new one. The credential opens the twin. Your Twinbay API key never travels to a twin, and a twin credential never reaches the Twinbay API.

Records

Records are the twin’s state, readable and writable through the control plane rather than through the provider surface.
The resource is the twin’s own collection name, emails on Resend or issues on Linear. A PUT replaces the record body, which is how you reach state a provider call would refuse to create.

Time and advancing

Provider lifecycles that take minutes or days in production move when you advance the sandbox, so a test asserts on a settled state instead of waiting for one:
Each twin defines what a step means. Resend settles queued and scheduled sends, finishes domain verification, and materializes broadcasts. Linear moves each open issue one step along its workflow and auto-archives finished ones. Delivery outcomes stay deterministic, so a hard bounce is a hard bounce on every run.

Request log

Every call a twin serves is recorded: method, path, query string, status code, latency, content_type, and a capture of the request and response bodies.
Read a captured body as the content of a call rather than as its bytes. Three things happen to it on the way in:
  • Secrets are dropped. Any JSON key holding a token, secret, password, API key, authorization header or credential is removed from the capture, at every depth.
  • JSON is re-serialized. A JSON body arrives parsed and written out again, so key order and whitespace are Twinbay’s rather than the caller’s. Other content types are captured as sent, and text is null for a payload that does not decode.
  • Large bodies are capped at 64 KiB, with truncated marking a capture the cap cut short. size reports what the caller sent.
Headers other than content_type are not recorded. Assert on the log for what a call carried, and on records for what it changed. Neither is a byte-for-byte transcript.

Templates

Set save_as_template when creating a sandbox and Twinbay saves the definition: its twins, their scenarios and their prompts. Start later sandboxes from it with template instead of listing twins, which is how a CI matrix gives every job the same starting state.

Not modelled

A twin refuses an operation it does not model in the provider’s own error shape, carrying Twinbay’s name. Read that refusal as a fidelity boundary, not a missing route and not an empty collection. Linear’s twin goes further inside a GraphQL document: a field it has no value for is refused rather than returned as null, because null is a fact about a record and a refusal is a fact about the twin. If your test needs behavior a twin refuses, tell us at hi@twinbay.ai.

Organizations and keys

Sandboxes, templates and keys belong to an organization, and every request is scoped to the one it was issued for. Two credentials reach the API: Create a key in the console under API keys, or with POST /organizations/current/api-keys. A key acts with the role of the member who created it, so demoting or removing that member changes what the key can do. Twinbay stores only a digest, so the creation response is the only time the token is readable. Revoke with DELETE /organizations/current/api-keys/{id}.