Your First API
Section titled “Your First API”With hoody-exec, any script in its scripts directory is a live HTTP endpoint: write the file, and the URL exists. You do not need Express, Docker, or a CI/CD pipeline. Always write exec scripts through the exec service’s own scripts/write endpoint (not the Files service) so they land in the exec-managed scripts directory and pass validation.
Step 1: Write a script
Section titled “Step 1: Write a script”# Write a script to the exec scripts directory (path is relative to that dir)# All kit commands require a target container: -c CONTAINER_ID (or HOODY_CONTAINER)hoody exec scripts write -c CONTAINER_ID --path "api/hello.js" --create-dirs --content \'// @mode serverlessconst name = metadata.query.name || "World";return { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Scope to your container, then write through the exec service (not the Files service)const box = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
const script = `// @mode serverlessconst name = metadata.query.name || "World";return { message: \`Hello, \${name}!\`, timestamp: new Date().toISOString() };`;
await box.exec.scripts.write({ path: 'api/hello.js', content: script, createDirs: true, validate: true});# Write the script via the exec service's scripts/write endpoint.# `path` is relative to the exec scripts directory.curl -X POST "https://$PROJECT-$CONTAINER-exec-1.$SERVER.containers.hoody.com/api/v1/exec/scripts/write" \ -H "Content-Type: application/json" \ -d '{ "path": "api/hello.js", "content": "// @mode serverless\nconst name = metadata.query.name || \"World\";\nreturn { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };", "createDirs": true, "validate": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Writes the script through the exec service’s own endpoint, the same call the CLI and SDK make.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/v1/exec/scripts/write&method=POST&json={"path":"api/hello.js","content":"//%20@mode%20serverless\nconst%20name%20=%20metadata.query.name%20||%20\"World\";\nreturn%20{%20message:%20`Hello,%20${name}!`,%20timestamp:%20new%20Date().toISOString()%20};","createDirs":true,"validate":true}&response=transparent Step 2: Call it
Section titled “Step 2: Call it”Your script is already live at:
https://{projectId}-{containerId}-exec-1.{server}.containers.hoody.com/api/hello# Call your new API endpoint directly via its URLcurl "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.com/api/hello?name=Developer"// Exec scripts are live HTTP endpoints; call them directlyconst response = await fetch( `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.com/api/hello?name=Developer`);const data = await response.json();console.log(data);// { message: "Hello, Developer!", timestamp: "2026-03-04T..." }curl "https://$PROJECT-$CONTAINER-exec-1.$SERVER.containers.hoody.com/api/hello?name=Developer"Response:
{ "message": "Hello, Developer!", "timestamp": "2026-03-04T12:00:00.000Z" }One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Runs the same GET through the container’s cURL service and returns the script’s JSON response as-is.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/hello?name=Developer&method=GET&response=transparent That URL works from anywhere: a browser, a webhook, an AI agent, a phone. There is no separate deployment step.
Step 3: Chain services
Section titled “Step 3: Chain services”Exec scripts can call any other service in the container. Sibling services share the same host as your script; only the service slug changes. Derive the base host from metadata.url (the full request URL) and swap in the target service:
// @mode serverless// metadata.url is the full request URL, e.g.// https://PROJECT_ID-CONTAINER_ID-exec-1.SERVER.containers.hoody.com/api/deploy-reportconst { hostname } = new URL(metadata.url);const host = (svc, idx = 1) => `https://${hostname.replace(/-exec-\d+\./, `-${svc}-${idx}.`)}`;
// Run the build (ephemeral=true auto-creates an isolated PTY for programmatic execution)const build = await fetch(`${host('terminal')}/api/v1/terminal/execute?ephemeral=true`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'npm run build', wait: true })});
// Log to databaseawait fetch(`${host('sqlite')}/api/v1/sqlite/db?db=logs&create_db_if_missing=true`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transaction: [ { statement: `INSERT INTO deploys (status, time) VALUES ('success', '${new Date().toISOString()}')` } ] })});
// Send a notification: `display` is required, and `1` is the container's primary desktopawait fetch(`${host('n')}/api/v1/notifications/notify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display: '1', summary: 'Deploy complete', body: 'Build succeeded' })});
return { status: 'deployed', timestamp: new Date().toISOString() };One script coordinates three services with plain HTTP requests and no additional infrastructure.
What you built
Section titled “What you built”You created a live API endpoint by writing a file, with no package.json, npm install, docker build, or deploy command. The script’s path, relative to the exec scripts directory, is the URL path:
| Script path | URL path |
|---|---|
api/hello.js | /api/hello |
api/users/list.js | /api/users/list |
api/deploy.ts | /api/deploy |
webhooks/stripe.js | /webhooks/stripe |
Each script is an endpoint, each endpoint can call any service in the container, and every service speaks HTTP. Writing the script, calling it, and chaining services all happened through URLs.
Next: The Hoody Kit →