---
topic: deploy
connector: deploy_app
api: POST https://ship.embarko.ai/apps
requires: [prerequisites]
---

[← Guide home](https://embarko.ai/docs)

# Requirements, options and errors

**On this page:** What each route accepts, Requirements, How it starts, App name, With the connector, Over HTTP, Examples, Large apps, Limits, Errors, What to tell the person

> Plain words unless the person is a developer. Never invent a link, name or code. [Rules](https://embarko.ai/docs/prerequisites) Needs first: [prerequisites](https://embarko.ai/docs/prerequisites).

Send the app's files; Embarko detects the language, builds it and serves it at https://{app-name}.embarko.app. No Dockerfile. The first deploy's reply has app.id: keep it as "appId" in embarko.json, and every redeploy updates the same app, even after the person renames it.

**Connector:** deploy_app

**Done when:** The app opens at https://{app-name}.embarko.app and its main feature works when you try it.

**What each route accepts**

- Connector: deploy_app takes the files themselves: [{ path, content, encoding? }]. Text as-is, binaries as base64.
- HTTP multipart: A .tar.gz of the app folder in the "source" form field. Not .zip.
- HTTP JSON: The same files list as the connector, as a JSON body.
- Upload page: A folder, a .zip, or one .html file (any name: it becomes the home page). A .tar.gz also works. The browser packs it.

**Requirements**

- The app reads its port from PORT and binds 0.0.0.0 (not a fixed port, not localhost).
- No Dockerfile: delete it or leave it out.
- The manifest (package.json, requirements.txt, go.mod, ...) or index.html sits at the top level of what you send.
- One upload is one app with one process. A frontend and a backend are two apps.
- Anything that must survive a redeploy is written under DATA_DIR (see the data topic).
- Leave out node_modules, build output, .git and .env files. Secrets go in env vars, not in the upload.

**How it starts**

- Node: Embarko installs dependencies, runs the build script if there is one, then npm start. The start script must listen on process.env.PORT and 0.0.0.0.
- Next.js: "next start" reads PORT by itself. Do not add -H localhost or -H 127.0.0.1.
- Vite, Create React App and other single-page apps (including Lovable and Bolt exports): send the project with its package.json and build script, not a built dist folder. Embarko builds it and serves the output as a website; no start script or server is needed, and links to inner pages (/products) load the app.
- Values a frontend build reads (VITE_..., REACT_APP_...) cannot come from .env (left out of the upload), and do not rely on env vars for them. Put them in a committed config file, for example src/config.ts. Public values are fine there: an API address, a Supabase URL and anon key, a Stripe publishable key. Never put a secret key in frontend code; secrets belong on a backend (see two-apps).
- Python: give it a main.py that starts the server itself on PORT and 0.0.0.0, with debug off. FastAPI: uvicorn.run(app, host="0.0.0.0", port=int(os.environ["PORT"])). Flask: from app import app, then app.run(host="0.0.0.0", port=int(os.environ["PORT"])). Dependencies go in requirements.txt.
- Plain HTML: nothing to start (see static-sites).

**App name**

- Pattern: `^[a-z0-9-]+$, no leading or trailing dash`
- Length: 3-63 characters
- Unique: Across all of Embarko. It becomes the address, so slugify: "My Landing Page" → my-landing-page.
- Reserved: Some names are reserved for Embarko and never free, for example www, api, app, admin, docs, help. A reserved name returns 409 app_name_reserved: pick another.

**With the connector**

1. Call deploy_app with { appName, files: [{ path, content, encoding? }], appType?, version? }.
2. Under 150 KB in total: call deploy_app once with all the files. Do not use start_upload for small apps. Over 150 KB in total, or a long conversation: send it in parts. Finish and call deploy_upload within 60 minutes, from the same token, or the upload expires. start_upload { appName, files } with the first part returns uploadId; add_upload_files { uploadId, files } for each next part; then deploy_upload { appName, uploadId }.
3. The reply has status and links.app. Poll read_app { action: "status", appName } as described in polling.

**Over HTTP**

1. Package the folder as .tar.gz (first example).
2. POST https://ship.embarko.ai/apps as multipart with the field source and the headers X-App-Name and X-Agent-Name (your own name, e.g. codex).
3. Optional headers: Authorization: Bearer {token} (permanent at once), X-App-Version (a git SHA), X-App-Type (what the app is for, e.g. portfolio), X-App-Id (app.id from an earlier reply, if you keep it somewhere other than embarko.json).
4. Settings such as timed jobs go in an optional embarko.json file in the app's top folder, sent with the other files (see embarko-json).
5. After the first deploy, write the reply's app.id into embarko.json as "appId" and keep the file with the app. Later deploys then reach this app even if the person renamed it in the dashboard.
6. Or POST https://ship.embarko.ai/apps with Content-Type: application/json and { appName, agentName, files } (second example).
7. Too big for one JSON call: POST https://ship.embarko.ai/uploads { appName, files } opens an upload and returns uploadId; POST https://ship.embarko.ai/uploads/{uploadId}/files { files } adds more (a file entry with "append": true continues a file already sent, so one big file can be split); then POST https://ship.embarko.ai/apps { appName, agentName, uploadId } deploys it. Same token on every call. 25MB and 2,000 files per upload; it expires 60 minutes after its last call.
8. A server app replies 202 "building". A plain static site usually replies 200 "live" at once. Then poll as described in polling.
9. Read notices[] in the reply: it says what the person changed in the dashboard since your last deploy, such as a rename. Use app.name from the reply from then on.

**Example: Multipart upload (server app) → 202 building**

```bash
tar -czf /tmp/app.tar.gz --exclude=node_modules --exclude=.git --exclude='.env*' --exclude=dist --exclude=.next --exclude=venv --exclude=.venv --exclude=__pycache__ --exclude='*.db' -C /path/to/app .
curl -sS -X POST 'https://ship.embarko.ai/apps' -H 'X-App-Name: my-app' -H 'X-Agent-Name: codex' -H 'X-App-Type: portfolio' -F 'source=@/tmp/app.tar.gz'
```

Reply 202:

```json
{"success":true,"status":"building","app":{"name":"my-app","kind":"service","claimed":false,"id":"cmh1q2w3e0001abcdmyapp","expiresAt":"2026-10-10T06:50:46.326Z"},"action":"created","next":"New app: save app.id as \"appId\" in embarko.json, so later deploys reach this app even if it is renamed. Building: poll links.status every 10 seconds until it says \"live\", \"failed\" or \"crashed\".","links":{"status":"https://ship.embarko.ai/apps/my-app/status","feedback":"https://ship.embarko.ai/apps/my-app/customer-query"}}
```

**Example: JSON upload (static site) → 200 live**

body.json:

```json
{"appName":"my-site","agentName":"codex","files":[{"path":"index.html","content":"<h1>Hello</h1>"}]}
```

```bash
curl -sS -X POST 'https://ship.embarko.ai/apps' -H 'Content-Type: application/json' --data @body.json
```

Write body to body.json first, then run the command.

Reply 200:

```json
{"success":true,"status":"live","app":{"name":"my-site","kind":"static","claimed":false,"id":"cmh1q2w3e0002abcdmysite","expiresAt":"2026-10-10T06:50:46.326Z"},"action":"created","next":"New app: save app.id as \"appId\" in embarko.json, so later deploys reach this app even if it is renamed. Live: give the user links.app and tell them it will be deleted at 2026-10-10T06:50:46.326Z unless claimed. To claim it, follow links.help.","links":{"app":"https://my-site.embarko.app","feedback":"https://ship.embarko.ai/apps/my-site/customer-query","help":"https://embarko.ai/docs/claim"}}
```

**Example: Redeploy after a rename in the dashboard → 202, reached the renamed app**

Reply 202:

```json
{"success":true,"status":"building","app":{"name":"my-shop","kind":"service","claimed":true,"id":"cmh1q2w3e0001abcdmyapp"},"action":"updated","notices":[{"code":"app_renamed","from":"my-app","to":"my-shop","detail":"This app was renamed from \"my-app\" to \"my-shop\" outside this deploy, so this deploy updated \"my-shop\". Use \"my-shop\" as the app name from now on."}],"next":"This app was renamed to \"my-shop\" outside this deploy, so this deploy updated it: use \"my-shop\" from now on and keep app.id as \"appId\" in embarko.json. Building: poll links.status every 10 seconds until it says \"live\", \"failed\" or \"crashed\".","links":{"status":"https://ship.embarko.ai/apps/my-shop/status","feedback":"https://ship.embarko.ai/apps/my-shop/customer-query"}}
```

**Example: Name taken → 409**

Reply 409:

```json
{"error":"App name \"my-app\" is already in use by a different workspace","code":"app_name_taken","next":"That name belongs to someone else: pick a different one.","links":{"feedback":"https://ship.embarko.ai/api/public/customer-query"}}
```

**Polling:** GET https://ship.embarko.ai/apps/{app-name}/status every 10s (2s for a static site) until it is final. Statuses and limits: [status](https://embarko.ai/docs/status).

**Large apps**

A connector call (and a JSON deploy) carries every file's full text inside one tool call, so it is limited by how much you, the AI, can write in one reply, not by Embarko. Embarko takes up to 25MB this way and 100MB through the upload page. In practice, past about 150 KB of files in total, or late in a long conversation, the call breaks or gets cut off. So send a big app in parts: each call stays small.

- Send source files only: no node_modules, build output or videos. Images the app shows are fine to send; only very large media (over about 5MB each) should move to an outside host.
- Check the size before you start. Under 150 KB in total: call deploy_app once with all the files. Do not use start_upload for small apps. Over 150 KB in total, or the conversation is already long: do not attempt one deploy_app call; send it in parts with start_upload, add_upload_files and deploy_upload. Finish and call deploy_upload within 60 minutes, from the same token, or the upload expires. Do not compress or minify the code to squeeze it in; one wrong character breaks the app.
- You can run commands that reach ship.embarko.ai: use the HTTP .tar.gz upload. The file goes straight from disk, so size is no problem.
- You have the connector: send the app in parts, about 120 KB of content per call. start_upload { appName, files } with the first part returns uploadId. Then add_upload_files { uploadId, files } for each next part, one call after another. Then deploy_upload { appName, uploadId }, and poll read_app as usual.
- To split one big file, such as a single-page index.html: send its first piece in start_upload, then each next piece with the same path and "append": true. Pieces join in the order sent, so cut anywhere. Base64 pieces must each be valid base64 on their own.
- Over HTTP the same upload works with a deploy token (see steps.http_api).
- Neither works (a chat app with no connector, such as Gemini, or the parts keep getting cut off): give the person the app as a file to download, the .html itself if the app is one page, otherwise a .zip of the folder. Tell them with say_to_user.too_big_for_chat, filled in.
- Be clear whose limit it is: the chat tool's, not Embarko's and not their app's. Do not say Embarko failed, and do not tell them to start a new chat and try again: the upload page is quicker and always works.
- A deploy_app call already cut off mid-way: do not resend it. Check read_app { action: "status", appName } once; it normally shows nothing deployed. Then send the app in parts as above.
- An add_upload_files call with no reply: you cannot tell if its piece arrived, and an "append" piece sent twice breaks the file. Start a new upload and send everything again, in smaller parts. A refused call (4xx) added nothing: fix that piece and send it again. 404 upload_not_found: the upload expired (an hour unused) or was deployed; start a new one and send everything again.
- Claude with a code sandbox: you may mention once, as their choice, that they can add ship.embarko.ai to the allowed domains of Claude's code execution (Settings → Capabilities; on a Team or Enterprise plan an Owner sets it). After that you can send big apps from your sandbox over HTTP. Never push it, and never hold up the upload for it.
- Once they paste the link back, open it yourself and check the main feature works. If it went through the upload page, redeploys work the same way: upload again on its page in the dashboard.

- say_to_user.too_big_for_chat: "Your app is ready, but it's too big for me to send from this chat. That's a limit of this chat, not of Embarko or your app. Here it is as a file: {file}. Sign in at https://embarko.ai/app (signing up is free), choose Host new app, open Upload file, pick the file and name it {app-name}. It's yours to keep, and it'll be live in about a minute; send me the link and I'll check it works."
- say_to_user.cant_send_from_chat: "I can't send apps to Embarko from this chat. That's a limit of this chat, not of Embarko, which is working fine. Here's your app as a file: {file}. Sign in at https://embarko.ai/app (signing up is free), choose Host new app, open Upload file, pick the file and name it {app-name}. It's yours to keep; send me the link once it's up and I'll check it works."

**Limits**

- Memory: 256MB per app; it cannot be raised.
- JSON and connector uploads: at most 25MB and 2,000 files. Multipart .tar.gz has no such file count.

**Errors**

| Code | Status | Retry | What to do |
| --- | --- | --- | --- |
| `host_not_allowed (from your own sandbox)` | 403 | No | Not Embarko: your environment blocks ship.embarko.ai. Switch route: connector, or ask the person to connect it. A big app goes through the connector in parts (see large_apps on the deploy topic; it also says how they can allow ship.embarko.ai for your sandbox, if they choose). |
| `permission_denied` | 403 | No | The token's owner may not publish this app (that needs Full access to it, permission "app.deploy") or add new apps ("app.create"). The reply names their access. Nothing was deployed. Tell the person (say_to_user.no_permission). |
| `app_name_taken` | 409 | No | Another account owns the name. On a first deploy, pick a close variant yourself: add the person's or business name if you know it, otherwise a short word (portfolio → portfolio-site, then portfolio-app). Mention the name with the link. Ask only if the exact name matters to them. |
| `no_manifest_at_root` | 422 | No | Read foundNestedAt and package from that folder. |
| `app_nested_in_subdirectory` | 422 | No | Deploy the folder named in deployThisInstead. |

Every other code, and the retry rules: see [errors](https://embarko.ai/docs/errors).

**What to tell the person**

- deploying: "Your app is deploying now. I'll tell you when it's live."
- failed: "It didn't go live because of a problem in the code. I'm fixing it now."
- no_permission: "Your access ({your access to the app, or your role}) doesn't let me {what it was}. An Owner or Admin can give you more on Members: https://embarko.ai/app/people"

**Next:** Deploy, then poll until the app is live.

**Related:** [status](https://embarko.ai/docs/status) · [logs](https://embarko.ai/docs/logs) · [static-sites](https://embarko.ai/docs/static-sites) · [data](https://embarko.ai/docs/data) · [claim](https://embarko.ai/docs/claim) · [feedback](https://embarko.ai/docs/feedback)

---

[Guide home](https://embarko.ai/docs) · JSON: add .json