Get started

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.

View as Markdown

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 with your email or GitHub. Your first key is shown once on the welcome page. Copy it into your server's environment:

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

Try a call

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

Terminal
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: 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:

server.mjs
// 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(${</span><span class="t1">API</span><span class="t3">}</span><span class="t1">/ui</span><span class="t3">, { 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:

server.py
# 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

Terminal
npm install @morphscreen/web
Terminal
pnpm add @morphscreen/web
Terminal
yarn add @morphscreen/web
Terminal
bun add @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), 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.

index.html
<div id="chat"></div>
<form>
  <input name="message" placeholder="Ask anything" aria-label="Your message" required>
  <button>Send</button>
</form>
main.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 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