Build an application
MarkdownAn application is code Apigami hosts for you. It answers HTTP, it can hold a database, it can run on a schedule, and it can use the session of a user who approved it — or call another application that already knows how. You write it, push it, run it and publish it without a server, a domain or a development machine of your own.
One object carries all of it: a version.
An application is an HTTP server with several routes. A version holds all of its files and all of its actions. Naming an action only ever means running that one.
There is no separate source, no build to track, no channel to point at. You write a version, you run one of its actions, you deploy it, you put it in production. Rolling back is putting an earlier version in production — the same call.
If your AI assistant is connected to Apigami over MCP, every step in these pages is one tool call
(app_settings, app_version_write, app_run, app_deploy, app_publish). Everything those tools
do, the /v1 routes in these pages do, so nothing is available only through an assistant.
The credentials in the examples
Section titled “The credentials in the examples”$TOKEN is the credential of a member of your team, sent as Authorization: Bearer $TOKEN to the
/v1/apps routes that create, write, deploy and publish: an agent token from an agent
sign-in, which is how an AI assistant in a terminal, on a server or in CI
gets one (in a hosted chat or a sandbox, the person adds the
connector instead), a personal access token, or the access token of a signed-in session. Running an action over HTTP takes
$API_KEY instead, an API key of the application, which a person creates in
the dashboard; an assistant holding an agent token runs actions with the MCP tool app_run. $APP_ID
is the id that creating the application returns.
Starter template: hello world without a browser
Section titled “Starter template: hello world without a browser”The smallest complete application: GET /healthz, one action that answers JSON, and a home page. It
reaches no site through a browser, so a person connects to it with their email alone, from any
device. Three files; replace the slug with a name of your own first, since a slug is unique on all
of Apigami.
{ "openapi": "3.1.0", "info": { "title": "Hello", "description": "Greets the person using it, by name.", "version": "1.0.0" }, "x-apigami": { "manifest_version": 2, "slug": "acme-hello", "entrypoint": "main.ts", "pages": [{ "kind": "home", "id": "home", "title": "Hello", "page_url": "/" }] }, "paths": { "/hello": { "post": { "operationId": "sayHello", "summary": "Say hello", "description": "Answers a greeting for the name given, and the user_ref of the person it serves.", "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 80, "description": "Who to greet, world when left out" } } } } } }, "responses": { "200": { "description": "The greeting", "content": { "application/json": { "schema": { "type": "object", "required": ["message", "user_ref"], "properties": { "message": { "type": "string" }, "user_ref": { "type": ["string", "null"], "description": "The person served, null when nobody is connected" } } }, "example": { "message": "Hello, Ada!", "user_ref": "usr_3f9c1a0e7b2d4c58a61e9f0b2d7c4e13" } } } } } } } }}main.ts exports default { fetch(request, env) }, reads APIGAMI_API_URL from env and imports
home.html as text: nothing reaches the network while the module loads.
import home from './home.html' with { type: 'text' }
const PAGE_HEADERS = { 'content-type': 'text/html; charset=utf-8', 'x-content-type-options': 'nosniff', 'referrer-policy': 'no-referrer', 'cross-origin-opener-policy': 'same-origin-allow-popups', 'x-robots-tag': 'noindex',}
const sayHello = async (request: Request, env) => { // Who is asking comes from the run token Apigami hands the action, never from its input. const token = request.headers.get('X-Apigami-Run') const me = await fetch(`${env.APIGAMI_API_URL}/v1/me`, { headers: { Authorization: `Bearer ${token}` } }) if (!me.ok) return Response.json({ error: 'unauthorised' }, { status: 401 }) const { user_ref } = await me.json() const input = await request.json().catch(() => ({})) const name = typeof input?.name === 'string' && input.name.trim() ? input.name.trim().slice(0, 80) : 'world' return Response.json({ message: `Hello, ${name}!`, user_ref })}
export default { async fetch(request: Request, env) { const { pathname } = new URL(request.url) if (request.method === 'GET' && pathname === '/healthz') return Response.json({ ok: true }) if (request.method === 'GET' && pathname === '/') return new Response(home, { headers: PAGE_HEADERS }) if (request.method === 'POST' && pathname === '/hello') return sayHello(request, env) return Response.json({ error: 'not found' }, { status: 404 }) },}<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hello</title><h1>Hello</h1><form id="hello"> <input id="name" placeholder="Your name" maxlength="80"> <button>Say hello</button></form><p id="answer"></p><script src="https://api.eu.apigami.com/apigami.js"></script><script type="module"> // Nothing personal is rendered here: the greeting comes from the action, under the person's token. // ready() waits for the session, sending a visitor through Apigami's connect page and back first. document.getElementById('hello').addEventListener('submit', async (event) => { event.preventDefault() try { await apigami.ready() const { message } = await apigami.call('sayHello', { name: document.getElementById('name').value }) document.getElementById('answer').textContent = message } catch (error) { document.getElementById('answer').textContent = String(error) } })</script>With the MCP tools: app_settings with a name, then app_version_write with the three files, then
app_run with the action sayHello, app_deploy, app_run again with version on the deployed
version, and app_publish, which refuses a version never run once deployed. Over HTTP, create the
application, then write the version from the three files in one call:
APP_ID=$(curl -s -X POST https://api.eu.apigami.com/v1/apps \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"name":"Hello"}' | jq -r .id)
jq -n --rawfile manifest manifest.json --rawfile main main.ts --rawfile home home.html '{ base_version_id: null, changes: [ { op: "write", path: "manifest.json", content: $manifest }, { op: "write", path: "main.ts", content: $main }, { op: "write", path: "home.html", content: $home } ]}' | curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' --data-binary @-The answer carries the version_id; then run, deploy and publish it as
Run it, deploy it, publish it shows. The address to give people is
https://app.apigami.com/connect?app_id=APP_ID: it connects them, then opens the home page. The home page’s own
address works too: when the page needs a person, it sends a visitor through the same connection, and back.
The guide, page by page
Section titled “The guide, page by page”Read them in this order the first time; each one stands on its own afterwards.
- The manifest:
manifest.json, the sites an application reaches and the name it goes by. - Write the server: the HTTP server whose routes are the actions, what an action receives and how it reaches a site.
- Push and publish a version: send the files, fix what the validation found, try an action, put a version in production, find out what failed.
- Side panel, home and settings pages: the screens an application serves.
- The person behind a page: which person a page serves, on the server and in the page, and how long a page keeps its access.
- Database and schedules: a SQLite database per application, PostgreSQL when it needs one, and actions that run on their own.
- Sign in to other services: a person’s account elsewhere, and asking them to set up or reconnect.
- Call other applications: use what another application already does, and show its settings.
- Your own address: serve production at an address of yours.
- Example: watch a page and show what is new: a complete application, a trigger reading a page through the person’s browser, its database and a home page.