Basics
The base URL is https://api.apposters.com, and every path on this page is relative to it: GET /app/sites means GET https://api.apposters.com/app/sites.
Get a token
Open Account, find API tokens, give the new token a name (what will use it), pick how long it lives - 30 days, 90 days, 1 year or never - and press Create token. The token starts with apo_ and is shown once: copy it then. Send it with every call:
Authorization: Bearer apo_...
A token acts as you: whoever holds it can change your projects and start payments. Keep it in an environment variable or a secret store, never in a web page, a URL, a commit or a log. You can revoke it on the same card at any time; from the next request on it answers 401.
Requests and answers
- Send JSON with
Content-Type: application/json. Answers are JSON too, except logs (plain text) and live streams (server-sent events). - An error is
{"error": "What went wrong, in words", "code": "machine_readable"};codeis there when there is something to tell apart. 401- no token, or one that is wrong, expired or revoked.404- not found, and also a project that belongs to someone else.402with"code": "payment_required"- a paid feature that is not paid for yet.429- a limit; try again later.- Call the API from a server, a terminal or a CI job. Web pages on other sites are blocked by CORS on purpose: a token in a web page is a token anyone can read.
export APPOSTERS_TOKEN=apo_... # from Account - API tokens
API=https://api.apposters.com
H="Authorization: Bearer $APPOSTERS_TOKEN"
curl -s "$API/agent/me" -H "$H"
# -> {"user": {"id": "...", "email": "you@example.com"}, "auth": "token", "token": {...}}
Quick start: from a repository to a running SaaS
The whole path in six calls. Only step 4 costs money: Embed App, which runs your app. You pay for it yourself on Stripe's page - the API hands you the link and never sees a card. Each step is explained in the sections below.
J="Content-Type: application/json"
# 1. Create a project from the repository
curl -s -X POST "$API/app/sites" -H "$H" -H "$J" \
-d '{"github_url": "https://github.com/you/your-app", "name": "Your App"}'
# -> 201 {"id": "SITE_ID", "slug": null, ...}
SITE=SITE_ID
# 2. Generate the product site: a landing page, About, FAQ and a sign-in page
curl -s -X POST "$API/app/sites/$SITE/generate" -H "$H" -H "$J" \
-d '{"kind": "template", "mode": "saas", "pages": ["about", "faq", "auth"]}'
# -> 202 {"job_id": "JOB_ID", "status": "running", ...}
JOB=JOB_ID
curl -s "$API/app/jobs/$JOB" -H "$H"
# -> {"status": "done", "next_job_id": "...", ...} poll every few seconds, follow next_job_id
# 3. Check that the repository can run here
curl -s "$API/app/sites/$SITE/repo-check" -H "$H"
# 4. Unlock Embed App: open the Stripe link in a browser and pay
curl -s -X POST "$API/billing/embed/checkout" -H "$H" -H "$J" -d "{\"site_id\": \"$SITE\"}"
# -> {"id": "cs_...", "url": "https://checkout.stripe.com/..."}
# 5. Set the app up and deploy it
curl -s -X PUT "$API/app/sites/$SITE/deployment" -H "$H" -H "$J" -d '{"port": 3000, "build_mode": "auto"}'
curl -s -X POST "$API/app/sites/$SITE/deployment/deploy" -H "$H"
# -> 202 {"job_id": "...", "run_id": "..."}
# 6. Pick the address and publish the site
curl -s -X POST "$API/app/sites/$SITE/slug" -H "$H" -H "$J" -d '{"slug": "your-app"}'
curl -s -X POST "$API/app/sites/$SITE/publish" -H "$H" -H "$J" -d '{}'
# -> {"url": "https://your-app.apposters.com", ...} the app answers at https://your-app.apposters.com/app/
Only need a page about the repository, without the app? Generate a landing page or a website in step 2, skip steps 3 to 5 and publish: nothing on that path costs money.
Projects and pages
Create a project
| Call | What it does |
|---|---|
POST /app/sites | {"github_url": "...", "name": "...", "design": "..."} -> 201 with the project. Its id is what every other call takes. Any form of a GitHub link works. name is the product's name on every page (the repository's name if left out). |
GET /app/designs | The 30 designs to pick from, no token needed: id, name, a one-line description, the project type it suits (type) and scheme (light or dark). Leave design out and Apposters picks the one that matches the repository's description and topics. |
Private repositories
Apposters reads a public repository like any visitor. For a private one, check first and connect GitHub if it cannot see it:
| Call | What it does |
|---|---|
GET /app/github/repo?url=... | Whether Apposters can read the repository: exists, private. |
POST /app/github/connect | {"repo": "https://github.com/you/your-app"} -> {"url": "..."}: open it in a browser and authorize GitHub. |
GET /app/github | connected: true once that is done. |
DELETE /app/github | Disconnect, which also revokes the access on GitHub. |
A private repository can only become a SaaS: its site is about the product and says nothing about the code. The other types answer 422 with "code": "private_repo".
Generate the site
POST /app/sites/{site_id}/generate -> 202 {"job_id": "..."}. The AI writes the pages from the repository's README and metadata, in the project's design. Three project types:
# SaaS: a product site about what the app does; its buttons open the app at /app/
{"kind": "template", "mode": "saas", "pages": ["about", "faq", "auth"], "community": false}
# Website: a landing page about the repository plus the pages you list
{"kind": "template", "mode": "website", "pages": ["about", "faq"]}
# Landing page: one page about the repository
{"kind": "template", "mode": "landing"}
pages is any of about, faq and auth (the sign-in page); the landing page is always made. "community": true adds a community forum for the people who register, linked from the menu. A SaaS is for running the app with Embed App; a landing page and a website are free to publish and can become a SaaS later.
Your own wishes: add "instructions": "...", up to 2000 characters, with what the pages should say beyond the repository, in your words - for example "Add a pricing section. On the FAQ page, answer whether it is free." The landing page, About and FAQ each take the part meant for them, and a wish that names no page goes on the landing page; the sign-in page ignores it. It is not saved with the project: send it again with any later generation that should follow it. A longer text answers 422 with "code": "instructions_too_long".
Wait for it
| Call | What it does |
|---|---|
GET /app/jobs/{job_id} | status: running, done, error, aborted or cancelled; error says why. The pages are made one after another: when a job is done and has next_job_id, follow that job the same way. A page takes a few minutes; poll every few seconds. A job is forgotten ten minutes after it ends. |
GET /app/jobs/{job_id}/stream | The same job as server-sent events, if you would rather watch the page being written: meta, then delta ({"i": n, "t": "text"}), then done or error. After a dropped connection, reconnect with ?from= the next i and nothing is lost. |
POST /app/jobs/{job_id}/cancel | Stop it. Pages finished before it stay. |
Read and change
| Call | What it does |
|---|---|
GET /app/sites | All your projects, newest first, with their address, visits and the state of their app. |
GET /app/sites/{site_id} | One project. wizard is its type and pages (null for a landing page), deployment a summary of its app. |
PATCH /app/sites/{site_id} | Any of name, description, ga_id (your Google Analytics ID), design (an id, or "" for the automatic pick), logo_url, auth_redirect_to_app. A new design shows after the next generation. |
GET /app/sites/{site_id}/versions?kind=template | Every version of a page, without the HTML. kind: template (the landing page), about, faq, auth. |
GET /app/versions/{version_id} | One version, with its html. |
POST /app/sites/{site_id}/versions | {"kind": "template", "html": "...", "name": "What changed"}: your own HTML (up to 5 MB) becomes the current version. |
PUT /app/sites/{site_id}/actual | {"kind": "template", "version_id": "..."}: make an earlier version current again. |
GET /app/sites/{site_id}/members | The people who registered on your published site. |
DELETE /app/sites/{site_id} | Delete the project and its versions; the published site stays up. Add ?purge=1 to take the site down too. A hosted app goes with the project, its data included. |
Generate again: {"kind": "template", "regenerate": true} to the same generate call remakes the landing page and keeps the project type. To change the type, send the whole new plan with "regenerate": true: the landing page and every page of the plan are made again, and the old versions stay in the history. Add instructions to change something specific: {"kind": "about", "instructions": "..."} remakes only the About page that way. Changes go live when you publish again.
Running the app (Embed App)
Embed App builds your repository and runs it at /app/ on the project's own address, for example https://your-app.apposters.com/app/, or on your domain. The /app prefix is taken off before a request reaches the app, so an app that serves at / works as it is; one that routes in the browser needs its base path set to /app. Setting up, deploying, fixing, starting and restarting answer 402 until Embed App is paid for; reading, stopping, logs, metrics, settings and removing always work.
Check the repository first
GET /app/sites/{site_id}/repo-check?ref=HEAD&path=Dockerfile reads the repository without building it:
dockerfile: true- there is a Dockerfile at that path.analysis.exposeis the port to use;analysis.kind: "cli"or anot-a-serverwarning means it is a command-line tool, not a web app.dockerfile_found- the repository builds another file; pass itspathasdockerfile_path.auto.found: true- Apposters can build it without a Dockerfile, the way its framework builds (Vite, Next.js, React, Vue, Angular, Astro, Express, Django, FastAPI, Flask, Streamlit, Go, a plainindex.htmland more):auto.name,auto.build,auto.startorauto.output, andauto.dockerfileis the Dockerfile it would write.setup.vars- the settings the app asks for, withname,required,descriptionandinput:autoones come with a readyvalue,generateones take any long random string,secretones are yours to fill in.
Set it up and deploy
| Call | What it does |
|---|---|
PUT /app/sites/{site_id}/deployment | Create (201) or change (200) the app's setup; every field is optional. build_mode: dockerfile (the default, with dockerfile_path), auto (no Dockerfile needed, see above) or bare (a plain Linux box from base_image - ubuntu:24.04, debian:bookworm-slim, alpine:3.20, node:22-bookworm or python:3.12-bookworm - that you set up in the terminal on the Embed App screen). port: what the app listens on, 3000 by default, passed in as PORT; listen on 0.0.0.0. ref: branch, tag or commit (HEAD is the default branch). health_path: what must answer before traffic switches over, / by default. volume_path: where the app keeps data that survives deploys, for example /data. A project without an address gets one here, from its name. |
PUT /app/sites/{site_id}/deployment/env | {"env": {"DATABASE_URL": "...", "OLD_KEY": null}}: adds and changes the keys you send, null removes one, the rest stay. A running app restarts with them at once. With build_mode: "auto" they reach the build too, so set VITE_... and NEXT_PUBLIC_... values before deploying. |
GET /app/sites/{site_id}/deployment/env | The keys, with masked values: a value is never shown back after it is saved. |
POST /app/sites/{site_id}/deployment/deploy | Build and start -> 202 {"job_id": "...", "run_id": "..."}. Follow GET /app/jobs/{job_id} (or its /stream for the build log) until finished_at is set. If a new build does not come up, the previous one keeps serving. Pushed a new commit? Deploy again. |
GET /app/sites/{site_id}/deployment | The setup and the state: status (configured, building, starting, running, stopped, failed), url, last_run. A failed run has last_run.diagnosis: title, detail and fix, what to do about it. |
POST /app/sites/{site_id}/deployment/fix | Let the AI fix the Dockerfile after a failure and deploy again, up to three attempts. The fix is kept on the Apposters side; your repository is never changed. |
DELETE /app/sites/{site_id}/deployment/override | Forget the AI's fix and build the repository's own Dockerfile again. |
While it runs
| Call | What it does |
|---|---|
GET /app/sites/{site_id}/deployment/logs?tail=500 | The app's output, plain text, up to 5000 lines. |
GET /app/sites/{site_id}/deployment/metrics | CPU, memory, network, and disk: storage.used_mb against storage.limit_mb. |
GET /app/sites/{site_id}/deployment/runs | The last 20 deploys. |
POST /app/sites/{site_id}/deployment/restart | Restart. POST /app/sites/{site_id}/deployment/stop and POST /app/sites/{site_id}/deployment/start work the same way. |
POST /app/sites/{site_id}/deployment/reset-data | {"confirm": "your-app"}, the app's slug as GET /app/sites/{site_id}/deployment shows it: empty the data volume and start clean. Cannot be undone. |
DELETE /app/sites/{site_id}/deployment | Remove the app: the container, the image and the data. |
Sign-in in front of the app: with a sign-in page published and the app deployed, PATCH /app/sites/{site_id} with {"auth_redirect_to_app": true} sends people who sign in on your site straight to the app, and lets only them use it.
Publishing
| Call | What it does |
|---|---|
GET /app/subdomain-check?slug=your-app | {"available": true}, or false with a reason: invalid, reserved or taken. |
POST /app/sites/{site_id}/slug | {"slug": "your-app"} -> {"slug": "your-app", "url": "https://your-app.apposters.com"}. 3 to 60 characters of a-z, 0-9 and -. Renaming a published site moves it, its app and its members to the new address and frees the old one: links to the old address stop working. |
POST /app/sites/{site_id}/publish | {} -> {"url": "...", "pages": [...]}. Every generated page goes live at once, with sign-in at /auth. "discourse": true also opens the community forum (on by default when the project was generated with "community": true). Publishing is free. |
POST /app/sites/{site_id}/publish-page | {"kind": "about"}: put one changed page live without touching the others. |
Publishing also writes the site's sitemap.xml and robots.txt, link-preview tags and the analytics tag, and lets search engines know about the pages.
Payments
Generating, publishing, the address on apposters.com and a hosted app's free tier - 512 MB of memory, one CPU and 3 GB of disk for its image and data together - cost nothing. These do:
| Product | {product} | What you get |
|---|---|---|
| Embed App | embed | Your app built and running at /app/. One purchase covers one app. |
| Memory | memory | 1, 2, 4, 8 or 16 GB for one app instead of 512 MB. One unit is 1 GB. |
| Disk space | disk | 5, 10, 20, 40 or 80 GB for one app instead of 3 GB. One unit is 5 GB. |
| Custom domain | domain | The project on your own domain. One purchase covers one domain. |
The prices are on the Pricing page and in GET /billing/prices (no token needed). You always pay on Stripe's page in a browser; the API starts the payment and checks it, and never takes card details.
| Call | What it does |
|---|---|
GET /billing | Embed App and Custom domain for your account: paid, required (has to be paid before use), price_label. GET /billing/{product} is one of them. |
POST /billing/{product}/checkout | {"site_id": "..."} -> {"id": "cs_...", "url": "https://checkout.stripe.com/..."}. Open url and pay. For a second app or domain add "additional": true. |
POST /billing/{product}/confirm | {"session_id": "cs_..."} -> {"paid": true} once Stripe has the payment (false means not yet). You can also just wait: GET /billing/{product} turns paid: true by itself. |
GET /billing/account/summary | What the account has bought and how much of it is in use. |
GET /app/sites/{site_id}/deployment/resources | The app's memory and disk now (limit_mb), what it already pays for (units) and what more would cost (steps). |
More memory or disk for an app, either together with Embed App or on its own once the app is set up. The number is the size the app should end up with, not an addition, and you pay only for the units you do not have yet:
POST /billing/embed/checkout {"site_id": "...", "resources": {"memory": 2, "disk": 1}}
POST /billing/memory/checkout {"site_id": "...", "units": 4}
POST /billing/disk/checkout {"site_id": "...", "units": 2}
Confirm them the same way. More memory takes effect on the next deploy, more disk at once. Signs an app needs them: a failed deploy whose last_run.diagnosis.limit.kind is memory, or a deploy answered 409 with "code": "storage_full". Lowering or cancelling what you pay for is done in the browser, under Manage payments in Account.
Custom domain
| Call | What it does |
|---|---|
PUT /app/sites/{site_id}/domain | {"domain": "www.example.com"} -> records, the DNS records to add at your DNS provider: a CNAME to your apposters.com address, or for a bare domain an A record together with a TXT record that proves the domain is yours. The domain has to point here directly (at Cloudflare: DNS only, not proxied). |
POST /app/sites/{site_id}/domain/verify | Checks DNS now; dns.message says what is still missing. Once the records are found, the HTTPS certificate is issued by itself, usually within minutes. |
GET /app/sites/{site_id}/domain | live: true when the site answers on the domain, its app at https://www.example.com/app/ included. |
DELETE /app/sites/{site_id}/domain | Remove the domain. Always free; the apposters.com address keeps working. |
Connecting a domain and checking its DNS are paid for with the domain product (see Payments).
Tokens
| Call | What it does |
|---|---|
GET /agent/me | Who the token belongs to, and the token itself: name, expires_at. |
GET /app/tokens | All of the account's tokens, without their secrets: name, the last characters (hint), last_used_at, expires_at, and source - account for one you created, agent for one an AI agent got with a code. |
DELETE /app/tokens/{token_id} | Revoke a token. It stops working on its next request. |
DELETE /agent/token | Revoke the token this call is made with: the sign-out for a script that is done. |
New tokens are created only in the browser, in Account: a token cannot make another one, so a leaked token cannot outlive its revocation. For the same reason a token gets 403 with "code": "session_required" on the Stripe billing portal. An AI agent does not need you to copy a token at all: its guide signs you in with a short code you read off a page.
Limits
| What | Limit |
|---|---|
| Generation | 2 at a time and 30 a day per account (the last 24 hours). Over that, 429: try again later. |
| App builds | One at a time per account, 20 a day. While other people's builds run, yours waits its turn (last_run.status: "queued"). |
| A hosted app | 512 MB of memory, one CPU and 3 GB of disk unless more is bought. The image and the app's data share that disk. |
| Apps and domains | One app per Embed App purchase and one domain per Custom domain purchase; past that, 409 with "code": "quota_reached". |
| Tokens | 50 working tokens per account. |