Erawan Docs llms.txt Console

Deploy with your coding agent

Connect your agent to Erawan once. After that you ask for what you want, and it writes the code, sends it, watches the build, and gives you a link.

Connect your agent

One account works everywhere. Most clients open a browser to sign in, so there is no key to copy or store.

Claude Code, Codex, OpenCode

Add the server once, in the project you work in:

claude mcp add --transport http erawan https://api.erawan.cloud/mcp

A browser opens, you sign in, and you approve the connection. Codex and OpenCode take the same URL in their own MCP settings.

Claude on the web

Settings → Connectors → Add custom connector, then paste https://api.erawan.cloud/mcp and connect. Chat has no filesystem, so your agent sends the files it wrote straight from the conversation.

Anything else, with no account at all

A first deploy needs nothing from you — no email, no password, no confirmation link. Any agent with a shell can ask for a guest session and put a site online in a minute:

curl -sX POST https://api.erawan.cloud/v1/guest

It returns an access token to deploy with, and a keep_url to hand the person it was built for. Opening that link is how the session becomes an account — the same apps, at the same address.

What a guest session can do

Websites: static files, or a frontend that builds to them. Up to three, and they are deleted after 24 hours unless somebody opens the keep_url. Names that read as a bank or a sign-in page are refused, and anything that has to keep running needs a plan.

There are no API keys to store. An agent that connects through the browser holds a token of its own, which you can revoke by name in the console.

Your first deploy

You do not call anything yourself. Describe what you want and let the agent work.

Build a FastAPI service that shortens URLs and deploy it on Erawan.
Call it shortener, and tell me the address when it is live.

1 · It is checked

Leaked passwords and keys, a bundled .env, an app listening only to itself — each stops the deploy, and your agent is told how to fix it.

2 · It is built

The deploy returns straight away and the build runs in the background. Your agent watches it and reads the error if something breaks.

3 · You get a link

Every app gets its own address with HTTPS. Deploy again for a new version; roll back if the new one is worse.

Apps live at {name}.erawan.app. The console shows the same apps, versions and logs — plus every agent connected to your account, each one revocable.

Sending code

Three ways in, and your agent picks the one that fits where it is running. They all end up in the same place, with the same checks and the same history.

Where your agent runsWhat it uses
On the same machine as the projectdeploy_app with a folder path
Somewhere else, but it has a terminalUpload the project first, then deploy_app with the upload
In a browser, with no files at alldeploy_files — the code goes inline

The upload step, for agents with a terminal:

tar -czf - . | curl -sS -X POST https://api.erawan.cloud/upload \
  -H "Authorization: Bearer erw_at_…" --data-binary @-

Runtimes

Erawan works out how to run your project. A Dockerfile wins if you wrote one, then Python, then Node, then a plain website.

KindRecognised byHow it starts
Dockera Dockerfileyour image, as written
Pythonmain.py, app.py or server.pyFastAPI runs under uvicorn; anything else runs the file
Nodepackage.jsonnpm start, or index.js / server.js / app.js
Websiteindex.html and no buildserved directly — no container, nothing to keep running

Never scope a cookie to the shared domain

Every app answers on a subdomain of one domain, so a cookie set with Domain=.erawan.app — from your server, or from document.cookie in the browser — is sent to every other customer's app on it, and their pages can set one yours will receive. Leave the domain off: the cookie then belongs to your app's own address and nothing else. The pre-deploy check refuses a deploy that does this. If two apps you own genuinely need to share a session, pass a token in a header between them instead. The check reads your files and cannot see a string your code builds while it runs, so treat it as a rule rather than something we catch for you.

Two more that follow from the same fact

Name a session cookie __Host-something — the prefix makes the browser refuse any attempt by a neighbouring app to set or replace it, and it is the one protection here that does not depend on us. And do not rely on SameSite to stop cross-site request forgery: every app on the shared domain counts as the same site, so those cookies are still sent on requests coming from a neighbour. Check a token your app issued. Anything with a login belongs on your own domain — a custom domain is its own site to a browser, which is where the boundary actually is.

Two things break most first deploys

The app must listen on 0.0.0.0, not 127.0.0.1 — otherwise nothing outside the container can reach it. And it has to actually start serving: a Flask app needs app.run(host="0.0.0.0", port=8000), or tell your agent to pass a start command such as gunicorn app:app -b 0.0.0.0:8000.

Plain websites are the cheap path: they go up in seconds and cost nothing to keep online. In exchange they have no environment variables, no rollback and no running logs — there is no process to have them.

Addons

Ask for one and it appears next to your app, already connected. Your app is restarted with the settings it needs, so there are no connection strings to wire up by hand.

Postgres

A real database with its own storage that survives deploys. Your app reads DATABASE_URL. pgvector is included on every plan — CREATE EXTENSION IF NOT EXISTS vector works as written.

Redis

For caching, queues and sessions. Your app reads REDIS_URL.

File storage

S3-compatible object storage with a bucket ready to use, for uploads and generated files.

Email

Receipts, sign-in codes, notifications. Your app posts the message to Erawan — no provider account, no keys of your own.

Bringing a database you already have

Add the addon, then send your dump straight in. It travels over the same authenticated connection as everything else and is never published.

gzip -c dump.sql > dump.sql.gz
erawan addon add postgres myapp
erawan db import myapp dump.sql.gz

Never put a dump on a public URL to fetch it back, however briefly. A CDN cache and the access logs at both ends keep the file after you delete the link, and a random token in a path is not a password.

The import stops at the first statement that fails rather than reporting success over half a database, and refuses a database that already has tables unless you pass --force. Import before the app runs its own migrations.

One app, more than one process

Most apps are a single process and need nothing here. When a repository runs a web server and a background worker, put an erawan.yaml at its root and deploy as usual — the file is read on our side, so no particular version of the CLI is needed.

version: 1
addons: [postgres, redis]
components:
  web:
    path: .
    port: 8000
    public: true
    resources:
      requests: {memory: 256Mi, cpu: 100m}
      limits:   {memory: 1Gi,   cpu: 500m}
  worker:
    path: .
    args: ["rq", "worker"]

A component can also carry its own name, which is how a product made of a site, an admin panel and an API stays one app. Two ways to give it one — subdomain: claims a name on erawan.app, domain: points a name you own at a part:

components:
  site:
    path: web/dist
    domain: [example.com, www.example.com]
  admin:
    path: admin/dist
    subdomain: acme-admin          # acme-admin.erawan.app
  api:
    path: backend
    port: 8080
    public: true
  worker:
    path: backend
    args: ["rq", "worker"]

Four parts, four addresses: site on two names you own, admin on one of ours, api on the app's own .erawan.app, and worker on none — it is reached by the others and by nobody else, which is what a worker is for.

Nothing gets an address unless it says so

There is no automatic <app>-<component>.erawan.app — that name is not created for you. A part is reachable from outside only if it has public: true, a subdomain: or a domain:. A part with none of them is reachable only by the other parts, at http://<app>-<component> inside the cluster.

That is right for a worker and wrong for something people were visiting, so check erawan status after a deploy: a part that answers nowhere prints inside the app only. Somebody read a blank there as "not filled in yet", deleted the app that had been serving that traffic, and took their own front end down.

Tell us when a part is ready, or we guess

Add {C}healthcheck: /healthz to a part that answers HTTP. Without one we check that something is listening on its port, which is the right question for most things and the wrong one for an app that opens its socket and then spends a few seconds connecting to a database — that pod looks ready, gets traffic, and the request hangs.

It matters most during a deploy: the new part is added to the rotation as soon as it is ready and the old one is removed, so "ready" being early is a window where visitors wait on a process that is not answering yet. With a path, that window closes.

Pick a path your app actually implements. If it serves a single-page app, an unknown path returns index.html with a 200 — so a healthcheck pointing at one you never wrote passes forever, including while the part behind it is dead. A customer found this on their own deploy: curl /healthz answered 200 and the real endpoint was /health. erawan status prints a ready by column, and erawan deploy says so when the path answers HTML.

A claimed name is first-come, like an app name

subdomain: acme-admin claims acme-admin.erawan.app in the same flat namespace app names come from, so it is refused if anything already answers on it — and the deploy says so before it builds. There is no DNS to set up and no certificate to wait for: it is live as soon as the release is.

public: true still names exactly one part, and that part keeps the app's own address. A name is released when the app is deleted or when you take the line out of erawan.yaml and deploy again — so a name your users have is only as safe as the file that claims it.

A part that is not the public one can hold up to four: subdomain: [acme-admin, acme-console]. That is how a name written into somebody else's configuration — a webhook URL, an endpoint compiled into a mobile build — is retired without a cutover: claim both, repoint whoever holds the old one, then delete the line.

A component with no server costs nothing

It is served as files by the same shared server that serves an ordinary static site — no container, and nothing against your plan's memory. It gets the same single-page fallback, so a refresh on /settings returns index.html rather than a 404.

Such a component needs an address to be served on: domain:, or public: true. A deploy says so rather than leaving it unreachable.

  • args replaces the image's command, which is how two components share one directory and one image. Prefer it to start_command when the repository has its own Dockerfile: start_command only shapes a Dockerfile we generate, so with one in the tree it is read and then ignored.
  • Declare resources for every component. What you leave out claims your plan's whole per-app allowance, so four silent components ask for four times what one app is sold and the fourth is refused.
  • addons in the file are re-created on every deploy. Removing one with erawan addon rm and deploying again brings it back — take it out of the file instead.
  • public: true belongs to exactly one component — the one that answers on the app's own .erawan.app address. Names in domain: are as many as you like, on as many components as you like.
  • Declaring a domain: does not claim it. It says which component the name belongs to; whether it points at Erawan is still erawan domain add and the DNS check, so a deploy never fails because somebody has not finished editing their DNS. A name already on another app of the same account is moved by the deploy — no removal, no gap.

Components accept path, port, public, disk, image, user, domain, args, start_command, resources, secrets and addons.

Logins for your users

For an app with customers of its own. Erawan runs the sign-in, so your app never stores a password and never handles a reset. It is an addon like any other: ask for it and the app restarts with what it needs already set.

erawan addon add myapp auth

Your app is a normal OpenID Connect client. Six variables arrive in its environment and nothing else has to be configured:

VariableWhat it is
AUTH_ISSUER_URLThe issuer, for validating tokens.
AUTH_URLWhere to send somebody to sign in.
AUTH_CLIENT_IDThis app's client id.
AUTH_CLIENT_SECRETIts secret. Rotate with erawan auth rotate-secret.
AUTH_JWKS_URLThe signing keys.
AUTH_WELLKNOWN_URLDiscovery, if your library prefers it.

The three commands you will actually need

erawan auth callback add myapp http://localhost:3000/callback
erawan auth providers set myapp google,email
erawan auth users myapp

A redirect URI has to be allowed before it is used, including the one you develop against. Providers are google, email, github and line. The user list is masked by default — the addresses of your users are not something a terminal should print by accident.

The page your users see

Sign-in and consent are served by Erawan, and by default they carry your app's name and nothing of ours but one line saying which account is being used. Two things are yours to set:

erawan auth theme show myapp
erawan auth theme set myapp --color blue --name "Suea Thong Gym"

The colour is a name from a fixed list — show prints it — and not a hex value: the list is shadcn's, every entry chosen to carry white text, so there is no combination that produces a button nobody can read. It moves the primary button and the focus ring, and nothing else. The name is what appears above the form, up to 32 characters, and cannot mention Erawan: your users type an Erawan password on that page, so a name claiming to be us is the one thing it must not be able to say.

Put an app with a login on your own domain

Every app here answers on a subdomain of erawan.app, and a browser treats all of them as one site — so a neighbour's page is not cross-site to yours. A custom domain is its own site, which is where the boundary actually is. Name the session cookie __Host-something either way.

Environment and secrets

Environment is non-sensitive configuration: feature flags, modes, URLs and log levels. Credentials are app secrets. Declare their names at deploy and enter each value once on Erawan's trusted setup page; an agent never carries it.

erawan deploy . --name myapp --secret OPENAI_API_KEY
erawan env set myapp LOG_LEVEL=info
erawan env list myapp
erawan env unset myapp LOG_LEVEL

Secret values never pass through the deploying agent

A deploy that declares --secret pauses and returns secrets_url, the app's own page in the console. The owner enters the value there, or through hidden CLI input when it is already on their machine. The console, CLI and API expose names and status only; there is no reveal endpoint.

Do not put a secret in code, --env or chat

The pre-deploy check refuses a deploy carrying a credential in its files, and a .env baked into an image is one of the things it looks for. Declare only the name with --secret and let the owner complete trusted setup.

Plain websites have no environment at all — there is no process to hold one. A site that needs a key needs to be an app.

Your own domain

Attach a name you own. Erawan prints the DNS records to create, watches for them, then issues the certificate itself — there is nothing to upload and nothing to renew. Included on Hobby and up.

erawan domain add myapp app.example.com
erawan domain ls myapp
erawan domain rm myapp app.example.com

A subdomain gets one CNAME. An apex — example.com with no prefix — cannot be a CNAME at all, so that answer is an A record with the address in it, plus a CNAME for www. Some providers call the apex form ALIAS, ANAME or CNAME flattening; if yours offers one, it works as well.

erawan domain ls says what each domain is waiting for, which is usually DNS that has not propagated yet. The .erawan.app address keeps serving throughout, and keeps serving after you remove a custom domain.

Ready-made software

Some things you do not want to write. Point Erawan at the project's own repository and it goes through the same path your own code does — the pre-deploy check, the build, the history, the rollback.

erawan deploy https://github.com/owner/repository --name my-app

Erawan no longer keeps manifests of its own

Until 2026-08-11 a handful of projects had an entry here: a manifest we wrote, an image pinned by digest, and a line saying who had read it. Keeping that true costs a person per project per release, and it was already not being paid — most entries said unreviewed in their own review field. A promise nobody keeps is worse than no promise, so the entries are gone. erawan catalog and GET /v1/catalog still answer, and are empty while the replacement is built from what people actually deploy successfully.

Three things to read before you start

A build takes minutes. These take seconds, and they are the three ways deploying somebody else's project actually fails.

  1. The Dockerfile, if there is one. It decides everything else, because detection stops there. BuildKit-only syntax — COPY --link, a --chmod with letters in it, RUN --mount — cannot be built here, and it fails at that step, which may be twenty minutes in.
  2. What is actually in the repository. The build context is capped at 150MB compressed, and a documentation site, translations and checked-in binaries all count toward it. If the part that runs is one directory, deploy that directory.
  3. Whether the source is the image. A project's repository and the container it publishes are often different things: a monorepo builds dozens of packages and none of them the server you meant. If its README tells you to docker run something, use that image in a two-line Dockerfile rather than building the source.

Then find what it needs told to it — the README, a docker-compose.yml, an .env.example. A missing key is a container that starts, crashes and explains nothing.

For code you wrote yourself there is a cheaper check: erawan assess . runs the same gate a deploy runs and changes nothing. It reads a directory, so it cannot check a GitHub URL — for those, deploying is the check, and a failed deploy never replaces a running version.

Deployed something worth other people knowing about? erawan catalog request <name or URL> --why "…" — what people run here is what the list will be built from.

Apps only you can open

Some apps should not be on the open internet — an admin panel, a tool you run for yourself, software that has no sign-in of its own. Put one line in your erawan.yaml and the URL stops answering strangers.

version: 1
access: owner

Anyone opening the app is sent to Erawan's sign-in, and only the account that owns it is let through. Your app is not changed and gets no extra container: the check happens at the front door, before a request reaches it, and what arrives is an ordinary request from a person Erawan has already identified.

One path can stay open. A webhook, a health check or a payment callback is a machine with no session, and the door would send it to a sign-in page. Name the exceptions when you deploy — up to five, matched exactly:

erawan deploy . --access owner --public-path /callback

Exactly, not by prefix: /callback does not also open /callback-admin. / is refused — that is the whole app.

It is a door, not a disguise. The app behind it still has whatever permissions it always had, and anyone you share the account with can open it. Software with no sign-in of its own is single-user software; this makes it yours, and it does not separate one person's data from another's.

Jobs and schedules

One-off work — a database migration, a script — runs with your app's own code and settings, and you get the output back.

Repeating work is a task: say when, in cron form, and what to run. Nightly reports and clean-ups keep going after you close the laptop. Schedules are in UTC, and deploying a new version moves the tasks across with it.

Your command can see the app's disk

A job and a scheduled task run with the app's image, its settings and its disk, mounted at $DATA_DIR — so du -sh $DATA_DIR finally answers.

It is read-only unless you pass --write. The app is writing to that same volume while your command runs, and two writers on one filesystem is how a database file is ruined. Reading, counting and copying out need no write.

When a deploy fails

Failures come back with the reason and the end of the log, written so your agent can act on them without you. Usually it just fixes the code and deploys again.

  • The build failed. Something went wrong installing or compiling — the build log has the real error, usually a missing dependency.
  • It built, then stopped. The app exited or never served. Check that it binds 0.0.0.0 on the port you gave, and that the start command actually starts a server.
  • The check blocked it. A password or key was found in the code, or a .env was about to ship. Move those into settings instead of the code, then deploy again.
  • It was fine before. Roll back to the last version that worked, then look into it without the site being down.

Tools

What your agent can do once it is connected. You will rarely name these — ask in your own words and it picks.

ToolWhat it does
assess_appRun the pre-deploy check on its own.
deploy_appDeploy from an upload, a public GitHub URL, or a folder path on the control plane.
deploy_filesDeploy code sent inline, for agents with no files.
get_statusWhether it is running, which version, and the deploy history.
get_logsThe app's own logs, or the last build's log.
list_deploymentsEvery app with its status and address.
set_envAdd or change settings and restart.
stop_app / start_appTurn an app off without deleting it, and back on.
rollbackGo back to the previous working version.
delete_deploymentTake an app down and remove it.
provision_addon
remove_addon
Attach or detach Postgres, Redis, file storage, email.
run_jobRun one command with the app's code and settings.
schedule_task
list_tasks
delete_task
get_task_logs
Set up and inspect repeating work.
get_metrics
get_errors
Traffic, error rate, p95 and memory; and why a container that died died, with its last output.
get_rewind_window
rewind_database
How far back the database can go, and taking it there.
add_domain
list_domains
remove_domain
Point a domain you own at the app. The erawan.app address keeps answering.
change_planMove the account between plans. Where payments are configured, a plan is bought in the console — an agent cannot buy one.
list_catalogSoftware known to have run here. Empty today — deploy a repository by its own URL.

REST API

Everything the tools above do, an ordinary HTTP request does too — same account, same pre-deploy check, same history. It is what the erawan command talks to, and it is the path for CI, a script, or an agent that has curl and nothing else.

Base URL and credential

https://api.erawan.cloud/v1
Authorization: Bearer erw_at_…

Every credential here ends. There is no key that lives for ever — a secret nobody has to renew is one nobody notices — so an access token is good for an hour and refreshes itself, and a token you make by hand carries an expiry you chose. Four ways to hold one:

  • With no account at all. POST /v1/guest takes nothing and returns an access token, a refresh token and a keep_url. Websites only, and gone in 24 hours unless somebody opens that link.
  • From the CLI. erawan login signs in through a browser and keeps the tokens in ~/.erawan/cli.json.
  • From a machine with no browser — a container, an agent's sandbox, a server over SSH. erawan login --device prints a short code and a URL; you enter the code on any device that does have a browser, and the machine signs itself in. It is the default when the CLI can see there is no browser to open, so an agent usually needs no flag. Do not copy ~/.erawan/cli.json into a box instead: that file's refresh token does not expire and reaches every app in the account.
  • From a connected agent. An agent that connected over MCP holds its own token and can call these endpoints directly. Revoke it by name in the console.
  • A token you made on purposeerw_pat_…, for CI or for a machine you trust less than your laptop. erawan token create ci, or Settings in the console. It expires (90 days by default), it does only what you chose — deploy, admin, or both — and --app limits it to named apps. Revoke one without touching the others. A token cannot create another token: minting needs a person signed in.
    Hand it to a machine with ERAWAN_TOKEN, or with ERAWAN_TOKEN_FILE pointing at a file holding it — the file is read again before every request, so a token rotated by whoever wrote it is picked up without a restart. Either beats a stored login and neither is written to disk, so a box that is handed a token needs no credential file at all.

When an hour is up, exchange the refresh token at POST /token with grant_type=refresh_token and the client_id you were given — guest for a guest session. Refresh tokens are single use: each exchange returns the next one.

Installing a pinned CLI

Building the CLI into an image? Pin the version rather than following the latest, and verify it. https://erawan.cloud/dist/SHA256SUMS gives the digest of the wheel currently published, in the format sha256sum -c reads; /dist/latest names the file and /dist/ lists what exists.

# what is published right now, and its digest
curl -s https://erawan.cloud/dist/latest
curl -s https://erawan.cloud/dist/SHA256SUMS

# pin that filename in your image, and move it when you mean to
uv tool install --force "https://erawan.cloud/dist/<the filename above>"

Only the current release is kept under {C}/dist/ — an old wheel served from there is an old wheel somebody installs — so a pinned filename stops resolving when we publish the next one. That is the intended signal: it is a version you chose, and moving it is a decision rather than a side effect of rebuilding.

--force is the part that matters: uv tool upgrade cannot move an install pinned to a wheel URL — uv's receipt holds the old one — so it bumps dependencies and leaves the command missing. The installer verifies this checksum itself; against somebody who controls the site it proves nothing, since they would publish both, but it catches a truncated download, a stale cache, and a wheel that moved under a version you pinned.

A deploy from nothing, in four calls

TOKEN=$(curl -sX POST https://api.erawan.cloud/v1/guest | jq -r .access_token)

curl -sX POST https://api.erawan.cloud/v1/apps \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"hello","files":{"index.html":"<h1>hello</h1>"}}'

curl -s https://api.erawan.cloud/v1/apps/hello -H "Authorization: Bearer $TOKEN"

curl -s "https://api.erawan.cloud/v1/apps/hello/logs?kind=build" \
  -H "Authorization: Bearer $TOKEN"

Deploys are asynchronous

POST /v1/apps answers 202 with a Location header and does the work in the background. Poll GET /v1/apps/{name} until status is running or failed; a failure carries last_error, and the build log has the rest. A failed deploy never replaces the version that is already running.

Deploying

POST /v1/apps takes a name and exactly one source. Re-posting the same name makes a new version and keeps the old one for rollback.

FieldWhat it is
filesText files inline, {"path": "contents"} — for a caller with no filesystem.
upload_idFrom POST /v1/uploads: a gzipped tar of the project.
github_urlA public repository, optionally with github_ref and github_subdirectory.
catalogAn entry by name. Empty today — see Ready-made software.
pathA directory on the control plane — development servers only, refused in production.
port · start_command · envOptional. Port defaults to 8000; the runtime is detected if no start command is given.

Endpoints

EndpointWhat it does
POST /v1/guestA session with no account. Unauthenticated, rate-limited per address.
GET /v1/me
PUT /v1/me/plan
Who this token belongs to, and keep_url while the account is still a guest; move the account between plans. Where payments are configured a plan is bought in the console, not here.
GET /v1/apps
POST /v1/apps
Every app with its status, address and parts — the live release's parts, several for an erawan.yaml with more than one; start a deploy (202).
POST /v1/uploadsGzipped tar in the body → an upload_id.
POST /v1/assessRun the pre-deploy check without deploying.
GET /v1/apps/{name}
DELETE /v1/apps/{name}
Status, version, URL and deploy history; take the app down.
POST /v1/apps/{name}/restart
POST /v1/apps/{name}/rollback
Restart it; or go back to the previous version.
POST /v1/apps/{name}/stop
POST /v1/apps/{name}/start
Turn the app off and on. Nothing is deleted — the disk, the database, the settings and every version stay, and its address answers a page saying it is stopped. Stopping frees the memory for another app. Both are idempotent.
GET /v1/apps/{name}/exportThe running version's code as a .tar.gz?version= for an earlier one. erawan pull <app> is this.
GET /v1/apps/{name}/logs?kind=runtime|build, ?tail=, ?component= for one part of a multi-part app — left out, the public part answers and the reply names it.
GET /v1/apps/{name}/metrics
GET /v1/apps/{name}/errors
Traffic, error rate, p95, memory; and why a container died, with its last output. ?window=1h|6h|24h|7d|30d.
GET /v1/apps/{name}/env
PUT /v1/apps/{name}/env
Variable names only — values are encrypted at rest and never read back. A null value removes one; anything unmentioned is left alone.
GET /v1/apps/{name}/secrets
POST /v1/apps/{name}/secret-requests
Secret names, status and bindings; or one app-level trusted setup request. Values never appear in a GET or agent deploy call.
POST /v1/apps/{name}/addons
DELETE /v1/apps/{name}/addons/{type}
postgres, redis, minio, email. Connection settings are injected and the app restarts.
POST /v1/apps/{name}/jobsOne command with the app's own image and settings. Waits for the result.
POST /v1/apps/{name}/renameChange an app's name. Its disk, add-ons and URL are untouched — the name is what commands take, the address is what people visit.
POST /v1/apps/{name}/db/importRestore a gzipped pg_dump into the app's postgres addon. The request body is the file. Refused if the database already has tables unless ?force=true.
GET /v1/apps/{name}/rewind
POST /v1/apps/{name}/rewind
What the database can be rewound to, and rewinding it. Destructive — the state being replaced is saved first.
GET /v1/apps/{name}/domains
POST /v1/apps/{name}/domains
DELETE /v1/apps/{name}/domains/{domain}
A domain you own, pointed at the app (202 — poll the GET). The erawan.app address keeps answering either way.
GET /v1/apps/{name}/tasks
POST /v1/apps/{name}/tasks
DELETE /v1/apps/{name}/tasks/{task}
GET /v1/apps/{name}/tasks/{task}/logs
Scheduled work, in cron form and in UTC.
GET /v1/catalog
GET /v1/catalog/{name}
POST /v1/catalog/requests
The list of known software (empty for now) and asking for an addition. The two reads need no credential.
POST /v1/ask
GET /v1/ask
GET /v1/ask/{ref}
Ask a question, request a feature, or say what is wrong — a person answers. ?mode=sync (the default) holds the connection for a while and returns 200 with the answer; otherwise 202 with a Location to poll. Needs an account: deploying does not, being answered does. Erawan answers some questions itself in seconds, from its own documentation and your account's state, and the reply says answered_by: erawan; send "human": true to wait for a person and keep the question away from a model provider.

Handing a secret to an app without an agent ever seeing its value has its own endpoints under /v1/secret-actions and /v1/apps/{name}/secret-requests. They are bound to a recent human approval, so they are worth reading in the description below before using them.

When a call fails

Every failure is JSON with an error and usually a hint written for whoever has to act on it.

StatusMeaning
202Accepted, and running in the background. Poll the Location.
400The request was wrong — or the pre-deploy check refused the code. That one carries a findings list: fix what it names and deploy again under the same name.
401No credential, or one that has expired or been revoked. Refresh it.
404No app by that name on this account.
409 · 410A one-time secret action was replayed, or had already expired.
413 · 429The upload was too large; or too many uploads, guest sessions or deploys too quickly.

The full description, generated from the server itself and never written by hand:

https://api.erawan.cloud/v1/openapi.json

/v1 only ever gains things. Fields and endpoints may appear; nothing that is already here changes shape or disappears without a /v2.

For agents

Reading this as an agent? The deploy playbook is one page, written for you:

https://erawan.cloud/llms.txt

Fetch it once before your first deploy. It picks between a shell, an MCP connection and having neither, and covers guest sessions, the pre-deploy check, and what to do when a build fails. https://erawan.cloud/llms.txt is the map of every surface here — it links to that page rather than repeating it.