The Apposters API

Everything you do in the workspace, a script can do too: turn a GitHub repository into a running SaaS - the app hosted at /app/ and its product site around it - publish it, change it and buy what it needs, over HTTPS and JSON. Working with an AI agent? Give it the guide written for agents, https://api.apposters.com/agent/guide: it tells the agent how to sign you in with a short code and build your project step by step.

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

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

CallWhat 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/designsThe 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:

CallWhat 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/githubconnected: true once that is done.
DELETE /app/githubDisconnect, 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

CallWhat 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}/streamThe 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}/cancelStop it. Pages finished before it stay.

Read and change

CallWhat it does
GET /app/sitesAll 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=templateEvery 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}/membersThe 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:

Set it up and deploy

CallWhat it does
PUT /app/sites/{site_id}/deploymentCreate (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/envThe keys, with masked values: a value is never shown back after it is saved.
POST /app/sites/{site_id}/deployment/deployBuild 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}/deploymentThe 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/fixLet 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/overrideForget the AI's fix and build the repository's own Dockerfile again.

While it runs

CallWhat it does
GET /app/sites/{site_id}/deployment/logs?tail=500The app's output, plain text, up to 5000 lines.
GET /app/sites/{site_id}/deployment/metricsCPU, memory, network, and disk: storage.used_mb against storage.limit_mb.
GET /app/sites/{site_id}/deployment/runsThe last 20 deploys.
POST /app/sites/{site_id}/deployment/restartRestart. 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}/deploymentRemove 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

CallWhat 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 AppembedYour app built and running at /app/. One purchase covers one app.
Memorymemory1, 2, 4, 8 or 16 GB for one app instead of 512 MB. One unit is 1 GB.
Disk spacedisk5, 10, 20, 40 or 80 GB for one app instead of 3 GB. One unit is 5 GB.
Custom domaindomainThe 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.

CallWhat it does
GET /billingEmbed 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/summaryWhat the account has bought and how much of it is in use.
GET /app/sites/{site_id}/deployment/resourcesThe 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

CallWhat 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/verifyChecks 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}/domainlive: true when the site answers on the domain, its app at https://www.example.com/app/ included.
DELETE /app/sites/{site_id}/domainRemove 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

CallWhat it does
GET /agent/meWho the token belongs to, and the token itself: name, expires_at.
GET /app/tokensAll 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/tokenRevoke 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

WhatLimit
Generation2 at a time and 30 a day per account (the last 24 hours). Over that, 429: try again later.
App buildsOne at a time per account, 20 a day. While other people's builds run, yours waits its turn (last_run.status: "queued").
A hosted app512 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 domainsOne app per Embed App purchase and one domain per Custom domain purchase; past that, 409 with "code": "quota_reached".
Tokens50 working tokens per account.

Ready to script it?

Create a token in your account, or hand the agent guide to your AI agent.

Create an API token Agent guide

Missing something, or found a call that does not work as written? Write to support@apposters.com or read the FAQ.