{"name":"Antideploy API","version":"v1","base":"https://antideploy.com","start":{"summary":"Log in once with the device flow, create an application, give it a database if the project needs one, push the project. Antideploy is where the project is built as well as where it ships: steps 2 and 3 happen while the code is still being written, and a later deploy reuses what they made. Steps 1 to 3 are skipped on every later deploy of the same project.","steps":[{"step":1,"what":"Get an account token, if ~/.antideploy/config.json does not already have one.","how":"POST https://antideploy.com/api/v1/device/code, show the user the verificationUriComplete it returns, then poll POST https://antideploy.com/api/v1/device/token with the deviceCode until it stops answering authorization_pending.","humanAction":"The user opens the link and clicks Approve. This is the only thing a human does in the entire flow. If they are signed out or have no account yet, the same link takes them through Google or GitHub, which creates the account, and back to Approve: do not ask whether they have one.","critical":"Write the token to a file with a redirect. Never echo it, never paste it into a chat, never put it in the project directory — it is an account credential, not a project key.","then":"The same response names the account in `account`. Tell the user which one they connected (`account.label`): many people have two, and one email can be on both a GitHub account and a Google one. If it is the wrong account, delete the token and log in again while signed in to the right one."},{"step":2,"what":"Create the application, if the project has no .antideploy.json yet.","how":"POST https://antideploy.com/api/v1/applications with {\"name\":\"<folder-name>\"}. Save the returned applicationId to .antideploy.json in the project. That file holds no secret and can be committed.","note":"409 name_taken means you already have it — the response carries the existing applicationId, so use that."},{"step":3,"what":"If the project needs a database and the user has not chosen their own, create it now and develop against it.","how":"POST https://antideploy.com/api/v1/database?applicationId=<id>, then write its variables into the project's .env: curl -fsS -H \"authorization: Bearer $TOKEN\" \"https://antideploy.com/api/v1/database/env?applicationId=<id>\" >> .env. The answer is plain text meant for a file, so the password never reaches the terminal.","note":"Skip this if the project has no database or already points at one of the user's own (a DATABASE_URL in .env that is not localhost, or a provider they named). Do not push DATABASE_URL or the PG* variables to /api/v1/secrets: the platform sets them itself and the deployed app uses this same database."},{"step":4,"what":"Push the project and watch it build.","how":"tar the directory and POST it to https://antideploy.com/api/v1/deploy?applicationId=<id>, then poll the watch URL until status is succeeded or failed.","report":"When it succeeds, tell the user where it is live and which Antideploy account it is on. The watch response's `summary` says both in one sentence, for example 'my-app is live at https://my-app.antideploy.app, on the Antideploy account you@example.com (signs in with GitHub).' A URL alone does not tell someone with more than one account where to find the project."}],"example":["# 1. login (once ever)","curl -sS -X POST https://antideploy.com/api/v1/device/code -H 'content-type: application/json' -d '{\"clientName\":\"Claude Code\"}' -o /tmp/ad-login.json","#    show the user .verificationUriComplete, then poll:","curl -sS -X POST https://antideploy.com/api/v1/device/token -H 'content-type: application/json' -d \"{\\\"deviceCode\\\":\\\"$DEVICE_CODE\\\"}\"","#    on success, write .token straight to ~/.antideploy/config.json - do not print it","","# 2. create the project (once per project)","curl -sS -X POST https://antideploy.com/api/v1/applications -H \"authorization: Bearer $TOKEN\" -H 'content-type: application/json' -d '{\"name\":\"my-app\"}'","","# 3. a database, if the project needs one (once per project, any time before the first deploy)","curl -sS -X POST \"https://antideploy.com/api/v1/database?applicationId=$APP_ID\" -H \"authorization: Bearer $TOKEN\"","curl -fsS \"https://antideploy.com/api/v1/database/env?applicationId=$APP_ID\" -H \"authorization: Bearer $TOKEN\" >> .env   # the answer holds a password: it goes to the file and nowhere else","","# 4. deploy (every time)","tar czf - --exclude=.git --exclude=node_modules . | curl -sS -X POST \"https://antideploy.com/api/v1/deploy?applicationId=$APP_ID\" -H \"authorization: Bearer $TOKEN\" -F \"archive=@-\""]},"authentication":{"scheme":"Authorization: Bearer <credential>","kinds":{"account":{"prefix":"adu_","obtainedBy":"The device flow. See `start` and `login`.","can":["create applications","deploy, set secrets and poll status on any application you own","mint and revoke project keys","create AI keys, and read the application's database, its rows included","create an application's database before its first deploy, and write its connection variables to a file"],"cannot":["read secret values back","delete an application","mint another account token"],"storage":"~/.antideploy/config.json, mode 0600. Never in the project directory — this one is not safe to commit.","revoke":"Immediately, from Account tokens in the dashboard.","note":"Requests must name the application: `?applicationId=<id>`."},"project":{"prefix":"ad_","obtainedBy":"POST /api/v1/keys with an account token, or the dashboard.","scope":"Exactly one application, decided by the key itself — so no applicationId is sent, and one that leaks costs you that project rather than the account.","note":"Built to survive being committed. Use this one for CI.","cannot":["mint project keys","read rows from the application's database","read the database's connection string (it can create the database)"]}}},"login":{"begin":{"method":"POST","path":"/api/v1/device/code","body":"{\"clientName\":\"Claude Code\"}","returns":"deviceCode (poll with this), userCode (show this), verificationUriComplete (open this), expiresIn, interval"},"poll":{"method":"POST","path":"/api/v1/device/token","body":"{\"deviceCode\":\"...\"}","interval":5,"returns":"200 {token} once approved.","errors":"400 with the machine code in `error` (RFC 8628) and a sentence in `error_description`. authorization_pending — keep polling. access_denied — the user refused. expired_token — start again. `code` carries the same value, for clients written against the older shape.","singleUse":"A poll that succeeds is the redemption. Capture the token from that response or the code is spent for nothing."},"handling":"Redirect the granted token into a file. It must not reach terminal output or an agent transcript."},"applications":{"create":{"method":"POST","path":"/api/v1/applications","body":"{\"name\":\"my-app\",\"subdomain\":\"my-app\"}","auth":"Account token only.","returns":"201 {applicationId, name, subdomain, dashboard}","conflicts":"409 name_taken, carrying the existing applicationId.","subdomain":{"optional":true,"description":"The address the app is published at: <subdomain>.antideploy.app. Omit it and one is derived from the name, numbering past a collision.","ask":"ASK YOUR USER what they want this to be, and show them the full URL before you send it. It is the address they will share, and picking it for them is picking their public name for them.","rules":"2 to 40 characters, lowercase letters, digits and hyphens, no leading, trailing or doubled hyphen. Some words are reserved.","check":"GET /api/v1/hostnames/check?label=<label> before creating, to avoid a wasted 409.","conflicts":"409 subdomain_taken if somebody already has it, 400 invalid_subdomain if it breaks a rule. Neither is retryable with the same value: ask your user for another.","change":"PATCH /api/v1/applications/<applicationId> with {\"subdomain\":\"new-name\"}. The old address stops working immediately."}},"list":{"method":"GET","path":"/api/v1/applications","description":"Everything you own. A project key sees only its own, which makes this safe to call with either credential."},"read":{"method":"GET","path":"/api/v1/applications/{applicationId}","description":"Its address, source, repository and branch, deployOnPush and rootDirectory."},"update":{"method":"PATCH","path":"/api/v1/applications/{applicationId}","body":"{\"subdomain\":\"new-name\",\"deployOnPush\":false,\"rootDirectory\":\"services/api\"}  // any one or more","fields":{"subdomain":"The address. See `create.subdomain`; the old one stops working at once.","deployOnPush":"GitHub applications only (400 not_a_repository otherwise): whether a push to the default branch deploys by itself. Off, pushes wait for POST /api/v1/redeploy with latest, which saves a free plan's deploys for when they are wanted.","rootDirectory":"The directory inside the project that holds this application, or null for the whole project. The project is analyzed again at once and `analysis` says what was found there. The running app changes on the next deploy."},"errors":"400 bad_request | not_a_repository | invalid_subdomain, 409 subdomain_taken, 422 analysis_failed: the directory is saved but nothing deployable was found in it; the body says why.","returns":"The settings as they now stand, plus `changed`."}},"deploy":{"method":"POST","path":"/api/v1/deploy","query":{"applicationId":"Required for account tokens, optional for project keys. In the query string rather than the form so a 404 or a 409 costs a header instead of a finished upload."},"encoding":"multipart/form-data","preferred":{"field":"archive","description":"A tar of the project directory, optionally gzipped. Carries the directory tree itself, so nothing can drift out of sync.","example":"tar czf - --exclude=.git --exclude=node_modules . | curl -X POST \"https://antideploy.com/api/v1/deploy?applicationId=$APP_ID\" -H \"Authorization: Bearer $KEY\" -F \"archive=@-\""},"alternative":{"fields":["files","paths"],"description":"One `files` part and one `paths` part per file, in the same order. Send every file in the project, not just the entry point. Mismatched counts are rejected rather than silently flattened."},"optional":{"env":"JSON object or .env-formatted string of environment variables. Write-only; never readable back.","force":"\"true\" to rebuild even when the content hash is unchanged."},"responses":{"200":"{status:\"unchanged\"} — identical content hash to the last push, nothing built.","202":"Queued. Returns taskId, contentHash, fileCount, files (the manifest, first 50), `account` (which Antideploy account it went to) and a `watch` URL.","400":"empty | bad_request | bad_archive | paths_mismatch | too_large | application_required. A `too_large` from the per-file ceiling also carries `files: [{path, bytes}]` naming every file over it.","401":"unauthorized — unknown or revoked credential.","402":"live_app_limit | deploy_month_limit: the account is at its plan's limit. Not a rate limit, so do not retry. Say what the message says and let the user choose between deleting something, waiting for the 1st, or upgrading.","403":"account_suspended: the account is suspended. Waiting does not clear it; do not retry.","404":"not_found — no such application, or not yours.","409":"deploy_in_progress — one deploy at a time per application.","429":"rate_limited","503":"hosting_paused | uploads_paused: deploys, or storing pushed folders, are paused platform-wide for a while. Retry after retryAfterSeconds; nothing about the project needs changing."}},"poll":{"method":"GET","path":"/api/v1/deployments/{taskId or deploymentId}","description":"Status, per-step progress, the analyzed spec, warnings and hazards. Poll until status is `succeeded` or `failed`. `account` names the Antideploy account the application belongs to, and once it is live `summary` is the sentence to tell the user: the URL and that account.","failure":"On a failure read `error` (the explanation), `failedStep` (where it stopped), `deployment.buildLog` (the end of the build output) and `deployment.runtimeLog` (what the app printed while we tried to start it). Fix the cause before redeploying: an unchanged redeploy fails the same way.","changes":"Once finished, `changes` says what differs from the last version that went live: `message` (the commit), `code` (`same`, `files` for a changed folder, or `commits` with counts and a GitHub `compareUrl`) and `env` (keys added, changed or removed, never values). After a failure, look here first. An empty `env` means no change was recorded, not that none happened.","ids":"A push returns a task id. The dashboard, the failure email and the Fix it prompt give a deployment id. Either works here.","security":"Every deploy that goes live is checked for security issues automatically, right after, without holding the deploy up. The result is in `security`: when its status is `scanning`, call this endpoint once more a few seconds later. When `security.issues` is not empty, tell the user and offer to fix them (each has a `recommendation`); redeploying checks again. `security.message` says what to do in one sentence."},"redeploy":{"method":"POST","path":"/api/v1/redeploy","body":"{\"applicationId\":\"<id>\"}  // add \"latest\": true for a GitHub application's newest commit","description":"Builds and releases the application again without its code being sent. By default the version last sent or analyzed, with the current variables: this is how a changed secret or a new AI key reaches the running app. With `latest`, a GitHub application deploys the newest commit on its default branch, analyzed first, exactly as a push would. Watch it like a deploy.","responses":{"200":"{status:\"unchanged\"}: with `latest`, that commit is already live or on its way. Leave `latest` out to build it again.","202":"Queued. Returns taskId, the commitSha being built and a `watch` URL.","400":"not_a_repository (`latest` on an application whose code is sent to us) | not_deployable (the code itself cannot deploy; redeploying it fails the same way)","403":"dockerfile_held | account_suspended","409":"deploy_in_progress | nothing_to_redeploy | repository_missing | repository_unreadable","402, 429, 503":"As for POST /api/v1/deploy."},"note":"A project key may omit applicationId."},"secrets":{"write":{"method":"PUT","path":"/api/v1/secrets"},"list":{"method":"GET","path":"/api/v1/secrets","description":"Key names only. Values are never returned, by design."},"remove":{"method":"DELETE","path":"/api/v1/secrets?key=NAME","description":"Removes one variable. The running app keeps it until the next deploy: POST /api/v1/redeploy applies it. 404 not_set if it was not set. Variables the platform supplies, such as a provisioned database's, cannot be removed."},"ask":{"method":"POST","path":"/api/v1/secrets/request","body":"{ \"names\": [\"STRIPE_SECRET_KEY\", \"STRIPE_WEBHOOK_SECRET\"] }","description":"A link where the user pastes values the project needs, so they never go through the chat. Returns `url` to show them, `alreadySet`, and how to poll. Creates nothing: the page writes each value into the application's encrypted variables, and its name then appears in GET /api/v1/secrets. At most 10 names; PORT and the values the platform fills in itself (the app's own address, generated signing secrets) are refused with 400 platform_value.","why":"Never ask a person to paste a key into the conversation: it stays in a transcript. This keeps it between them and the application."},"note":"Account tokens must pass ?applicationId=<id>."},"connections":{"note":"Account token only: a connection belongs to the person, not to one project. Values are never returned by any of these.","supabase":{"status":{"method":"GET","path":"/api/v1/connections/supabase","description":"{connected:false, connectUrl} until the user approves at connectUrl (one click on Supabase's own screen); then {connected:true, organizations, projects:[{ref, name, organization, region, status, paused, url}]}. `withdrawn` means they took the grant back on Supabase's side and must connect again."},"write":{"method":"POST","path":"/api/v1/connections/supabase/env?applicationId=<id>","body":"{ \"projectRef\": \"<20-letter ref>\", \"env\": { \"NEXT_PUBLIC_SUPABASE_URL\": \"url\", \"NEXT_PUBLIC_SUPABASE_ANON_KEY\": \"anon_key\", \"SUPABASE_SERVICE_ROLE_KEY\": \"service_role_key\" } }","description":"Writes the project's values into the application's variables under the names given. Kinds: url, anon_key, service_role_key, publishable_key, secret_key. Applied on the next deploy.","responses":{"200":"{written, project, summary}. No values.","400":"invalid_project_ref | env_required | invalid_name | invalid_kind | platform_value | too_many | server_key_in_browser (a service_role_key or secret_key under a NEXT_PUBLIC_, VITE_ or similar name)","404":"project_not_found: not in the organizations the user connected","409":"not_connected (with connectUrl) | project_paused (Supabase lists no keys for a paused project; the user restores it on Supabase) | key_not_available (with `missing` and `available`: newer projects have publishable and secret keys instead of anon and service_role)","503":"supabase_unavailable: connecting Supabase is not switched on here; ask with POST /api/v1/secrets/request instead"}},"localEnv":{"method":"GET","path":"/api/v1/connections/supabase/env?projectRef=<ref>&env=NEXT_PUBLIC_SUPABASE_URL:url,NEXT_PUBLIC_SUPABASE_ANON_KEY:anon_key","description":"The same values as a .env body, for a redirect into the project's .env while it is built (curl -fsS ... >> .env). Never print it. Errors are JSON with a 4xx or 5xx, so curl -f writes nothing."},"api":{"method":"POST","path":"/api/v1/connections/supabase/api","body":"{ \"method\": \"POST\", \"path\": \"/v1/projects/<ref>/database/migrations\", \"body\": { \"name\": \"create_todos\", \"query\": \"create table todos (...)\" } }","description":"Any call to Supabase's Management API (https://supabase.com/docs/reference/api) with the user's connection: tables and migrations, read-only queries, sign-in settings (config/auth), Edge Functions and their secrets, restoring a paused project, logs. Answers {status, body} with Supabase's status; keys, passwords and secrets in the body are hidden.","responses":{"202":"needs_approval: { approval: { id, url, reason, expiresAt } }. Waits for the user: SQL that drops a table or column, truncates, deletes or updates without WHERE, disables Row Level Security or uses EXECUTE; creating a project; restoring a backup; key, password, network and database-setting changes; anything not on the list of plain changes. Show the url, then poll actions/{id}.","403":"not_through_antideploy: deleting a project, billing and add-ons, organizations, branches, custom domains, database access grants. The user does these in Supabase.","409":"not_connected (with connectUrl)","413":"too_large: request over 256 KB"}},"actions":{"method":"GET","path":"/api/v1/connections/supabase/actions/{id}","description":"A call that waited for the user: pending | running | done | failed | refused | expired, with Supabase's answer in `result` once it ran."}}},"inspect":{"note":"All of these take ?applicationId=<id> with an account token. A project key is already scoped to one application and may omit it.","metrics":{"method":"GET","path":"/api/v1/metrics","query":{"days":"Default 7, maximum 30."},"description":"Requests, 5xx errors and their rate, p50 and p95 response times in milliseconds, and requests per day, oldest first.","why":"Read it when the user says the site is slow or failing: logs show what happened, this shows how often. Requests count everything that reached the app, bots and each asset on a page included, so they are not visitors."},"history":{"method":"GET","path":"/api/v1/deployments","query":{"limit":"Default 10, maximum 50."},"description":"Recent deploys for this application, newest first: status, commit, url, and the full failure sentence in `error`.","why":"Read this before redeploying after a failure. The most common outcome on this platform is a build that succeeds and an app that never listens on its port, and the sentence in `error` names that, redeploying unchanged reproduces it exactly."},"logs":{"method":"GET","path":"/api/v1/logs","query":{"limit":"Default 100, maximum 300."},"description":"What the running container has printed, newest last.","note":"`setupNeeded` in the response means our own log access is misconfigured, not that the application is broken. Do not act on it as a deploy failure."},"health":{"method":"GET","path":"/api/v1/health","description":"Last observed state: healthy, degraded, down or unknown, with `checkedAt`.","why":"A reading from a sweep that runs every five minutes, not a probe taken now. Compare `checkedAt` against your deploy before concluding anything from it."}},"security":{"note":"Scan one of your apps for common vulnerabilities. Free. An account token passes ?applicationId=<id> or sends it in the body; a project key is already scoped to its app and may omit it.","automatic":"Every live deploy already gets a lighter check that only reads what the app shows the public (it never calls the app's endpoints); see `security` in the deploy status. A scan here is the full one, which also tests endpoints for data open without a login and SQL injection.","scan":{"method":"POST","path":"/api/v1/security/scans","body":"{ \"applicationId\": \"<id>\" }","description":"Start a scan of the application's live URL. Returns `scanId` and a `watch` URL. It runs in seconds: keys shipped to the browser, endpoints open without a login, SQL injection, downloadable config files, open CORS and missing security headers."},"poll":{"method":"GET","path":"/api/v1/security/scans/{id}","description":"Status, per-step progress, and each finding with a severity, a plain-language explanation and how to fix it. Poll until status is `completed` or `failed`.","fix":"Every finding carries a `recommendation`. Fix each at its root, then scan again to confirm it's gone."}},"services":{"note":"Antideploy keeps a list of services: some work today, some are planned, some are only ideas to find out whether anybody wants them. Nobody is expected to know every name. Check it before you pick a service for a project, and tell us when the project needs something that is not on it.","list":{"method":"GET","path":"/api/v1/services?applicationId=<id>&need=<plain words or a vendor name>&status=<live|planned|idea|recipe|not_offered>","description":"Every entry, grouped, or just the ones a word or a vendor points at (`need=sentry`, `need=send text messages`). With an applicationId it also says what the project's own code already reaches for (`project.reaches_for`) and a few things worth asking the person about (`project.worthAsking`). Each entry has a `status`: `live` works now and `use` says how; anything else is not offered yet and `meanwhile` says what to do."},"request":{"method":"POST","path":"/api/v1/services/requests","body":"{ \"need\": \"send SMS reminders in India\", \"vendor\": \"MSG91\", \"reason\": \"bookings need a reminder\", \"applicationId\": \"<id>\", \"blocking\": false, \"recommendedByAgent\": true, \"confirmedByUser\": false }","description":"Tell us the project needs something, whether or not it is on the list. Only `need` is required; name the vendor if you have one in mind. Set `confirmedByUser` to true only if a person said they want it. The answer is never an error: it says whether the thing is listed, what to do meanwhile, and that we will email the account if we offer it. 429 only past 50 requests a day from one account."},"afterDeploy":"A live deploy's status carries `services`: what the project already reaches for, up to five things worth asking, and how to report. Ask the person once, in their words, say which you would add and why, and send what they answer. Do not choose for them and do not stop what you are doing."},"included":{"summary":"These are created and wired into the application's environment by the platform. Do not send your user to another provider for them, and do not ask them to paste a connection string.","database":{"what":"Postgres. Created when you ask for it, which should be while the project is still being written, or by the platform on the first deploy of a project that needs one.","variables":"DATABASE_URL, plus the PG* set derived from it.","create":{"method":"POST","path":"/api/v1/database","body":"{\"applicationId\":\"<id>\"}  // or ?applicationId=<id>; a project key may omit it","auth":"Account token or project key.","description":"Creates the application's database now, before its first deploy, so the project is written and tested against the real thing instead of SQLite or mock data. Idempotent: a second call answers 200 with the same database. The deploy that follows reuses it, so what you tested is what goes live.","returns":"201 (200 when it already existed) with `connection` (masked), `variables` (their names), `account` and `summary`. The connection string is not in the answer; see `env`.","errors":{"402":"database_limit: the plan's allowance is used (see `limits.databases`). Not a rate limit, so do not retry. Say what the message says and let the user choose.","409":"own_database (the project already connects to a database of its own, so none is made) | database_in_progress (another request is creating it, retry after `retryAfterSeconds`)","429":"rate_limited: the last attempt just failed, retry after `retryAfterSeconds`","502":"provider_error: nothing was created, try again in a minute","503":"database_unavailable"},"ifBlocked":"If your environment refuses this call (a permission prompt, or an automatic refusal that gives no reason), do not look for another route or another credential. Tell the user and wait. They can approve it, or press Create database on the application's page in the console (the `dashboard` link that creating the application returned) while nothing has been pushed. Then carry on from `env`, which only reads.","startsSmall":"A database made this way starts at 0.25 CU and falls asleep after a minute idle, so one that never ships costs little. It is not capped there for good: it scales to 1 CU, the same ceiling as any other."},"env":{"method":"GET","path":"/api/v1/database/env?applicationId=<id>","auth":"Account token only. A project key gets 403 account_token_required.","returns":"text/plain, a .env body: DATABASE_URL and the PG* variables, the same set the deployed app receives. Not JSON, so it can be redirected into a file.","use":"curl -fsS -H \"authorization: Bearer $TOKEN\" \"https://antideploy.com/api/v1/database/env?applicationId=<id>\" >> .env. Keep -f so an error writes nothing into the file, and never print the answer: it holds a password. Make sure .env is in .gitignore.","errors":"404 no_database | 409 own_database | 403 account_token_required"},"development":{"sameDatabase":"The database you develop against is the one the deployed app uses. Before the first deploy it is a scratch space. Once the app is live its data is real: do not reset, truncate, drop or reseed it from a laptop without asking the user.","pooled":"The connection is Neon's pooled endpoint, the same one the deployed app uses. A migration tool that needs a direct connection (Prisma Migrate can) works with the host written without `-pooler`, for the local command only.","dontPush":"Do not copy DATABASE_URL or the PG* variables from .env into /api/v1/secrets. The platform sets them, and a copy would only look like a database the user brought themselves.","backups":"A database whose application has never gone live is not part of the daily backup. Do not promise one."},"read":"GET /api/v1/database: state, size, tables, and a masked connection string.","rows":"GET /api/v1/database/rows?table=<name>&limit=20&offset=0: up to 50 rows at a time, read-only, with an account token only (a project key gets 403 account_token_required). Columns named like credentials come back as \"[hidden]\" and `hidden` names them. The rows are the app's users' data: read what the task needs and do not paste them into a chat.","code":"Read DATABASE_URL from the environment, in development and in the deployed app alike. The running container already has it; the JSON endpoints return it masked, and `env` is the one way to get it into a file.","byo":"A value the user set themselves wins over ours, so a project pointed at Supabase or its own Neon keeps working. GET /api/v1/database reports which of the two it is.","migrations":"Run them from the app itself on boot, or as a one-off command in the project. There is no separate migration endpoint."},"storage":{"what":"A private S3 bucket, with a key that reaches that bucket and nothing else. Created when you ask for it, which should be while the project is still being written, or by the platform on the first deploy of a project that reads an S3 client and brought no key of its own.","variables":"AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION, BUCKET_NAME and the S3_* and AWS_S3_* spellings of the same, plus AWS_S3_FORCE_PATH_STYLE.","create":{"method":"POST","path":"/api/v1/storage","body":"{\"applicationId\":\"<id>\"}  // or ?applicationId=<id>; a project key may omit it","auth":"Account token or project key.","description":"Creates the application's bucket now, before its first deploy, so uploads are written and tested against the real thing instead of the container's disk, which every deploy erases. Idempotent: a second call answers 200 with the same bucket. The deploy that follows gives the running app the same variables.","returns":"201 (200 when it already existed) with `bucket`, `endpoint`, `region`, `variables` (their names), `account` and `summary`. The key is not in the answer; see `env`.","errors":{"402":"storage_limit: the plan's allowance is used (see `limits.buckets`). Not a rate limit, so do not retry. Say what the message says and let the user choose.","409":"own_storage (the project already reaches an object store with a key of its own, so none is made) | storage_in_progress (another request is creating it, retry after `retryAfterSeconds`)","429":"rate_limited: the last attempt just failed, retry after `retryAfterSeconds`","502":"provider_error: nothing was created, try again in a minute","503":"storage_unavailable"}},"env":{"method":"GET","path":"/api/v1/storage/env?applicationId=<id>","auth":"Account token only. A project key gets 403 account_token_required.","returns":"text/plain, a .env body with the variables above, the same set the deployed app receives. Not JSON, so it can be redirected into a file.","use":"curl -fsS -H \"authorization: Bearer $TOKEN\" \"https://antideploy.com/api/v1/storage/env?applicationId=<id>\" >> .env. Keep -f so an error writes nothing into the file, and never print the answer: it holds a secret key. Make sure .env is in .gitignore.","errors":"404 no_storage | 409 own_storage | 403 account_token_required"},"read":"GET /api/v1/storage: whether the application has a bucket, and where it is. Never the key.","usage":"Any S3 client. Addressing is path style, which current AWS SDKs and boto3 choose by themselves for this bucket's name (set forcePathStyle: true in new JavaScript code anyway). They read the AWS_* variables from the environment, so code that sets nothing else already works.","browsers":"Browsers on any origin may read and write the bucket, but it is private: every request still needs the key or a link signed with it. Have the server hand the browser a presigned PUT URL.","dontPush":"Do not copy these variables into /api/v1/secrets: the platform sets them, and a copy would shadow the real ones if the bucket is ever made again.","byo":"A key the user set themselves wins over ours, and a project that sets one is left alone: 409 own_storage."},"auth":{"what":"Sign-in for the application's own users: Neon Auth, which is Better Auth run by Neon, switched on for the application's own Postgres. The people who sign up are rows in a `neon_auth` schema of that database, so the application's tables can join on them. Created when you ask for it, which should be while the project is still being written.","variables":"NEON_AUTH_BASE_URL, NEON_AUTH_COOKIE_SECRET (the secret that signs session cookies, different for every application), NEON_AUTH_JWKS_URL and VITE_NEON_AUTH_URL.","create":{"method":"POST","path":"/api/v1/auth","body":"{\"applicationId\":\"<id>\"}  // or ?applicationId=<id>; a project key may omit it","auth":"Account token or project key.","description":"Switches on sign-in for the application now, before its first deploy, so a login page is written and tested against the real thing. If the application has no database of ours yet one is made, and it counts against the plan's databases. Idempotent: a second call answers 200 with the same service. The deploy that follows gives the running app the same variables.","returns":"201 (200 when it already existed) with `baseUrl`, `jwksUrl`, `trustedOrigins`, `variables` (their names), `databaseCreated`, `use`, `account` and `summary`. The cookie secret is not in the answer; see `env`.","errors":{"402":"database_limit: a database had to be made and the plan's allowance is used (see `limits.databases`). Not a rate limit, so do not retry. Say what the message says and let the user choose.","409":"own_auth (the project already runs a Neon Auth of its own) | own_database (it connects to a database of its own, which cannot hold the users) | database_in_progress or auth_in_progress (another request is at work, retry after `retryAfterSeconds`)","429":"rate_limited: the last attempt just failed, retry after `retryAfterSeconds`","502":"provider_error: nothing was created, try again in a minute","503":"database_unavailable"}},"env":{"method":"GET","path":"/api/v1/auth/env?applicationId=<id>","auth":"Account token only. A project key gets 403 account_token_required.","returns":"text/plain, a .env body with the variables above, the same set the deployed app receives. Not JSON, so it can be redirected into a file.","use":"curl -fsS -H \"authorization: Bearer $TOKEN\" \"https://antideploy.com/api/v1/auth/env?applicationId=<id>\" >> .env. Keep -f so an error writes nothing into the file, and never print the answer: it holds the secret that signs session cookies. Make sure .env is in .gitignore.","errors":"404 no_auth | 409 own_auth | 403 account_token_required"},"read":"GET /api/v1/auth: whether the application has sign-in, where it answers and which origins it trusts. Never the cookie secret.","nextjs":"npm install @neondatabase/auth@latest and follow https://neon.com/docs/auth/quick-start/nextjs. The library is a beta, so that page is the source of truth for its code. The two variables it asks for, NEON_AUTH_BASE_URL and NEON_AUTH_COOKIE_SECRET, are already set.","http":"Any other stack: Better Auth over HTTP at NEON_AUTH_BASE_URL. POST /sign-up/email {email, password, name}, POST /sign-in/email {email, password}, GET /get-session, and GET /token for a JWT that a backend verifies against the keys at NEON_AUTH_JWKS_URL. A request that changes anything needs an Origin header that is the application's own address.","origins":"Localhost works while the project is written. The platform registers the application's address, every domain applications are published under, and any custom domain that has not failed, when sign-in is switched on, on every deploy and when a custom domain is added. Do not configure it. A request from an origin nobody registered is refused with 403 INVALID_ORIGIN.","email":"Email verification is off, and the emails the service sends (a password reset) come from Neon's shared sender, which is rate limited and suits development. The service uses the application's name in them.","rateLimit":"Sign-up and sign-in are limited to about nine attempts per ten seconds from one address; a tenth answers 429 with an x-retry-after header in seconds. A login page should show the person a message to try again. Through the Next.js library every visitor reaches the service from the application's server, so they share that allowance; a front end that calls the service directly is limited per visitor. Reading a session is far looser.","social":"Google or GitHub sign-in needs the user's own OAuth app and is not set up here. Leave it out unless the user asks.","dontPush":"Do not copy these variables into /api/v1/secrets: the platform sets them, and a copy would shadow the real ones.","byo":"A service the user chose themselves wins over ours: a project that sets its own NEON_AUTH_BASE_URL is left alone (409 own_auth), and so is one that connects to a database of its own (409 own_database).","missing":"If a deployment's `missing` lists NEON_AUTH_BASE_URL or NEON_AUTH_COOKIE_SECRET, the project reads them: switch sign-in on (POST /api/v1/auth) instead of asking the user for values. Once it is on they are no longer listed."},"email":{"what":"Email for the application's own users (a welcome, a password reset, a receipt, a contact form) through a relay that speaks Resend's own API, so the `resend` packages for Node and Python work unchanged. There is no Resend account to open and no domain to verify. Created when you ask for it, which should be while the project is still being written.","variables":"RESEND_API_KEY (a token for the relay, not a Resend key), RESEND_BASE_URL (read by the Node package), RESEND_API_URL (read by the Python package) and EMAIL_FROM (a ready From value).","create":{"method":"POST","path":"/api/v1/email","body":"{\"applicationId\":\"<id>\"}  // or ?applicationId=<id>; a project key may omit it","auth":"Account token or project key.","description":"Lets the application send now, before its first deploy, so a password reset or a welcome message is written and tested against the real thing. One row, nothing provisioned elsewhere, so there is no plan allowance to take. Idempotent: a second call answers 200 with the same sender. The deploy that follows gives the running app the same variables.","returns":"201 (200 when it already existed) with `from`, `address`, `perDay`, `sentLast24Hours`, `young`, `perMessage`, `perMinute`, `variables` (their names), `use`, `account` and `summary`. The token is not in the answer; see `env`.","errors":{"409":"own_email (the project already sets a RESEND_API_KEY of its own, so none is set up)","503":"email_unavailable: email is not set up on this server. Carry on without it and tell the user."}},"env":{"method":"GET","path":"/api/v1/email/env?applicationId=<id>","auth":"Account token only. A project key gets 403 account_token_required.","returns":"text/plain, a .env body with the variables above, the same set the deployed app receives. Not JSON, so it can be redirected into a file.","use":"curl -fsS -H \"authorization: Bearer $TOKEN\" \"https://antideploy.com/api/v1/email/env?applicationId=<id>\" >> .env. Keep -f so an error writes nothing into the file, and never print the answer: it holds the token that lets anyone send as this application. Make sure .env is in .gitignore.","errors":"404 no_email | 409 own_email | 403 account_token_required"},"read":"GET /api/v1/email: whether the application can send, from which address, its allowance, what it has sent in the last 24 hours, who stopped it if it is stopped (pausedBy, pausedReason), the last 14 days (`daily`), when it last sent and how its mail is landing (`health`). Never the token.","overview":{"method":"GET","path":"/api/v1/email/overview","auth":"Account token (every application of the account) or project key (its own).","returns":"`applications`, each with state (ready or paused), pausedBy, pausedReason, address, from, sentLast24Hours, lastSentAt and `health`; `withoutEmail` (the applications that have none); `perDay`, `plan`, `young`, `domain`, `available` and `reporting`."},"activity":{"method":"GET","path":"/api/v1/email/activity?applicationId=<id>&limit=<1-200>&before=<time>&status=<accepted|rejected>&outcome=<delivered|bounced|complained|delayed|failed|suppressed>","auth":"Account token (all of the account's applications when applicationId is left out) or project key (its own).","returns":"`rows`, newest first, each with id, application, at, recipients, status (what the relay did: accepted by Resend or rejected), outcome (what Resend reported afterwards, null until it has, and always null while `reporting` is false) and detail (a reason, never an address). `next` is the `before` of the following page. No subject, body or address is kept, so none is returned.","errors":"400 invalid_parameter"},"deliverability":{"method":"GET","path":"/api/v1/email/deliverability?applicationId=<id>","auth":"Account token (all of the account's applications when applicationId is left out) or project key (its own).","returns":"`health` (the last 7 days: sent, delivered, bounced, complained, delayed, failed, suppressed, bounceRate, complaintRate, `state` watch, healthy or no_data, and `reasons`), `recent` problems with the reason Resend gave, `rules` (the numbers the relay decides on) and `reporting`. While `reporting` is false every count is zero because Resend's reports are not connected: that is unknown, not healthy."},"domain":{"method":"GET","path":"/api/v1/email/domain?applicationId=<id>","auth":"Account token or project key.","returns":"The sending `domain`, the application's `address` and `from`, and `ownDomain.available` (false: sending from the owner's own domain is not offered, so do not ask the user to add DNS records for email)."},"pause":{"method":"POST","path":"/api/v1/email/pause","body":"{\"applicationId\":\"<id>\"}","auth":"Account token only. A project key gets 403 account_token_required.","description":"Stops the application's email at once: its token is refused with 403 suspended_api_key until it is resumed. Idempotent. Nothing else about the application changes.","returns":"`state` (paused), `changed`, `account` and `summary`.","errors":"404 no_email"},"resume":{"method":"POST","path":"/api/v1/email/resume","body":"{\"applicationId\":\"<id>\"}","auth":"Account token only.","description":"Starts it again after the owner paused it. A pause the platform made is not lifted here: 409 paused_for_deliverability (its mail bounced or was marked as spam; the owner replies to the notice they were emailed) or 409 paused_by_platform (an operator stopped it).","errors":"404 no_email | 409 paused_for_deliverability | 409 paused_by_platform"},"test":{"method":"POST","path":"/api/v1/email/test","body":"{\"applicationId\":\"<id>\"}","auth":"Account token only.","description":"Sends one real message to the address of the account that owns the application, through the relay exactly as the application's code would, so a person can see that email works. The recipient is never taken from the request. It is checked like any other message and counts as one address against the day.","returns":"`sent`, `messageId`, `to` and `summary`.","errors":"404 no_email | 409 paused | 422 no_email_on_account | 429 daily_quota_exceeded or rate_limit_exceeded"},"remove":{"method":"DELETE","path":"/api/v1/email","body":"{\"applicationId\":\"<id>\"}","auth":"Account token only.","description":"Takes email away from the application: its token is refused from then on, and the running app keeps the variables it was given, so its mail fails until POST /api/v1/email sets it up again (with the same token). Refused with 409 while the platform has it paused, because removing and asking again would hand back a clean one.","errors":"404 no_email | 409 paused_for_deliverability | 409 paused_by_platform"},"stopping":"The platform stops an application's email by itself when its mail bounces or is marked as spam: at least 3 addresses bouncing that are 20% or more of the last 24 hours, or 2 spam reports in 7 days. It emails the owner, and it is not undone from the API. Tell the user what happened in one sentence; do not remove email and ask again to get round it.","use":"Node: new Resend(process.env.RESEND_API_KEY), then resend.emails.send({ from: process.env.EMAIL_FROM, to, subject, html }). Python: resend.api_key = os.environ['RESEND_API_KEY']. The package reads RESEND_BASE_URL or RESEND_API_URL, which point at the platform's relay, so nothing else is configured.","sender":"The message always leaves from the application's own address, <its address>@<the platform's sending domain>, whatever `from` says, and the display name is the application's name, never the one in `from`: a display name is how mail pretends to be somebody else. Reply-To is passed through, and it is how people answer: nothing receives mail at the sending address, so set reply_to (replyTo in Node) to a real one.","limits":{"perMessage":"At most 5 addresses on one message, to, cc and bcc together.","perMinute":"10 messages a minute for one application.","perDay":"Addresses an application may send to in 24 hours, by its owner's plan (see `limits.emailsPerDay`), and no more than 10 for an account only days old.","refusal":"429 with Resend's own names: daily_quota_exceeded or rate_limit_exceeded, and a retry-after header. Show a person a message to try later; do not retry in a loop."},"notSupported":"Attachments, scheduled sending, templates and batch sending (resend.batch.send) are refused with a sentence saying so. Send each message on its own. It is transactional mail, never for newsletters or bulk mail.","dontPush":"Do not copy these variables into /api/v1/secrets: the platform sets them, and a copy would shadow the real ones.","byo":"A RESEND_API_KEY the user set themselves wins over ours: that project is left alone (409 own_email)."},"notIncluded":"Any email or sign-in service other than the ones above is not provided. They are an ordinary library plus a secret, and the database above is all they need from the platform.","uploads":{"warning":"Never write uploads to local disk. The container is replaced on every deploy and anything written to it is gone. Put them in the bucket above."}},"projectKeys":{"create":{"method":"POST","path":"/api/v1/keys","body":"{\"name\":\"ci\",\"applicationId\":\"<id>\"}","auth":"Account token only. A project key cannot mint another one.","returns":"201 with `secret`, shown only in this response. Write it to a file or a CI secret; never print it.","limit":"Ten live keys per application."},"list":{"method":"GET","path":"/api/v1/keys?applicationId=<id>","description":"Names and prefixes. The secret is hashed and never returned."},"revoke":{"method":"DELETE","path":"/api/v1/keys/{id}?applicationId=<id>","auth":"Account token only."}},"rollback":{"method":"POST","path":"/api/v1/rollback","body":"{\"applicationId\":\"<id>\"}  // deploymentId optional","description":"Puts back the last version that went live before the one serving now, or the deploymentId given. That version is built again from its source, so it takes as long as a deploy. Watch it like a deploy.","conflicts":"409 deploy_in_progress if a deploy is already running."},"schedules":{"summary":"Call a path on the application on a schedule: a nightly cleanup, a digest email, a sync.","list":{"method":"GET","path":"/api/v1/cron","description":"The jobs, each with its next and last run, last status and error, and failures in a row."},"create":{"method":"POST","path":"/api/v1/cron","body":"{\"schedule\":\"0 3 * * *\",\"path\":\"/api/cron/nightly\",\"method\":\"POST\"}","errors":"400 invalid_schedule (the message says what is wrong) | 409 job_limit"},"pause":{"method":"PATCH","path":"/api/v1/cron/{id}","body":"{\"enabled\":false}","note":"{\"enabled\":true} resumes it and resets its failure count. To change a schedule or path, remove the job and add it again."},"remove":{"method":"DELETE","path":"/api/v1/cron/{id}"},"rules":"Five-field cron in UTC, or @hourly, @daily, @weekly, @monthly. At most 3 jobs per application, runs at least 5 minutes apart. Each run is a GET or POST to the application's public address plus the path, with the header x-antideploy-cron: 1, and gives up after 60 seconds. 10 failures in a row pause the job.","security":"Anyone can send the x-antideploy-cron header, so an endpoint that does real work should also check a secret of the app's own.","note":"Account tokens pass ?applicationId=<id> or put applicationId in the body; a project key may omit it."},"ai":{"provider":"OpenRouter","summary":"Create an OpenRouter API key paid from the account's prepaid AI wallet. It works with any OpenAI-compatible client at baseUrl https://openrouter.ai/api/v1, with OpenRouter model ids such as openai/gpt-4o-mini.","wallet":{"method":"GET","path":"/api/v1/ai","description":"Balance in paise, the rate usage is charged at, and the keys on this account."},"create":{"method":"POST","path":"/api/v1/ai/keys","body":"{\"name\":\"chatbot\",\"applicationId\":\"<optional>\"}","returns":"201 with `secret`, shown only in this response. With applicationId it is also saved to that application as OPENROUTER_API_KEY; POST /api/v1/redeploy to use it.","projectKeys":"A project key creates keys for its own application only."},"revoke":{"method":"DELETE","path":"/api/v1/ai/keys/{id}"},"prepaid":"A key on an empty wallet is disabled. Agents cannot add money: give the user wallet.topUpUrl from GET /api/v1/ai and wait for them.","topUpCost":"Adding money carries a fee on top, shown on the page before they pay: 15%, so ₹100 of wallet credit costs ₹115. Say that when you ask, so the amount they are quoted is the amount they are charged.","spendingLimits":"The wallet is shared across the account's keys: together they can never spend more than it holds, and the split follows recent use once a minute. A key that gets 403 'Key limit exceeded' while the wallet still has money usually needs a minute for its share to catch up; a busy app should use one key rather than several.","paused":"If GET /api/v1/ai returns paused: true, AI keys are stopped for a safety check and creating keys returns 503. Wallet money is kept. Do not retry in a loop or create new keys; tell the user and try again later."},"limits":{"staticSites":{"free":"unlimited","pro":"unlimited","meaning":"A static site (a folder of HTML, or a frontend such as Vite or Create React App that builds to files) never counts against any limit below, on any plan, and neither do its deploys."},"liveApplications":{"free":1,"pro":9,"meaning":"Applications with a server and an address that serves. Creating one costs nothing; the deploy that first puts it online is what counts. Static sites never count. Whether a project is static is decided when it is analyzed, so a refusal for this limit can arrive on the deploy task rather than as a 402."},"successfulDeploysPerMonth":{"free":10,"pro":"unlimited","meaning":"Only deploys of apps with a server that reach live count, and the allowance starts again on the 1st. Static sites never count. A failed build is free, so fixing one and pushing again costs nothing."},"databases":{"free":1,"go":1,"pro":3,"scale":5,"meaning":"Counted per account however a database came about, whether it was created with POST /api/v1/database before the first deploy or by a deploy. An application that has never been deployed but holds one shows in the console as 'never deployed'."},"buckets":{"free":1,"go":2,"pro":3,"scale":5,"meaning":"File storage buckets, counted per account however one came about, whether it was created with POST /api/v1/storage before the first deploy or by a deploy of a project that uses an S3 client. Storage itself is not metered."},"emailsPerDay":{"free":20,"go":100,"pro":500,"scale":2000,"youngAccount":10,"youngAccountDays":7,"perMessage":5,"perMinute":10,"meaning":"Addresses one application may send to through the email relay in any 24 hours, by the plan of its owner, and no more than `youngAccount` while the account is under `youngAccountDays` days old. A message goes to at most `perMessage` addresses and one application sends at most `perMinute` messages a minute."},"deploysPerHour":20,"maxFiles":4000,"maxFileBytes":29360128,"maxTotalBytes":29360128,"concurrentDeploysPerApplication":1},"acceptableUse":{"refused":"Crypto miners, remote desktops (VNC, RDP, browser desktops) and remote shells or tunnels are refused at analysis. The deploy fails with a notDeployableReason starting \"Antideploy doesn't host\" that names the file and the text that matched. Do not retry; remove that part of the project.","newAccounts":"A custom Dockerfile builds from the first deploy for accounts signed in with GitHub or Google, and on any paid plan. An account made with only an email address waits until it is 7 days old, and an account whose app our CPU monitor stopped in the last 30 days waits for a review. While an account waits, a project the platform can recognise without its Dockerfile is built without it and the deploy says so; one that is nothing but a Dockerfile fails saying why. Do not retry that failure.","suspension":"A suspended account gets 401 on every credential, and 403 with code account_suspended where a deploy or creation is refused. Waiting does not clear it, so do not retry or work around it.","terms":"https://antideploy.com/terms#acceptable"},"notes":["Send the whole project directory. A push containing only an entry point builds and deploys successfully, then serves a page whose assets 404 — the most common failure on this API.","Warnings and hazards in the poll response are structured data meant to be read and relayed, not decoration.","A file larger than maxFileBytes fails the whole push with 400 too_large and names it. Nothing is dropped and built around, so a 202 means every file you sent is in the build — check `files` in the response if you want the manifest back.","Credentials belong in files, never in output. An account token printed to a terminal is a live credential in scrollback and in the transcript of whatever agent is reading along."]}