> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twinbay.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Concepts

> Twins, sandboxes, scenarios, credentials, records, logs and time

## 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](#not-modelled).

[Browse the twins](/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.

```bash theme={null}
POST /sandboxes
{
  "name": "checkout-tests",
  "twins": [{ "twin": "resend" }, { "twin": "linear" }],
  "prompt": "a hard-bounce recipient, and an issue already in progress"
}
```

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:

| State                 | Meaning                                    |
| --------------------- | ------------------------------------------ |
| `pending`             | Queued, nothing started yet                |
| `provisioning`        | The container is being created             |
| `starting`            | Booting and seeding                        |
| `ready`               | Serving requests at its `url`              |
| `stopping`, `stopped` | The container was destroyed on request     |
| `failed`              | The run is not serving; `failure` says why |

<Warning>
  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.
</Warning>

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.

```bash theme={null}
{ "twin": "resend", "scenario_template": "hard-bounce" }
```

`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.

```bash theme={null}
GET /sandboxes/{id}/twins/{twin_id}/records/{resource}
PUT /sandboxes/{id}/twins/{twin_id}/records/{resource}/{external_id}
```

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:

```bash theme={null}
POST /sandboxes/{id}/twins/{twin_id}/advance
{ "at": "2026-09-14T12:00:00Z" }   # defaults to now
```

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.

```bash theme={null}
GET  /sandboxes/{id}/logs?sandbox_twin_id=...&page=1&size=50
GET  /sandboxes/{id}/logs/{request_id}
```

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](#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.

```bash theme={null}
GET    /sandbox-templates
DELETE /sandbox-templates/{template_id}
```

## 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](mailto: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:

| Credential           | Header                  | Use                            |
| -------------------- | ----------------------- | ------------------------------ |
| Organization API key | `X-API-Key`             | CI jobs, scripts, the CLI      |
| AuthKit access token | `Authorization: Bearer` | The console and the MCP server |

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}`.
