Everything Embarko can do, one entry each, in the order an agent needs it. Each entry says what it is, when you need it, how to use it, and what to know before you rely on it.
If this page and the live endpoint disagree, believe the endpoint.
GET https://ship.embarko.ai/capabilities
is generated by the platform, so it is always current. This page is written
by hand and can fall behind.
/capabilities/custom-domain, for the full answer.via field that names who can use it:{
"runtime": { "port_env": "PORT", "bind_address": "0.0.0.0", "dockerfile": "rejected" },
"persistence": { "data_dir_env": "DATA_DIR", "survives_redeploy": true, "survives_node_loss": false },
"features": {
"deploy": { "status": "available", "via": ["agent", "dashboard"] },
"env_vars": { "status": "available", "via": ["agent", "dashboard"] },
"rollback": { "status": "available", "via": ["agent", "dashboard"] },
"custom_domains": { "status": "available", "via": ["agent", "dashboard"] },
"analytics": { "status": "available", "via": ["agent", "dashboard"] },
"rename_app": { "status": "available", "via": ["agent", "dashboard"] },
"change_app_address": { "status": "available", "via": ["agent", "dashboard"] },
"delete_app": { "status": "available", "via": ["agent", "dashboard"] },
"backups": { "status": "unavailable" }
}
}
Entries marked NOT AVAILABLE YET are planned but not there. An app that needs one today cannot use it. Each entry says what to do instead.
An agent with a deploy token can do almost everything here, including custom domains, analytics, renaming and deletion. One step it cannot do: create the DNS record for a custom domain. Only the domain's owner can, at their own registrar.
Everything is on ship.embarko.ai. The path does not tell you the auth
rule:
| Auth | Calls |
|---|---|
| None, ever | GET /capabilities, POST /api/public/… (rate limited) |
| None while unclaimed, token once claimed | deploy, status, logs, agent feedback |
| Deploy token, always | domains, analytics, rename, change web address, delete, env vars, deployment history, rollback, showcase |
Deploy, status, logs, domains, analytics, rename and delete sit under
/apps/<name>. Env vars, history, rollback and showcase sit under
/api/apps/<name>. Copy each path from its own entry. Don't guess one.
Upload an app's source. Embarko builds it, runs it and serves it at a live HTTPS URL.
When you need it: The user asks to deploy, ship, publish, release or "put online" an app you built.
How:
tar -czf /tmp/app.tar.gz --exclude=node_modules --exclude=.git \
--exclude='.env*' -C /path/to/app .
curl -X POST "https://ship.embarko.ai/apps" \
-H "X-App-Name: my-app" \
-H "X-App-Version: $(git rev-parse --short HEAD)" \
-H "X-App-Type: portfolio" \
-H "X-Agent-Name: claude" \
-F "source=@/tmp/app.tar.gz"
Authorization header.X-App-Type is optional. It says what the app is for (personal website,
portfolio, task management, feedback system...). Set it if you know.
If not, ask the user once. If they skip it, leave it out.X-Agent-Name is your own name as an agent (claude-code, claude,
chatgpt...). Always send it.Full parameter and error reference: API contract.
Good to know:
202 means accepted, not live. Poll links.status until status is
live before you report anything. Follow the next field in each reply.200 and live straight away. See
Static sites.index.html must be at the archive root..tar.gz for
anything bigger.Ship a new version of an app that already exists.
When you need it: Any change to an app that is already live.
How: Make the same call as Deploy, with the same
X-App-Name and a new X-App-Version. There is no separate update
endpoint and no "create then deploy" step. If the app is claimed, send the
deploy token. An anonymous deploy cannot update a claimed app.
Good to know:
DATA_DIR data carry over. Don't resend them.DATA_DIR
is lost.An upload that is just files is published as is, with no build and no server
process. That means an index.html and its assets, a public/ folder, or a
Staticfile.
When you need it: It is detected on every deploy. The reply has
app.kind: "static", and status is usually already "live" because the
request waits for the publish. A single-page app that needs a build step
(Vite, Create React App and similar) is still built and run as a server app.
How: Deploy it like any other app.
200 with status: "live" and the next step (claim
it, or the showcase).202 with
status: "publishing". Check GET .../status every 2 seconds until it
reads live.Good to know: Nothing runs on the server, so:
GET .../logs answers that a static site has no app logs.409
not_supported_for_static. Put configuration in the files.DATA_DIR, and no scale to zero.422 app_kind_changed. Use a new app name.X-App-Name is the app's identity and also its public subdomain. There is
no separate project id and no create-a-project step.
When you need it: Every call. Status, logs, env vars, history, rollback and showcase all use the app name.
How:
^[a-z0-9-]+$, 3 to 63 characters, with no
leading or trailing dash.my-landing-page.Good to know:
409 app_name_taken. Choose a different
name. This error does not mean "your app already exists". Redeploying
your own app is a normal success.ship, api, www, login, docs,
admin, mail, and others). Treat a rejection as final.PATCH /api/apps/<name>. See
Renaming an app. That never moves the URL.The result of the latest deploy attempt, plus the app's current running state.
When you need it: After every deploy and every rollback, until it settles. Deploys run in the background, so this is how you know the app works.
How:
curl "https://ship.embarko.ai/apps/my-app/status"
status is building or publishing while it
works.live, failed or crashed.next field says:
live.A static site's deploy reply is usually already live, so there is nothing
to poll.
Good to know:
404, which
never confirms the app exists.The output of a build that failed before the app started.
When you need it: status is failed and the app never came up. A
failed build starts no container, so there are no runtime logs.
How: Use the same logs call. When there are no runtime logs, it returns
the build output instead, with source: "build" and a note:
curl "https://ship.embarko.ai/apps/my-app/logs?stream=stderr"
It names the failing command. Read it before you change anything.
Good to know:
exit code: 127 is the most common sign. It means "command not found"
in the build image: the build tool isn't in the app's dependencies, or the
real app is in a subfolder whose dependencies were never installed.docker-import ... no such file line is a side effect, not the
cause.422 codes on the deploy call. See troubleshoot.md.The tail of the running app's stdout and stderr. It is the only view
into a live app.
When you need it: The app is up but wrong, crashing, or restarting.
How:
curl "https://ship.embarko.ai/apps/my-app/logs"
curl "https://ship.embarko.ai/apps/my-app/logs?stream=stderr"
{ available, allocId, stream, logs }.{"available": false, "reason": "..."} with
a next step.Good to know:
OOM Killed / Exit Code: 137 means the app hit the 256MB limit.Not Restarting / Exceeded allowed attempts is a crash loop. The
real cause is logged above it.The app embeds its own database, SQLite via better-sqlite3, and writes the
file under DATA_DIR, which survives every redeploy.
When you need it: The app needs structured data to outlast a deploy or restart: records, accounts, orders, content.
How: Open the database at a path under DATA_DIR, and create the folder
on startup:
const dir = process.env.DATA_DIR;
fs.mkdirSync(dir, { recursive: true });
const db = new Database(path.join(dir, "app.db"));
Good to know:
DATA_DIR survives redeploys, restarts and the app being stopped. It lives
on one machine and does not survive losing that machine.window.storage. It is a Claude Artifacts sandbox API and is
refused at deploy time with 422 unsupported_storage_pattern.Files written under DATA_DIR persist across redeploys. That covers user
uploads, generated files and caches.
NOT AVAILABLE YET: a managed object store, meaning durable file storage
separate from the app's own disk, with its own URLs, redundancy and
lifecycle. DATA_DIR is one folder on one machine. It persists, but it is
not an object store and has no redundancy.
When you need it: Any time the app accepts uploads or writes files it must keep.
How, today:
path.join(process.env.DATA_DIR, "uploads").{ recursive: true } on startup.Good to know:
Per-app environment variables, encrypted at rest and visible only to that app at runtime. Keep secrets out of the source: the tarball becomes an image, and anything in it ships with the app.
When you need it: Any API key, credential, or environment-specific setting. Not for static sites: they run no process, so env var writes to them are refused.
How: Needs a deploy token.
curl -X PUT "https://ship.embarko.ai/api/apps/my-app/env-vars/STRIPE_KEY" \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "sk_live_..."}'
curl -X POST "https://ship.embarko.ai/api/apps/my-app/env-vars/apply" \
-H "Authorization: Bearer $DEPLOY_TOKEN"
Other calls: GET /env-vars lists keys, PUT /env-vars replaces all,
DELETE /env-vars/<key> removes one.
Good to know:
apply or
redeploy, or the app keeps the old value.GET returns keys only. No one holding the
token can read a secret back, including you.^[A-Za-z_][A-Za-z0-9_]*$.NOT SUPPORTED. There is no cron, no timer, and no way to have Embarko
call your app on a schedule. An in-process scheduler (node-cron,
APScheduler, a setInterval loop) is not reliable either: the process
restarts on every redeploy and may be stopped when idle, so the schedule may
never fire.
When you'd want it: Anything recurring: a nightly export, an hourly sync, a weekly email, reports, cleanups, digests, polling. An app that assumes a schedule will never run that code.
How, today: Keep the app on Embarko and trigger it from outside.
Don't fake it with an in-process timer. The app can restart or move at any
time, so a setInterval is not a schedule.
Good to know: Tell the user the schedule is external until this ships. Do not report a scheduled feature as working just because the route exists.
NOT AVAILABLE YET. Planned. There is no worker process, no queue, and no
supported way to run work that outlasts the request that started it.
Embarko runs one long-lived process per app, listening on PORT.
Background work shares the same 256MB and CPU share with request handling,
and is lost if the container moves.
When you'd want it: Image or video processing, long imports, retries with backoff, anything that takes minutes.
How, today:
Good to know: No queue, no retries, no concurrency control, and no instance count to raise. A job that outlasts its request may not finish.
Every path on the app's URL reaches the app. The app does its own routing, including routes that receive webhooks from Stripe, GitHub, or anything else. There is no route registration, gateway setup or per-endpoint config.
When you need it: Any public API, form handler, callback URL or webhook receiver.
How:
https://<app-name>.embarko.app/<your-path>.Good to know:
Content-Security-Policy, are set by the
platform at the edge. They override what your app sets.ws, Pusher-style realtime). An app that needs live push
(chat, multiplayer, live dashboards) will deploy, but its realtime features
won't work. Use plain HTTP requests, polling from the browser on an
interval.Every app is served at https://<app-name>.embarko.app from its first
deploy. The user gets a shareable link at once, with no domain to buy and no
DNS to set up.
When you need it: Always. It is the URL you hand back.
How: Nothing to do. It comes from the app name, so choose the name with care: it is the subdomain.
Good to know:
The user's own domain in front of the app, next to the default URL.
When you need it: The user asks for it and owns the domain.
How: An agent can do every step except the DNS record.
# 1. Register it. Returns the exact DNS record to create.
curl -X POST "https://ship.embarko.ai/api/apps/my-app/domains" \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "app.yourdomain.com"}'
# 2. After the owner creates that record and it propagates:
curl -X POST "https://ship.embarko.ai/api/apps/my-app/domains/app.yourdomain.com/verify" \
-H "Authorization: Bearer $DEPLOY_TOKEN"
Other calls: GET /api/apps/my-app/domains lists what's registered.
DELETE /api/apps/my-app/domains/<domain> removes one.
Good to know:
CNAME. An apex gets an A record, which skips any CDN
or DDoS proxy in front of it. Prefer a subdomain.*.embarko.app URL.400 domain_reserved).409 domain_taken).TLS certificates are issued and renewed automatically, for *.embarko.app
and for connected custom domains. Nothing to buy, install or renew.
When you need it: Always on. There is no HTTP-only mode.
How: Nothing to do.
Good to know:
NOT AVAILABLE YET. Actively in progress.
What it will be: Sign-up, login and sessions for the app's own end users, provided by Embarko, so the agent doesn't build auth for every app.
What it is not: Authentication, which is the deploy token you use to call the platform. Never mix the two up in a message to a user.
When you'd want it: Any app with accounts: a CRM, a dashboard, anything per user.
How, today: Build auth into the app. The app is a real server, so
sessions, a users table in SQLite under DATA_DIR, and hashed passwords all
work. Do not wait for the platform feature, and do not tell the user one
exists.
Good to know: Nothing is provided today: no identity provider, no OAuth integration, no session handling, no user store.
NOT AVAILABLE YET. Planned.
What it will be: A gate in front of the whole app, so only invited people can open it. End-user login gives an app users. This makes an app private.
When you'd want it: An internal tool, a client demo, a staging version that shouldn't be public.
How, today: Every deployed app is public to anyone with the URL. If it must be private, the app has to enforce that itself, for example with a password gate on every route. A hard-to-guess app name is not privacy.
Good to know: No IP allowlist, no basic-auth toggle, no preview protection. If the user assumes the app is private, tell them plainly the URL is public.
The lasting record of every deploy: its id, its version, when it ran, and whether it worked. It shows what is running, what changed, and what you can go back to.
When you need it: Before a rollback, and when the user asks what shipped.
How:
curl "https://ship.embarko.ai/api/apps/my-app/deployments" \
-H "Authorization: Bearer $DEPLOY_TOKEN"
Newest first. GET /deployments/<id> returns one entry. Use it to poll a
rollback too.
Good to know:
X-App-Version, or entries are just timestamps. Never
reuse a tag for different code.Put an earlier successful build back in service. It switches to an image that already exists, with no rebuild, so recovery takes seconds.
When you need it: A deploy broke something and the previous version was fine. Restore first, debug after.
How:
POST /rollback with {}.deploymentId.curl "https://ship.embarko.ai/api/apps/my-app/deployments" \
-H "Authorization: Bearer $DEPLOY_TOKEN"
curl -X POST "https://ship.embarko.ai/api/apps/my-app/rollback" \
-H "Authorization: Bearer $DEPLOY_TOKEN" -H "Content-Type: application/json" \
-d '{"deploymentId": "<deploymentId>"}' # or '{}' for the previous good one
202. Poll the returned deployment until it leaves
in_progress.Good to know:
422 rollback_image_unavailable. Its retainCount says how far back you
can go.DATA_DIR is untouched.NOT AVAILABLE YET. Actively in progress.
What it will be: Automatic backups of an app's persistent data, with a way to restore.
What exists today: Nothing. Data under DATA_DIR survives redeploys and
restarts, and that is the whole promise. There is no snapshot, no backup,
and no restore. Data is safe from your deploys, not from hardware loss.
When you'd want it: Any app with data the user would hate to lose, which is most of them.
How, today: Have the app export its own data: a scheduled dump the user can download, an endpoint that streams the SQLite file, or a copy pushed to storage they control. Tell the user this is the situation. Don't let them assume backups exist.
Good to know:
The health object in the status reply. It shows the app's current running
state, apart from how the last deploy went. An app can be healthy while the
newest deploy failed, because the previous version is still serving.
When you need it: To check that a live app is actually up, or after a deploy that worked but behaves wrongly.
How:
curl "https://ship.embarko.ai/apps/my-app/status" # read the `health` field
Good to know:
Not Restarting / Exceeded allowed attempts.health, and whether a
readiness/liveness check can be set per app. Branch only on the status
reply's status field (live, crashed, …), which is specified.Each app runs in its own environment, with its own filesystem and its own volume, on managed infrastructure certified to ISO/IEC 27001.
When you need it: The user asks about security, where data lives, or compliance. It decides whether an app can hold other people's data.
How: Nothing to set up. For a compliance answer, point to the DPA and Privacy Policy. Don't summarise them.
Good to know:
embarko.ai or the dashboard, which run elsewhere.What every app gets without asking: 256MB of memory, a shared CPU share, one process that stays on (unless it scales to zero), and a runtime detected from the source by Railpack. There is nothing to size or pick, so never ask the user about these.
When you need it: Designing the app, and diagnosing an OOM.
How: Build to the defaults.
process.env.PORT.0.0.0.0.Good to know:
Caddyfile for static sites), but not replacing the build.An opt-in that lets a server app sleep after a stretch with no traffic and wake on the next request. Good for an app that is used now and then.
When you need it: Decide before you build. Use it only when a few seconds' delay on the first request after a quiet spell is fine: an internal tool or a rarely used form, not a live dashboard.
How: Send X-Scale-To-Zero: true on POST /apps to opt in, or false
to turn it off. Without the header nothing changes: apps stay on.
Good to know:
DATA_DIR, which survives the stop.404 while it sleeps. Only real requests wake it.Topic: GET /capabilities/scale-to-zero.
Remove the running app, its routing, its image and its record.
When you need it: The user clearly asks to remove an app for good.
How: The token alone is not enough. Resend the app's own name to confirm.
curl -X DELETE "https://ship.embarko.ai/api/apps/my-app" \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirmAppName": "my-app"}'
If confirmAppName is missing or wrong, it returns
400 confirmation_required and deletes nothing.
Good to know:
DATA_DIR data
is destroyed, and the name is freed for anyone to take.confirmAppName guards against a misread instruction,
not against an agent that is sure and wrong. See
When to involve the human.Change an app's display name, the name the dashboard shows. The slug,
and so the live URL, never changes. An app slugged my-app in a hurry can
still show as "Customer Portal".
When you need it: The user dislikes the shown name, or it was made up automatically.
How:
curl -X PATCH "https://ship.embarko.ai/api/apps/my-app" \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "My App"}'
Good to know:
An Embarko MCP connector lets an agent deploy through its tools instead of
making HTTP calls. It shows in the connector list as Embarko. It needs
no curl, no packaging and no token: it uses the account from the user's
login. Its requests come from the connector's servers, so it works where a
sandboxed agent cannot reach ship.embarko.ai at all.
When you need it: Whenever it is available, because it is the shorter path. Also when a direct call is blocked before it leaves your environment. See If you can't reach the API at all.
How:
| Tool | What it does |
|---|---|
Embarko Onboarding | Returns the deploy instructions. Read it first |
Deploy App | Deploys, given an app name and a source_url |
Submit Feedback | Reports a problem with Embarko itself |
Good to know:
source_url must be a direct link to a .tar.gz that anyone can
fetch, with no login wall and no redirect. The connector does not upload
a local folder the way POST /apps does, so the source must be hosted
somewhere reachable first. If it isn't, use the HTTP API.PORT and 0.0.0.0, no
Dockerfile, DATA_DIR for anything lasting, 256MB. A deploy with no
account connected is deleted after 24 hours.There are three ways for a person or an agent to reach an Embarko account. All three lead to the same account. This is not the same as end-user login for the app itself.
| Way in | Who uses it | What it gives |
|---|---|---|
| Connector | An AI tool with an Embarko connector (Claude today) | The tool acts as the signed-in person. No token handled at all |
| Token by email | An agent with no browser | A deploy token, emailed to the person, which they paste back |
| Sign-in on embarko.ai | A person, with Google | The dashboard, and from it, deploy tokens they can create and hand over |
One account per email. All three match the person by email: a token
requested for you@example.com and a Google sign-in as you@example.com
reach the same account and the same workspaces.
When you need it: Claiming an app, and every control-plane call: env vars, history, rollback, showcase. Not needed for a first deploy, which needs no credential at all.
How: Use the first option that works. Full steps: Choose how to authenticate.
$DEPLOY_TOKEN, or
~/.embarko/credentials.curl -X POST "https://ship.embarko.ai/api/public/deploy-tokens/request" \
-H "Content-Type: application/json" -d '{"email": "them@example.com"}'
Save a new token to ~/.embarko/credentials (chmod 600). Send it as
Authorization: Bearer <token>.
Good to know:
202,
whether or not the address exists or you hit a limit. Ask the person to
paste the token.401. It is never
treated as anonymous.A company's plan sets how many apps it can run at once and what usage is included. A deploy can be refused for reasons that have nothing to do with the app's code.
When you need it: Before deploying many apps for one user, or when a deploy is refused for a reason the error table doesn't explain.
How: Plans and limits are at embarko.ai/pricing, and can change apart from this page. Anything about the plan (upgrading, paying, changing a limit) is the user's decision and needs their approval.
Good to know:
Useful, but outside the core flow above.
A deploy with no credential that makes a real live app, which can be claimed later. No sign-up needed for the first deploy.
Authorization. To claim, redeploy the same name with a
token.app.expiresAt and the claim steps in the
status reply's next field once the app is live, plus a countdown banner
added to HTML pages. A JSON-only API has no page to add it to.Show a deployed app on the public Showcase gallery or on an event Showcase page (a hackathon or demo day). An app has one listing, reused on every page it appears on.
GET /api/public/showcase/apps/<name> (no
token).PUT /api/apps/<name>/showcase. Leave out collectionSlug for the
public gallery. Send an event page's slug otherwise.tagline and creatorName are required the first time. name defaults to
the app's name, and changing it renames the app in the dashboard too. Every
field and limit: List on Showcase.code is sent. Calling again merges, it doesn't error.
An unknown slug returns 404 SHOWCASE_COLLECTION_UNKNOWN.Report a platform limit or a misleading doc from inside a deploy.
POST /apps/<name>/customer-query with an X-Agent-Name header
and {type, message}. Without an app, POST /api/public/customer-query
with {type, message, email}.X-Agent-Name is required. type is the kind of
problem: platform_bug, missing_capability, docs_issue,
unexpected_behavior or other. message is 1 to 5000 characters. Full
detail: API.Request traffic (total and by status code) and CPU/memory use.
GET /api/apps/<name>/analytics?range=7d with a deploy token.
range is 24h, 7d, 30d, 90d or all (default 24h). The same
data is on the dashboard's Analytics tab.503 means the analytics system is unreachable. That
is not zero traffic, so don't report it as zero. No alerting and no metrics
export.| Capability | Status | Who |
|---|---|---|
| Deploy / redeploy | Available | Agent |
| Application identity | Available | Agent |
| Deployment status | Available | Agent |
| Build errors | Available | Agent |
| Runtime logs | Available (server apps only) | Agent |
| Database (embedded SQLite) | Available | Agent |
Persistent files under DATA_DIR | Available | Agent |
| Secrets / env vars | Available (server apps only) | Agent |
| Static sites (no build, no server) | Available | Agent |
| Public HTTP and webhook routes | Available | Agent |
| Default domain + automatic HTTPS | Available | Agent |
| Deployment history | Available | Agent |
| Rollback | Available | Agent |
| Application health (field only) | Available | Agent |
| Isolation | Available | None |
| Resource defaults (256MB, 1 process) | Fixed | None |
| Scale to zero (opt-in) | Available | Agent |
| Agent / API authentication | Available | Agent |
| MCP connector (deploy, feedback) | Available | Agent (user connects it) |
| Showcase listing | Available | Agent |
| Rename (display name) | Available | Agent |
| Custom domains | Available | Agent (DNS record: user) |
| Delete application | Available | Agent (confirmation required) |
| Analytics | Available | Agent |
| Account / project limits | Plan-based | User |
| Backups and restore | Not available, in progress | None |
| Application authentication (end users) | Not available, in progress | None |
| Access protection (private apps) | Not available, planned | None |
| WebSockets / realtime connections | Not supported | None |
| Scheduled jobs / cron | Not supported | None |
| Background jobs / workers | Not available, planned | None |
| Managed object storage | Not available, planned | None |
| Managed database service | Not available, planned | None |
| Memory / CPU adjustment | Not available | None |
| GPUs | Not available, not planned | None |
GET /capabilities/not-supported
is the final word. Anything it lists is truly absent, however old this table
is.