# Quick start

> From a key to an answer drawn in your page, in about ten minutes. One route on your server, a few lines in your page.

You need a MorphScreen key, a server that can make an HTTP request (Node.js and Python are shown), and a page that can import an npm package.

## Get a key

Sign up at [app.morphscreen.com](https://app.morphscreen.com/signin) with your email or GitHub. Your first key is shown once on the welcome page. Copy it into your server's environment:

```bash
MORPHSCREEN_KEY=ms_live_...
```

The free plan has 100 calls on our model, so you don't need a model key to try it, then up to 3,000 calls a month on [your own OpenRouter key](/docs/agents/openai-compatible).

## Try a call

The API speaks OpenAI's chat completions. This streams an answer from the model included in your plan:

```bash
curl https://api.morphscreen.com/v1/chat/completions \
  -H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek/deepseek-v4.1-flash", "stream": true,
       "messages": [{"role": "user", "content": "Plan a weekend in Lisbon"}]}'
```

Each event's `delta.content` is the next piece of the answer, in [our format](/docs/format): Markdown with a few tags.

## Add a route to your server

Your page talks to your server, and your server talks to MorphScreen with the key. One route carries the page's runs on to `/v1/ui` and streams the events back:

```js
// Your server's one route for the page: each AG-UI run goes on to MorphScreen
// with your key, and the events stream straight back. The key never reaches
// the browser, and the server, not the page, picks the model.
import { createServer } from "node:http";

const API = process.env.MORPHSCREEN_API ?? "https://api.morphscreen.com/v1";
const MODEL = process.env.MODEL ?? "deepseek/deepseek-v4.1-flash";

createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/api/agent") return res.writeHead(404).end();
  let body = "";
  for await (const chunk of req) body += chunk;
  const run = JSON.parse(body);
  run.forwardedProps = { ...run.forwardedProps, model: MODEL };
  const upstream = await fetch(`${API}/ui`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY },
    body: JSON.stringify(run),
  });
  res.writeHead(upstream.status, { "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream" });
  for await (const chunk of upstream.body) res.write(chunk);
  res.end();
}).listen(process.env.PORT ?? 3000);
```

The same route in Python, with only the standard library:

```python
# Your server's one route for the page, with nothing but the standard
# library: each AG-UI run goes on to MorphScreen with your key, and the
# events stream straight back. The key never reaches the browser, and the
# server, not the page, picks the model.
import json
import os
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

API = os.environ.get("MORPHSCREEN_API", "https://api.morphscreen.com/v1")
MODEL = os.environ.get("MODEL", "deepseek/deepseek-v4.1-flash")


class Agent(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/api/agent":
            self.send_error(404)
            return
        run = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        run["forwardedProps"] = {**run.get("forwardedProps", {}), "model": MODEL}
        req = urllib.request.Request(
            f"{API}/ui",
            data=json.dumps(run).encode(),
            headers={"Content-Type": "application/json", "X-MorphScreen-Key": os.environ.get("MORPHSCREEN_KEY", "")},
            method="POST",
        )
        try:
            upstream = urllib.request.urlopen(req)
        except urllib.error.HTTPError as e:
            upstream = e
        with upstream:
            self.send_response(upstream.status)
            self.send_header("Content-Type", upstream.headers.get("Content-Type", "text/event-stream"))
            self.end_headers()
            while chunk := upstream.read1(4096):
                self.wfile.write(chunk)
                self.wfile.flush()


ThreadingHTTPServer(("127.0.0.1", int(os.environ.get("PORT", "3000"))), Agent).serve_forever()
```

## Install the web SDK

```sh
npm install @morphscreen/web
```

> **Before the launch:** The package goes to npm with the public launch. Until then, `/v1/render` draws answers as HTML on your server ([HTML without JavaScript](/docs/integrations/html)), and everything on this page works the same once it's published.
### Draw the conversation

`connectAgent` holds the conversation: it sends what the reader types, draws each answer as it streams, and sends buttons, forms and quiz answers back as the next turn.

```html
<div id="chat"></div>
<form>
  <input name="message" placeholder="Ask anything" aria-label="Your message" required>
  <button>Send</button>
</form>
```

```ts
import { connectAgent, loadCore } from "@morphscreen/web";
import "@morphscreen/web/morphscreen.css";

// morphscreen-agent.wasm from the package, served with your page
const core = await loadCore("/morphscreen-agent.wasm");
const chat = connectAgent(document.querySelector<HTMLElement>("#chat")!, { core, url: "/api/agent" });

const form = document.querySelector("form")!;
form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const input = form.querySelector("input")!;
  const text = input.value;
  input.value = "";
  await chat.send(text);
});
```

Serve `morphscreen-agent.wasm` from the package with your page. Each [integration](/docs/integrations) shows how for its build tool.

That's a working chat. Ask it for a trip, a dashboard or a quiz, and the answer arrives as cards, charts and questions, checked line by line.

## Where next

- [The format](/docs/format): what the model writes, and why half an answer is always valid.
- [Components](/docs/components): all 75, drawn live, with their attributes.
- [Your own components](/docs/format/custom-components): teach the model your product's own cards.
- [Actions and state](/docs/format/actions): what comes back when the reader taps, types and answers.
