Your App Store pages, from your terminal.
Everything Pagefully does in the browser, it does over HTTP: read your app, plan a page for each search, make the screenshots, check them, and send them to App Store Connect. Call it from a script, from CI, or hand it to your coding agent. It is on every plan, Free included.
Working from Claude Code or Cursor? The Pagefully MCP server gives your agent the same work as tools, with setup in one line.
Try it before you sign up
One command, no token and no account. Give it any app on the App Store and you get the pages we would plan for it, the first one designed, and a link to look at it.
npx pagefully preview https://apps.apple.com/us/app/your-app/id1234567890Or the same thing as one HTTP call:
curl -X POST https://pagefully.com/api/v1/preview \
-H "Content-Type: application/json" \
-d '{"app_store_url": "https://apps.apple.com/us/app/your-app/id1234567890"}'The first call for an app starts the work and answers "state": "making". Ask again in a few seconds and it is ready. A preview is made once for an app and kept, so asking again costs nothing.
Make a token
Sign up free, open Settings, and make a token under API tokens. You see it once, so copy it then. Send it with every call:
Authorization: Bearer pf_live_...A token is one of three kinds, so you can hand out only what a job needs:
- Read reads your apps, plans, pages, results and usage. It changes nothing.
- Generate also adds apps, makes plans and pages, and edits and approves them. It cannot publish. A good fit for CI.
- Full also sends approved pages to App Store Connect.
Revoke a token in Settings and it stops working at once. Tell us what is calling in an X-Pagefully-Client header: cli, ci, mcp or other.
The CLI
If you would rather type commands than write curl, pagefully is the whole API from your terminal. It has no dependencies and needs Node 20 or later. Run it with npx pagefully, or install it with npm install -g pagefully.
pagefully login # paste a token from Settings
pagefully add <app store link> # add your app and read it
pagefully plan <app> # one page for each search
pagefully generate <app> # make the pages
pagefully page <page id> # the words, pre-flight, and a link to look at it
pagefully page <page id> --approve # your yes
pagefully publish <page id> # shows what would be sent, then askspagefully login asks for your token without showing it, checks it, and keeps it in ~/.config/pagefully/token, readable by you alone. Or set PAGEFULLY_TOKEN. Where a command takes an app, give its name, its id, or its App Store link.
preview <app store link or id> | The pages Pagefully would plan for any app, and the first one designed. No token. |
|---|---|
login | Keep a token on this machine. logout forgets it. |
apps | Your apps. |
add <app store link or id> | Add an app from the App Store and read it. |
plan <app> | The plan, built the first time. --approve <intent> switches one on, --remove <intent> takes one out. |
generate <app> | Make a page for every intent that is on and has none. |
pages <app> | The app’s pages. |
page <page id> | One page: each screenshot’s headline, pre-flight, and its preview link. --approve approves it. |
refresh <app> | Make pages again after an update and print their preview links. --pages <ids> names them. It never publishes. |
publish <page ids…> | Show exactly what would be sent, ask, and send on yes. |
retire <page id> | Free the place of a page you sent with publish --local. |
results <app> | Each page against your default page. |
usage | Each app’s plan, what is used and what is left. |
job <job id> | A job’s state and steps. --wait follows it to the end. |
Every command takes --json for scripts. Anything that takes a while shows each step as it finishes, and --no-wait hands you the job’s id instead.
pagefully publish shows you exactly what would be made in App Store Connect, then asks Send these to App Store Connect? [y/N]. Only a yes sends. With no terminal to ask at, nothing is sent.
In CI
Give your pipeline a generate token. It can make pages for you to look at and cannot send one. publish --yes is refused unless you also set PAGEFULLY_ALLOW_PUBLISH=1, so a pipeline cannot publish by accident.
- run: npx pagefully generate "$APP_ID" --json
env:
PAGEFULLY_TOKEN: ${{ secrets.PAGEFULLY_TOKEN }}0 | Done. |
|---|---|
1 | It failed or was refused. The message says why. |
2 | The app’s plan does not allow it. The message and the plan screen’s address are printed as the API gave them. |
3 | No token, or one Pagefully did not accept. |
Working in Claude Code, Cursor or another coding agent? Connect the MCP server and ask it for your first page. This page is also plain markdown for agents at /developers.md.
Add your app
By its App Store link or Apple’s id for it. You do not need an App Store Connect key to plan and make pages. You need one to publish, and you connect it in the web app when you get there.
curl -X POST https://pagefully.com/api/v1/apps \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "X-Pagefully-Client: cli" \
-H "Content-Type: application/json" \
-d '{"app_store_id": "1234567890"}'
# {"app": {"id": "APP_ID", ...}, "did": "started", "job_id": "JOB_ID"}
curl https://pagefully.com/api/v1/jobs/JOB_ID -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# {"job": {"state": "done", "progress": {"done": 3, "total": 3},
# "result": {"brief_job_id": "..."}, "error": null, ...}}Anything that takes a while answers with a job_id. Poll GET /jobs/{id} until its state is done or failed. Adding an app reads its listing and then writes its brief, so there is nothing else to start.
Plan the pages
The brief is what we understood about your app. The plan is one page for each thing people search for. Read both, change what is wrong, and switch off any page you do not want.
# Read the brief, then build the plan from it
curl https://pagefully.com/api/v1/apps/APP_ID/brief -H "Authorization: Bearer $PAGEFULLY_TOKEN"
curl -X POST https://pagefully.com/api/v1/apps/APP_ID/plan -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# The intents, each with its keywords and what its page says
curl https://pagefully.com/api/v1/apps/APP_ID/plan -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# Change it: switch one off, add one, put them in order
curl -X PATCH https://pagefully.com/api/v1/apps/APP_ID/plan \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"update": [{"id": "INTENT_ID", "on": false}],
"add": [{"name": "Study sessions", "kind": "problem",
"message": "Timed sessions that keep you off your phone."}]}'Make and check the pages
Each page is written, drawn on your own screenshots and read against App Review’s rules. Every page comes back with a preview_url, because a page is something you look at before you say yes.
# One page for every intent that is on
curl -X POST https://pagefully.com/api/v1/apps/APP_ID/pages -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# {"started": 2, "jobs": [{"job_id": "...", "page_id": "PAGE_ID"}, ...]}
# The page: headlines, screenshots, pre-flight, and a link to look at it
curl https://pagefully.com/api/v1/pages/PAGE_ID -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# Change a headline, then approve the page
curl -X PATCH https://pagefully.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"position": 1, "headline": "Start in one tap"}'
curl -X POST https://pagefully.com/api/v1/pages/PAGE_ID/approve -H "Authorization: Bearer $PAGEFULLY_TOKEN"Editing a page withdraws its approval, so what you approve is always what you last saw. A page cannot be approved while its pre-flight has a failure.
Use your own screens
Pages are drawn from your listing’s screenshots. You can add screens of your own: captures from the simulator, as PNG or JPEG, 8 MB a file at most, at a size Apple takes for an iPhone or iPad screenshot. An app has 10 places for iPhone screens and 10 for iPad, your listing’s own included. An uploaded screen takes the next free place, and a page shows it when you ask for it by that position, or chooses it by itself when the page is made.
# Capture a screen from the simulator, then upload it
xcrun simctl io booted screenshot --type=jpeg home.jpg
curl -X POST https://pagefully.com/api/v1/apps/APP_ID/screens \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-F "file=@home.jpg"
# {"added": [{"id": "SCREEN_ID", "source": "uploaded", "device": "iphone", "position": 6, ...}],
# "screens": [...]}
# Put it on a page: the screenshot in place 1 now shows the screen in position 6
curl -X PATCH https://pagefully.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"position": 1, "screen": 6}'GET /apps/{id}/pages gives each page screens_changed: true when a screen the page shows is a different picture now than when the page was drawn, and null when that cannot be told. Nothing you upload is sent to Apple until a page that shows it is published.
Make it again
When a page is not right, change it by hand with PATCH, or have it made again. Making it again counts against the month’s rewrites for the app’s plan. Changing words by hand does not.
# The whole page, written and drawn again. Answers 202 with a job_id
curl -X POST https://pagefully.com/api/v1/pages/PAGE_ID/regenerate -H "Authorization: Bearer $PAGEFULLY_TOKEN"
# One screenshot: new words, the next style, or the next screen
curl -X POST https://pagefully.com/api/v1/pages/PAGE_ID/regenerate \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"what": "headline", "position": 2}'Publish, in two steps
Sending a page to App Store Connect is slow to take back, so no single call does it. The first call tells you exactly what would be sent and gives you a confirmation token. The second call spends it. The token lasts 10 minutes, works once, and only for the API token that asked for it.
# Step one: what would be sent. Nothing is sent yet.
curl -X POST https://pagefully.com/api/v1/pages/publish \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"page_ids": ["PAGE_ID"]}'
# {"confirmation_token": "pfc_...", "expires_in_minutes": 10,
# "will_send": [{"name": "...", "screenshots": [...], "preview_url": "..."}],
# "will_not_send": [], "what_happens": [...]}
# Step two, once a person has read the summary and said yes
curl -X POST https://pagefully.com/api/v1/pages/publish/confirm \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirmation_token": "pfc_..."}'If you are building an agent, show the person the summary and each preview link between the two calls, and wait for their yes. A page is made in App Store Connect and is not submitted for review: that click stays yours, in App Store Connect. We never touch your default page, your keyword field, your app binary, your pricing or your in-app purchases.
Errors
Every error has the same shape. The message is a plain sentence you can show as it is.
{
"error": {
"code": "upgrade_required",
"message": "This app is on Free and its one page is made. The Pack ($49 once) allows 10 pages. Pro ($348 a year or $59 a month) allows 70, which is Apple's limit for an app.",
"upgrade": {
"url": "https://pagefully.com/apps/APP_ID/billing",
"plan": "pack",
"facts": { "plan": "free", "pages_made": 1, "pages_allowed": 1 }
}
}
}400 invalid_request | The body could not be read, or a field is missing. |
|---|---|
401 unauthorized | No token, or one that is not valid or was revoked. |
402 upgrade_required | The app’s plan does not allow it. The message has the facts and upgrade.url is the plan screen. |
403 forbidden | The token’s scope is too small for this call. |
404 not_found | Not on your workspace. |
409 key_required, plan_exists, own_words, not_ready, confirmation_used, changed | Something has to happen first. The message says what. |
410 confirmation_expired | The confirmation is more than 10 minutes old. Ask for a new one. |
413 too_large | An uploaded screen, or the call that carries it, is over the limit. |
422 refused | Understood, and the product said no. The message is the one the web app shows. |
429 rate_limited | Too many calls. retry_after is seconds to wait. |
Plans work the same here as in the web app, and belong to an app: Free makes 1 page, the Pack ($49 once) makes 10, and Pro goes up to Apple’s 70. When a plan stops a call you get upgrade_required with the numbers and a link to the app’s plan screen. GET /account/usage tells you what is left before you ask. There is no charge for a call. See plans.
Webhooks
Instead of polling, give us an https address and we tell you when something happens. Add one in Settings, or with a full token. A workspace holds 5.
curl -X POST https://pagefully.com/api/v1/webhooks \
-H "Authorization: Bearer $PAGEFULLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/pagefully", "events": ["page.ready", "page.sent", "job.failed"]}'
# {"webhook": {"id": "WEBHOOK_ID", ...}, "secret": "whsec_..."} The secret is shown this once.
# What arrives, as a POST:
# Pagefully-Signature: t=1791547200,v1=5f2b...
# {"id": "page.ready:JOB_ID", "type": "page.ready", "created_at": "2026-10-09T12:00:00.000Z",
# "data": {"app_id": "...", "page_id": "...", "name": "...", "state": "ready", "preview_url": "..."}}page.ready | A page has been written, drawn and checked, and is ready to look at. |
|---|---|
page.sent | A page was made in App Store Connect. It is not submitted for review: that click is the developer's, in App Store Connect. |
job.failed | A job failed. The message says what happened and what can be done. |
results.weekly | Once a week, for an app on the Pack or Pro: each page's numbers against the default page. |
There is no page.live and no page.rejected. Pagefully makes a page in App Store Connect and does not submit it for review, and it does not read Apple's verdict back, so it cannot say that a page went live or was rejected.
Every delivery is signed. Pagefully-Signature carries a timestamp and an HMAC-SHA256, made with your secret, of the timestamp, a full stop and the body exactly as sent. Check it before you trust a delivery, and refuse one more than 300 seconds old:
import { createHmac, timingSafeEqual } from 'node:crypto'
// body is the request body exactly as it arrived, before any JSON parsing
export function fromPagefully(secret, header, body) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const want = createHmac('sha256', secret).update(parts.t + '.' + body).digest('hex')
return want.length === parts.v1.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1))
}Answer with any 2xx within ten seconds. Anything else is tried again, 6 attempts in all, after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. A delivery can arrive more than once, so dedupe by its id. A redirect is not followed, and an address on a private or local network is refused.
Limits
- 120 requests a minute for a token.
- 20 calls an hour that start generation: adding an app, a brief, a plan, pages.
- Past either you get
429withretry_afterin seconds. - The preview without a token has the same limits as the preview on this site.
Every call
All under https://pagefully.com/api/v1. The full description, for tools that read one, is openapi.json (OpenAPI 3.1).
/preview | POST. Preview any app’s pages. No token. |
|---|---|
/apps | GET, POST. Your apps, and adding one from the App Store. |
/apps/{id} | GET. One app, and where it stands. |
/apps/{id}/screens | GET, POST. The app’s screens, and uploading ones you captured. |
/apps/{id}/screens/{screenId} | DELETE. Remove an uploaded screen. |
/apps/{id}/brief | GET, POST, PATCH. What the app does and who it is for. |
/apps/{id}/plan | GET, POST, PATCH. One page for each search intent. |
/apps/{id}/pages | GET, POST. The app’s pages, and making them. |
/pages/{id} | GET, PATCH. One page, and changing a screenshot’s words, screen or style. |
/pages/{id}/regenerate | POST. Make a page, or one screenshot, again. |
/pages/{id}/results | GET. One page against your default page, by day. |
/pages/{id}/approve | POST. Approve a page. |
/pages/publish | POST. Step one of publishing. |
/pages/publish/confirm | POST. Step two of publishing. |
/pages/{id}/bundle | POST. Local mode: a page’s files and the App Store Connect calls, to send with your own key. |
/pages/{id}/sent | POST. Local mode: say which page your machine made. |
/pages/{id}/retire | POST. Local mode: free a page’s place on your plan. |
/apps/{id}/results | GET. Each page against your default page. |
/apps/{id}/suggestions | GET. Pages your app does not have yet. |
/jobs/{id} | GET. A job’s state and progress. |
/account/usage | GET. Each app’s plan, and what is left. |
/webhooks | GET, POST. Your webhooks, and adding one. |
/webhooks/{id} | DELETE. Remove a webhook. |
/webhooks/{id}/test | POST. Send a test delivery. |
For coding agents there is the MCP server. An app that signs you in with OAuth can call the API too: its metadata is at /.well-known/oauth-protected-resource/api/v1. Questions, or something you need that is missing: hello@pagefully.com.