# Import an API

Turn an OpenAPI 3, Swagger 2 or Postman specification into typed API code, and keep it up to date as the specification changes.

## Importing

**Import API** on the APIs page turns an OpenAPI 3, Swagger 2 or Postman specification — from a URL, a file, or pasted text — into typed API code: pick the endpoints, choose the options, preview the files, then write them. Studio generates the files itself and writes them to the output directory, in a folder named after the source in kebab case (`src/api/pets/` for `pets`, `src/api/pet-store/` for `petStore`), where `source.ts` exports the source, and records what it wrote in `.courier/imports-state.json` — see [Which files Studio changes](/docs/studio/import-api#which-files-studio-changes).

Studio lists only the sources `courier.config.ts` declares — it does not look for API files in the project. So after the write the wizard shows the change that declares the import there — the import of the generated source, its `sources` entry and its `imports` entry — and **Add to courier.config.ts** makes it: Studio adds those lines and nothing else, written like the rest of the file, loads the config, and the API appears on the APIs page, in the Explorer and in the Playground. A config saved after the change was shown is never overwritten — the wizard shows the change against it as it is now — and one that does not load once changed is put back as it was. A config that only passes another module's default export on — `export { default } from "./src/courier/config"`, as a server whose TypeScript compiles `src/` alone has it — is changed where its object is written, in that module, so the server registers the import too. When the config is written in a way Studio cannot add to safely, such as `sources` built by a function call, or the browser is not on the machine Studio runs on, the wizard shows the lines to add by hand and says why. Either way the config ends up like this:

```ts
// courier.config.ts
import { defineConfig } from "@noverax/courier/config"
import { api } from "./src/api"
import { pets } from "./src/api/pets/source"

export default defineConfig({
  sources: { api, pets },
  imports: {
    pets: {
      importType: "url",
      type: "openapi",
      from: "https://specs.example.com/pets.json",
      outputDir: "src/api",
      sourceName: "pets",
    },
  },
})
```

A config that keeps its list of APIs in a module of the project — `import { sources } from "./lib/sources"`, by a relative path or a tsconfig path alias, the module exporting the object as `export const sources = { … }`, as a default export, or under another name it imports by — is changed in both files as one change. The generated source is imported and listed in that module, the `imports` entry goes in the config, the wizard shows the lines each file gains, and the button reads **Add to sources.ts and courier.config.ts**. Studio writes both files or neither: a save to either after the change was shown changes neither, and when the config does not load once changed, both are put back as they were. A module that builds the list by a function call, or that is a package's rather than the project's, is left to you with the lines to add, each under the file it goes in:

```ts
// lib/sources.ts
import { api } from "../src/api"
import { pets } from "../src/api/pets/source"

export const sources = { api, pets }
```

```ts
// courier.config.ts
import { defineConfig } from "@noverax/courier/config"
import { sources } from "./lib/sources"

export default defineConfig({
  sources,
  imports: {
    pets: {
      importType: "url",
      type: "openapi",
      from: "https://specs.example.com/pets.json",
      outputDir: "src/api",
      sourceName: "pets",
    },
  },
})
```

- **`sources`** is what Studio lists APIs from, and what your app calls. An API written by hand is declared there the same way. Studio matches an import to the source of its name, so an import needs a name no other source takes: while `sources` declares another API under it, Studio writes nothing and says so. The APIs page lists a source as imported only when the config loads the `source.ts` Studio generated for it.
- **`imports`** is what Studio checks and manages an import from: the update check fetches `from` again, and when the specification changed, **Review update** — **Manage** given a newer specification — updates the import to it: every endpoint you keep is generated from it afresh, so its types and schemas follow the specification, while an endpoint you edited keeps your code; you tick the new endpoints to add and untick the ones to drop; the endpoints the specification no longer has go, and Studio keeps a copy of every file it changes. The endpoints you add are generated with the options written there, and the ones you left out when you imported stay out. With the same specification and nothing picked, **Manage** offers **Regenerate files**: every file written again as the running version of Studio writes it, your edits to endpoints kept — the way to bring an import written by an older Studio up to date. The wizard's entry carries the source name and every option you changed from its default, but not the endpoints you left out in the Select step — the entry has no way to keep that selection — so Manage lists those as endpoints you can add, and the wizard says how many there are. A file or pasted import has no `from`: Manage asks for its specification each time, and the update check marks it not checked, rather than up to date. Studio keeps each import's last check while it runs, and every page shows it, one opened or reloaded later too; a check that fails says why on the import's card. The update check sends `auth` with the request to `from`'s own origin, never to a host it redirects to, and refuses a specification whose hash is not the one `pinHash` names — the hash Studio records for the import as `specHash` in `.courier/imports-state.json`. An update, Manage and a new write of a declared import generate from what its `transform` returns for the parsed specification, keeping only the endpoints `include` names, by tag, by path as the specification writes it, or by operationId, and then leaving out those `exclude` names.
- Importing the same name again keeps the endpoints you edited by hand, while the specification still describes them, and the ones you added, in the files that declare them. Every other generated file — the source of an API grouped by tag or path, its `index.ts`, the schemas — is written as generated; when it holds edits of yours, Studio asks before replacing it. A group left out this time has its files removed, as long as Studio wrote them and nobody changed them; the message after the write says how many files it removed, and their copies are kept.
- A part the config declares already is left out, and once it declares both, the wizard closes after the write and says only what it wrote.
- A tag that names a `parent`, as OpenAPI 3.2 allows, is imported as a group inside its parent tag's group: its own folder, listed under `groups` in the parent's file, its prefix after the parent's, so `client.billing.invoices.list()` reaches the path the specification gives. A tag only other tags sit under becomes a group of its own, and a parent the specification does not declare, or one that would put a tag under itself, is left out. **Manage** adds a new inner group to its parent's file and takes a removed one out of it; a group whose endpoints all go stays while groups sit inside it, and goes once they do.
- Every operation whose verb the builder has is imported: GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE, and QUERY — OpenAPI 3.2's `query` operation, or a QUERY a specification lists under `additionalOperations`. An operation that reads is generated as a read, one addressed by a closure included. One the builder has no verb for — TRACE, or another verb under `additionalOperations` — is listed among the operations left out, with the reason, rather than dropped.
- Each endpoint keeps what its operation says it does — the summary, or the first paragraph of the description — as `.meta({ description: "…" })`, whether or not it becomes a doc comment too: a tool the project's [`agents` policy](/docs/agents/overview#what-agents-may-call) offers without `.agent()` describes itself to the model by it, and the OpenAPI written from the definition keeps it.
- An operation is called with its path and query parameters and its header parameters, beside its body: a header parameter is a variable under its own name, validated with the rest and sent as a request header by `headerVariablesMiddleware`. `Accept`, `Content-Type` and `Authorization` are not taken as parameters, as OpenAPI says — the client sets them — and neither is a header named like a path or query parameter of the same operation, nor one beside a multipart form, which is the whole of what such a call is made with.
- A schema with `readOnly` or `writeOnly` fields, itself or through a schema it refers to, is two types: the answer, `User`, without the fields only the client sends and with the ones the server sends as required as declared, and the input a request sends, `UserInput`, the other way round — named `UserInput2` when the specification has a schema called `UserInput`. A schema that is only ever answered has no input type.
- What a schema allows beyond its type — a string's `minLength`, `maxLength` and `pattern`; a number's `minimum`, `maximum`, exclusive bounds and whole `multipleOf`, and that an `integer` is whole; a list's `minItems` and `maxItems` — is checked on what a request sends, and never on what an answer carries: a validator that refuses what the API really sends is worse than none. A schema both sent and answered with such a constraint, itself or through a schema it refers to, is then two schemas in the same way: `Product` reads the answer and `ProductInput` checks the request. An operation without a body has its parameters validated when one of them carries such a constraint, and only typed otherwise. Left unchecked: `uniqueItems`, a `multipleOf` that is not a whole number, which floating point cannot check exactly, and a `pattern` JavaScript cannot compile.
- A field named like a member every object has — `constructor`, `toString`, `valueOf`, `__proto__` — is a field like any other. Every validation library read a `constructor`, `toString` or `valueOf` a value left out as `Object`'s own member and refused the value, and none checked a field `__proto__` — zod left it unchecked, and Valibot threw — so an object with such a field is checked through `_ownFields`, a function the module declares after its declaration: on a copy of the value's own fields, with no prototype, a field `__proto__` checked and kept as a field of its own. It is described as its fields are, a field `__proto__` among them, inside another schema too, where zod leaves one with a field `__proto__` unspecified; a Valibot one is described as far as the converter you pass `toOpenAPI` for Valibot can. An update and **Manage** carry the function with the code that calls it and take it out with the last call, and Contract Drift's fix and the Playground read an object through it, wherever it sits — the Playground starts a call with a field `__proto__` as a field of its own. An optional such field is typed with the member beside its own type, `string | Object["constructor"]`, as TypeScript reads `Object`'s members on every object.
- Endpoints are grouped by the specification's tags, by path, or not at all, with **Single file**. By path, an endpoint goes in the group its resource names: the first segment of its path after the ones every path shares above every operation — Stripe's `/v1`, an API served under `/api` — that names neither a version, such as `v2`, `v1beta1` or `2010-04-01`, nor a parameter. So Stripe's endpoints go in `customers`, `charges` and the rest of its 78 resources, while an API whose every path starts with `/settings`, one of them `/settings` itself, keeps them all in `settings`.
- A group takes its name from its tag, its Postman folder or, grouped by path, its segment, made an identifier: `pet store` is `petStore`, and `raw`, a name the client keeps for itself, is `rawGroup`. A tag or folder with no letter or digit Studio spells in Latin letters names its group after the last segment every path of the group shares, `users` for `/api/users` and `/api/users/{id}`, and a group named by neither is `group`. A name another group took gets a number, `pets2`, and a name is cut to 128 characters, its number included, the longest a folder's name is made from.
- An endpoint in no group sits in the API itself: an operation with no tag, a path no segment groups — the API's root `/`, or `/v1` alone — a request at a Postman collection's root, and with **Single file** every endpoint. `source.ts` holds it beside the groups, and the client calls it on itself: `client.health()`, beside `client.pets.list()`. The wizard and **Manage** list these endpoints at the top level; adding the first of them to an import gives its `source.ts` an `endpoints` object beside `groups`, and removing the last takes the object away again with what only it needed. A specification that tags none of its operations — Stripe's, say — is grouped by path unless your settings choose a single file: by tag, none of its endpoints would be in a group, which the wizard says when tag is chosen anyway.
- A schema composed with `allOf` is merged into one, the properties of every schema it names copied in. A composition that takes in the schema it sits in — a category's `parent` composed of the category and a `depth` — is declared as a schema of its own, named after where the specification writes it: the component and the path inside it, `CategoryParent`, `NodeChildrenItem` for the items of a node's `children`. It refers to itself, so each level of it is typed and checked alike.
- A schema only one group's endpoints use is declared in that group's folder. One that several groups use, or the API's own endpoints, is declared once, in the import's own `schemas.ts`, and each group imports it from there: declared in every group that used it, Stripe's schemas — nearly all of which refer to each other — came to 59 MB over its 78 resources; declared once, they are 4.7 MB, 1.5 MB of it the import's own `schemas.ts`, which Biome checks only with `files.maxSize` raised past its default of 1 MiB. Where each schema goes is decided over the whole specification, whatever you select, so endpoints added later find their schemas where the first write put them; a schema leaves the import's `schemas.ts` only once nothing left in the import uses it.
- An import writes its folder under `outputDir`: `source.ts`, the API as the specification describes it — its own endpoints and its groups; `schemas.ts`, the schemas its own endpoints and several groups use; `index.ts`, the direct caller with the schemas and types, re-exported by name; `proxy.ts`, the proxy caller alone; and a folder for each group, with its endpoints and the schemas only it uses. Import the proxy caller in browser code from `proxy.ts`: it takes the source's routes from the project's [routes module](/docs/proxy-mode/expose), which Courier writes from the endpoints marked `.expose()`, so the browser bundle holds no part of the source — not its upstream address, its middleware nor a schema — which importing it from `index.ts` would bring along with the direct caller and the schemas. An import with a proxy caller marks its API `expose: true` in `source.ts`, so a page may call each endpoint it generates; mark one `.expose(false)` to keep it to the server, and the routes module follows as the config loads, as it does when **Manage** adds or removes endpoints. Types are imported from `index.ts` with `import type`, which a bundle drops.
- The generated code passes Biome's recommended rules, stricter ones and its import organizer as it is written, before any formatter runs: an import's `index.ts` re-exports each schema and type by name rather than with `export *`, a pattern is a regular expression literal, a file that imports only types imports them with `import type`, and imports, re-exports and the names in each come in the order the organizer keeps them. It compiles with `noUnusedLocals`, `verbatimModuleSyntax` and `exactOptionalPropertyTypes` on.
- The generated files are application code. A project that emits declarations compiles them, since every type their exports carry is one the packages export, but not one under `isolatedDeclarations`, which asks for a written type on every export: the type of a group or a source is the whole chain of its endpoints.

### Which files Studio changes

Studio changes an import's files — when it writes the import, updates it, adds or removes endpoints, or removes the import — only inside the import's folder, or, removing an import Studio 0.3.0 bound as `eval`, `arguments`, `undefined` or `globalThis`, inside the folder 0.3.0 wrote it into, and decides for each file by who it belongs to. It records every file it writes, with a hash of its content, in `.courier/imports-state.json`, and starts each with two lines that say so:

```ts
// @courier-generated {"import":"pets","sha256":"3f2a…"}
// Courier Studio wrote this file. It keeps your edits to endpoints when it updates the file.
```

- **A file Studio wrote and nobody changed** is replaced or removed as the change needs. A file whose first line names the import, but which Studio has no record of — in a fresh clone, say, where `.courier/` is not checked in — is treated the same way when it is replaced; removing one waits for you, offered ticked.
- **A file you changed** keeps your edits to endpoints when Studio updates it. A file Studio would replace whole, or remove, waits for you instead — and so does a file whose endpoints Studio cannot read as code, one saved mid-edit say, since it cannot keep your edits in it.
- **A file Studio did not write** — one you added to the folder, or one its first line says Studio wrote for another import — is never removed, and replacing it waits for you.
- **A symbolic link or a folder where a file should be** is never written through, replaced or removed, and nothing is written through a folder link that leads out of the project. A change that would write one of them is refused until you move it out of the way.
- **A group Studio names otherwise than the Studio that wrote it** — `evalGroup` where 0.3.0 named a group `eval`, and so for `arguments`, `undefined` and `globalThis`, or after its path where its tag spells no name — takes its new name the next time the import is written, updated or managed. Manage keeps its endpoints, reading the group's old file as the group it is now. That file then belongs to a group the specification no longer has, and goes as such a file does: at once when Studio wrote it and nobody changed it, once you allow it when you did. What you changed in it is not carried into the group's new file; the copy in `.courier/backups/` keeps it.

When a change holds steps that wait for you, Studio shows them with the rest of the change, each with its own checkbox, and takes only the steps you tick; a file left unticked stays as it is, and so does every file it imports: Studio never removes a file that a file staying in the import's folder imports, and the message after the change counts it among the files left as they were. An update, a change to endpoints and a removal are applied only as you previewed them: if a file changed in the meantime, Studio shows the change as it now stands instead. A write needs no preview, and asks only when a step waits for you.

Every change is applied all at once. Studio keeps a copy of each file it replaces or removes in `.courier/backups/<change>/`, and keeps the copies of the last 20 changes that made any. A change that Studio is stopped in the middle of is undone the next time it starts, file by file, except for a file changed since, which is left as it is. A change another Studio on the same project is still making is left to that Studio. Studio tries such an undo once: its copy of each file it could not restore is kept, whatever the limit, and Studio names those files at every start until you delete their folder in `.courier/backups/`. Studio undoes only what its own records name: a `.courier` folder that came with the project cannot make it touch other files. Removing an import takes it out of `courier.config.ts` too. Under the box **Also remove … from courier.config.ts**, ticked, the dialog shows the lines Studio removes: the import's `sources` entry with the import only that entry used, and its `imports` entry — an `imports` left empty goes, a `sources` left empty reads `{}`. Where the config takes its `sources` from a module of the project, the box reads **Also remove … from lib/sources.ts and courier.config.ts**, and the dialog shows the lines each file loses: the `sources` entry and its import come out of that module, the `imports` entry out of the config, both files changed or neither. Studio makes that change before any file goes, while the files are still there: it loads the config without the import and checks that the config no longer declares it nor imports any of the files that go, and only then removes them. A config — or that module — saved after the dialog was shown, or a change Studio could not make — the config would still import one of the files, say — removes nothing, and the dialog shows the removal again with the reason, the box unticked when the change failed. Untick the box to leave the config as it is; then, and whenever Studio cannot change the config — it does not load, it builds `sources` in a way Studio cannot change, or the browser is not on the machine Studio runs on — the dialog warns when the config imports one of the files that go, since the config stops loading until you remove that import, and reminds you to remove the import's entry under `imports` too, since Studio goes on checking the import for updates while the config declares it.

A request Studio refuses changes nothing. It is answered with `{ error }` — `404` for an import Studio does not know, `400` for a body it cannot read — and, for the refusals below, with a `code` and, where there is one, the plan as it stands. A change that needs `courier.config` to load is refused as [How Studio loads the project](/docs/studio/project-loading) describes.

| Code | Status | Meaning |
|---|---|---|
| `NEEDS_CONFIRMATION` | 409 | Some steps wait for you; send the request again with the plan's `planDigest` and the paths you allow in `confirm`. |
| `PLAN_NOT_SEEN` | 409 | An update, a change to endpoints or a removal quoted no plan; preview it first. |
| `PLAN_STALE` | 409 | The plan is not the one you saw: a file it rests on changed, or the import's declaration, the change asked for or the project's formatter configuration did. |
| `CONFIG_STALE` | 409 | A removal quoted, as `configBaseHash`, a change to `courier.config.ts` planned against the config as it was — and the module it takes its `sources` from, when the change writes that too: one of them was saved since the preview. Nothing was removed. |
| `CONFIG_NOT_CHANGED` | 409 | Studio could not take the import out of `courier.config.ts` — the config did not load without it, still imported a file the removal takes, or could not be written — and left it as it was. Nothing was removed. |
| `TARGET_BLOCKED` | 409 | A file the change writes is a symbolic link or not a file. |
| `CONFIRMATION_NOT_IN_PLAN` | 400 | `confirm` names a path the plan does not ask about. |
| `DECLARATION_MISMATCH` | 409 | `imports` declares the import in another folder, or with another type, validation, grouping, client or other generation option, than the wizard's options; the message names each one. An update and Manage generate as declared, so an import written otherwise would change layout at the next of them. |
| `DIR_OVERLAP` | 409 | Another import owns the folder, or one inside or around it. |
| `NAME_TAKEN` | 409 | `sources` declares another API under the import's name. |
| `IMPORT_RENAMED` | 409 | The import's files are still where Studio 0.3.0 bound it as `eval`, `arguments`, `undefined` or `globalThis`, which this release binds as a reserved word's is, `evalGroup` in `eval-group/`. Studio does not move an import's files: remove the import, which takes them from that folder, then import it again; where both folders hold its files, delete the one you do not keep first. |
| `FILE_NOT_RECOGNIZED` | 409 | A file Studio would add endpoints to no longer shows where its endpoints are. |
| `ENDPOINT_OUTSIDE_PREFIX` | 409 | An endpoint you add would call a URL the prefix its group file declares cannot reach, or Studio cannot read that prefix; add it by hand, or import the API again under the same name. An endpoint the file's prefix can reach is written relative to it, whatever prefix the specification gives the group. |
| `IMPORTS_STATE_UNSAFE` | 409 | Studio cannot read `.courier/imports-state.json`, or keep a copy of it, so saving it would lose the other imports' records. |
| `COURIER_DIR_UNSAFE` | 409 | `.courier`, or a folder Studio keeps in it, is a symbolic link or not a folder; Studio keeps nothing there. |
| `INVALID_IMPORT` | 400 | The output directory, the source name or a group's name is one Studio does not write to: outside the project, in `.git`, `node_modules` or `.courier`, or a name Windows treats differently. |
| `PATH_OUTSIDE_PROJECT` | 400 | A path leads out of the project. |
| `SPEC_NOT_GENERATED` | 400 | Studio could not generate code from the specification sent. |
| `INVALID_SPEC` | 400 | The specification sent holds a value Studio cannot write into the code it generates: a name that is not an identifier or is one the generated code keeps for itself, an endpoint named so that its client could not call it, two groups of one name or written to one folder, a group whose parent names no group or whose chain of parents loops, a bound that is not a finite number, a pattern whose literal would not end where it does, or a node that is not an object or a list. The message names where it is. |
| `SPEC_TOO_LARGE` | 413 | The specification is larger than 64 MB once parsed: Studio takes the parsed specification back when it generates, writes, updates or manages an import, and a request body may be at most 64 MB. |
| `SPEC_WORK_LIMIT` | 413 | Reading the specification, generating its code or comparing it took longer, or more memory, than Studio gives such work; see [Specifications on a private network](#specifications-on-a-private-network). |
| `STUDIO_BUSY` | 503 | Studio already fetches, reads, generates or compares as much for pages without the token as it takes at once; try again in a moment. |
| `STUDIO_TOKEN_REQUIRED` | 401 | The page has no token: it may not change the project, read a file or the code of it, preview a change to an import or to `courier.config.ts`, or check imports for updates. A visitor of a public demo is told what the demo does not do. |
| `STUDIO_DEMO_REFUSED` | 403 | A visitor of a public demo asked Studio to read a specification from a file of the project. |
| `SPEC_UNREADABLE` | 502 | The import's `from` answered with something that is not a specification Studio reads, such as a page. |
| `SPEC_PIN_MISMATCH` | 409 | The specification's hash is not the one the import's `pinHash` names. |
| `IMPORT_TRANSFORM_FAILED` | 400 | The import's `transform` threw, or returned something that is not a specification or one holding a value `INVALID_SPEC` describes; the message names where it is. |

### How Studio formats the files

Studio writes the files the way your project formats its code, and shows them that way before you apply the change:

- **Biome**, when a `biome.json` or `biome.jsonc` stands in the project or a folder above it, or its `package.json` lists `@biomejs/biome`: Studio runs the project's own Biome on each file — its native program, without the Node launcher — with your configuration: the formatter and the import sorting. Lint rules do not change the code.
- **ESLint**, when an ESLint configuration stands there — an `eslint.config.*` file, or for ESLint 8 an `.eslintrc` file or an `eslintConfig` key in `package.json`: Studio applies the fixes your rules offer, as `eslint --fix` does. A file your configuration ignores stays as it is.
- **Prettier**, when a Prettier configuration stands there, or `package.json` lists `prettier` or has a `prettier` key: Studio runs it last, alone or after Biome when Biome's formatter is off, reading `.editorconfig`, `.prettierignore` and `.gitignore` as Prettier's command line does.
- **None of them**: the files are written as Studio generates them.

Studio loads ESLint and Prettier once, from your project, in processes of their own — up to three — and keeps them between changes, so a file takes milliseconds: Petstore's eight files take about 20 ms once the tools are loaded, GitHub's 89 under a second. A configuration you change is read on the next change, and the processes go after five idle minutes. A file goes through the tools again until a pass changes nothing.

A file you edited is formatted whole when Studio merges a change into it, as your editor would format it when you save it; a `courier.mocks.ts` Studio creates is formatted the same way. The change says which tools ran, with their versions. When a tool fails on a file — an ESLint configuration with no TypeScript parser cannot read TypeScript, say — the other tools still run on it, and the change says why; a tool the project configures but did not install is named the same way. A file no tool formats is still correct code.

## From the command line

`courier-studio import` imports a specification as **Import API** does — the same files, written into the same folder and recorded the same way — from a terminal, a script or a CI job, without Studio running:

```bash
npx @noverax/courier-studio import https://petstore3.swagger.io/api/v3/openapi.json
```

It takes a URL, a file, or `-` for standard input, and names the import after the specification, as the wizard does, unless `--name` names it. It shows the plan first — every file it creates, updates, replaces or removes, and the lines that declare the import in `courier.config.ts` — and asks before it writes anything. Then it declares the import as **Add to courier.config.ts** does: the lines added, written like the rest of the file — in the module the config takes its `sources` from too, when it does — the config loaded, and every file put back as it was when it no longer loads.

- `--yes` writes without asking, taking the plan as the wizard offers it: a step that waits for you — replacing a file you changed, say — is taken only when the wizard offers it ticked, or when `--confirm` names its path. Every other file stays as it is, and the command says how to allow it. Where no one can answer, as in a CI job, nothing is written without `--yes`.
- `--dry-run` shows the plan and writes nothing.
- `--include` and `--exclude` pick endpoints by tag, by path as the specification writes it, or by operationId — `--include tag:pets,op:getUser` — and the import's `imports` entry records them, so an update keeps the selection. Once the config declares the import, a selector other than the one it declares is refused: change it in the config.
- `--out`, `--validation`, `--group-by`, `--client`, `--no-types`, `--no-schemas`, `--no-comments` and `--auth-middleware` are the wizard's options, with its defaults.
- `--no-declare` writes the files only, and prints the lines to add to `courier.config.ts` — each under the file it goes in, when the config takes its `sources` from a module.
- A URL is fetched by the wizard's rules — see [Specifications on a private network](/docs/studio/import-api#specifications-on-a-private-network).
- The project needs a `courier.config.ts`: without one, the command prints the config to start from. Its variables are read first, as Studio reads them — `--dotenv` names the files.

It exits 0 once the import is written and declared, or its plan shown with `--dry-run`, and 1 when it writes nothing or something fails.

## From traffic, for an API with no specification

An API with no specification can be declared from the calls your app makes to it. A source the app defines in its own code, which `courier.config.ts` does not list, shows in the Explorer with what its traffic tells — and its endpoints' panels say they are **known only by their traffic**. **Declare `<source>` from traffic** reads an OpenAPI 3.1 document from the calls Studio recorded and opens **Import API** on it, as though it were pasted; from there it is imported as any specification is: pick the endpoints, choose the options, preview the files, write them, and the source is declared.

- **Routes.** A segment every call shares is written as it is; one that differs between calls, or that was a variable's value, is a parameter named after that variable — `/products/{id}` — or `param1` when no variable carried it. The segments every endpoint's paths start with are the API's base path.
- **Requests.** A read's query is the keys its calls sent, required when every call sent them, typed by the variables they carried; a write's body is its variables less those in its path.
- **Answers.** The answer's schema is merged from every answer that came whole with the status most calls got: a field is required when every answer held it, nullable when it came as `null`, of every type it came as; a string has a format — `date-time`, `uuid`, `email` — when every string there had it. Numbers stay numbers: a few answers cannot tell a price of `12` from an integer. No value goes into the document.
- **Names.** An endpoint keeps the name and the group the app gave it, as its operation id and tag. An answer that is a list of objects names the schema of one of them after its endpoint — `ProductsListItem` for `products.list` — so the code has a type for one item rather than an unnamed one nested in the list.
- **How sure each part is.** Every field, path parameter and query parameter read from traffic says so, and on how many calls it rests: `Inferred from traffic: in 2 of 3 answers.`, `Inferred from traffic: sent in all 4 calls.` The generated code keeps it as the field's comment, so whoever reads the code sees which parts rest on one call and which on many.
- **What is left out.** A stream or a channel, a call with no answer, and an answer kept only in part; an endpoint whose route and method another already took is named rather than written twice.

The document is only as complete as the traffic: an endpoint the app never called is not in it, and a field no answer held is not in its schema. Call what you want declared first — or declare it now and [follow its contract](/docs/studio/contract-drift) as traffic shows more.

A source declared this way stays tied to its traffic. The APIs page says it was **read from traffic**, and **Manage endpoints** reads the calls Studio recorded again rather than asking for a specification: an endpoint the app has called since is there to add, and one the traffic no longer shows stays until you remove it.

An endpoint of a declared source can show up in traffic too, when the app calls one the declaration does not have; its panel says it was **called, but not declared**. For a source declared from its traffic — while the app still calls its own definition — the panel offers **Add `<endpoint>` from traffic**, which opens **Manage endpoints** on the source with that endpoint picked, and the change is shown before it is written. For any other source it means the app runs another version of it than the one Studio read — one built before a change to its definition, or a definition the config does not import — so restart the app, or point the config at the definition the app uses.

## Specifications on a private network

**Import API** and the update checks fetch a specification by URL on your behalf, so Studio does not reach into a network you could not see from your own browser. It connects to public addresses, and to this machine when the URL you gave points there — a specification your own dev server serves — following redirects as long as every hop stays on this machine. This machine is its loopback addresses and every address of its own network interfaces, a global IPv6 address included; a public address your router sends back to this machine, by hairpin NAT, is on none of them, so Studio cannot tell it from another machine's. It reaches a host on a private network, or this machine once any hop has been elsewhere, only when `studio.allowedSpecHosts` in `courier.config.ts` names that host:

```ts
// courier.config.ts
import { defineConfig } from "@noverax/courier/config"
import { api } from "./src/api"

export default defineConfig({
  sources: { api },
  studio: { allowedSpecHosts: ["gitlab.internal", "10.0.0.7:8080"] },
})
```

- Studio reads the list only when a fetch would reach such a host, from `courier.config` as it is on disk at that moment: a host a save has just added or removed counts.
- While `courier.config` does not load, no private host is reached, not even one the last config that loaded named: the request is refused with 409 `CONFIG_NOT_LOADING`, and with 503 while Studio is still loading the config. A project without a config is told to create one and name the host there.
- List hosts literally. An entry is the host the URL names, in any case — `gitlab.internal` allows it on any port, `10.0.0.7:8080` on that port only — an IPv6 address may be written with or without brackets, and a name in other letters than ASCII as it is written. An entry written as a URL, with a path, or ending in a dot never names a host: the config does not load, and its error names the entry. A URL that names no port has its scheme's default, so `gitlab.internal:443` allows `https://gitlab.internal`. A host computed from something Studio does not watch, such as an environment variable or a file the config reads, is taken as of the last load, and one computed in a module Node runs itself as of the last time Node ran it.
- `0.0.0.0` and `::`, which mean every interface, are never reached, even when listed.
- A name answered with an address of 198.18.0.0/15, as a fake-IP proxy such as Clash, Surge or sing-box answers every name, is reached as the name it was: the proxy takes it to that host. Such an address written out in a URL, or in a redirect, names no host, and is reached only when the list names it.
- Every hop is checked as it connects: a host name is resolved once, every address it resolves to must pass, and the connection goes to one of those; a redirect is followed only once its target passes the same check, up to five redirects.
- Behind a corporate proxy, start Studio with `NODE_USE_ENV_PROXY=1` and the proxy in `HTTPS_PROXY`: it fetches through the proxy, checking each host as this machine resolves it before the proxy is asked, and reaches this machine directly. A certificate authority of your own goes in `NODE_EXTRA_CA_CERTS`. [Proxies & Certificates](/docs/advanced/proxies-and-certificates#studio) has the details.
- A URL Studio refuses, such as one that is not `http://` or `https://` or that names a private host the list does not, is answered 400. A host Studio could not read is answered 502, with a message that says which and why: it could not be reached, did not answer or finish sending within 60 s, answered with an error status, redirected more than five times or to something that is not an `http://` or `https://` URL, sent a response that is not HTTP Studio can parse, or sent a body that was cut off, does not decompress or passes 64 MB. A check of every import reports the same message as that import's result.
- All of the above is for a page opened from the link Studio printed in its terminal, which carries the project's token. For a page or process without it — a machine on the network Studio was bound to with `--host 0.0.0.0`, a visitor of a [public demo](/docs/studio/overview#host-a-public-demo) — Studio fetches from public addresses only, whatever `studio.allowedSpecHosts` names, and never through the proxy, looking a host's name up from the name servers this machine uses, asked directly, rather than from its hosts file, and refusing one answered with an address of 198.18.0.0/15, which a fake-IP proxy could take to any host it reaches — a name that does not resolve is answered as one that resolves to a private address, 400, naming the host and none of its addresses; it reads no file of the project, refusing before it looks at the path — 401 `STUDIO_TOKEN_REQUIRED`, or 403 `STUDIO_DEMO_REFUSED` for a demo's visitor — so the answer is the same whether the file is there or not, and checks no import for updates. Such a fetch may take 15 s and read 16 MB; Studio runs four of them at once and holds 32 MB of their bodies between them, and answers more with 503 `STUDIO_BUSY` until one ends.
- A fetch stops once the page that asked for it goes away.
- Studio reads every specification — pasted, fetched, from a file or by a check for updates — in a process of its own, so a large one holds up nothing else Studio does, and one stopped or failing leaves Studio running; for a page without the token it generates code and compares specifications there too. Such work may take two minutes, as many at once as this machine has cores but one, up to four, the rest waiting their turn; for a page without the token, 20 seconds and 256 MB of heap, its process 512 MB in all — JSON of some 15 MB, YAML of a few, the code of 60,000 operations — with two at once, and more answered 503 `STUDIO_BUSY`. Work past its budget is answered 413 `SPEC_WORK_LIMIT`.
