Shipping a static site to Cloudflare

Everything needed to take a folder of HTML files and put it on the internet, on your own domain, with HTTPS. Three parts — two for you, one for the agent.

What is in this guide
  1. Part 1 — You: create a Cloudflare API token (with screenshots)
  2. Part 2 — The agent: the deployment playbook. You do not run this.
  3. Part 3 — You: point your domain at Cloudflare. The last step.

What you will hand over. A ZIP file of your website, the API token from Part 1, and your domain name. That is all the agent needs.

Part 1 — Create the API token

You do this

This gives the agent permission to act on your Cloudflare account. It takes about two minutes. You need a Cloudflare account first — signing up at dash.cloudflare.com/sign-up is free.

1.1 — Find the API tokens page

Log in to dash.cloudflare.com. In the left sidebar, scroll to the bottom and open Manage account, then click Account API tokens.

Then click the blue Create Token button at the top right.

Cloudflare dashboard showing Manage account expanded in the sidebar with Account API tokens selected, and the Create Token button top right
Sidebar → Manage accountAccount API tokens. The Create Token button is top right.

Use this page, not the profile one. There is a second, similar page under your user profile. Use Account API tokens as shown above. Tokens made here are scoped to the account, which is what the agent needs.

1.2 — Open the full template list

The create page shows only four templates by default. The one you need is not among them. Click View all templates.

The Create a token page showing four default template options and a View all templates link
The default view shows only four templates. Click View all templates to see the rest.

1.3 — Choose "Developer Services"

From the expanded list, click Developer Services (19 permissions). This is the only template that covers deploying a site.

Expanded list of all Cloudflare API token templates with Developer Services visible
The full template list. Pick Developer Services — 19 permissions.

Clicking it creates two permission policies automatically:

Two permission policies created by the Developer Services template
One policy for all zones, one for the whole account. Both are added for you.

1.4 — Add two more permissions by hand

The template does not include permission to manage DNS or to add domains. Without these, the agent cannot finish the job. Add them now.

  1. On the first policy — the one that says All zones in … Account — click Edit policy.
  2. Expand the DNS & Zones category.
  3. Tick Edit on the row named DNS.
  4. Tick Edit on the row named Zone.
  5. Click Close editor.
DNS and Zones category expanded with Edit ticked on the DNS row and the Zone row
DNS & Zones should read 2/12, with Edit ticked on DNS ("Grants write access to DNS") and Zone ("Grants write access to zone management").

Why these two are easy to miss. They only appear in the All Domains policy, not the account one — even though adding a domain feels like an account-level action. If you cannot find them, you are editing the wrong policy.

1.5 — Name it and set the expiry

Give the token a name you will recognise later, such as site-deploy. Cloudflare pre-fills a random name like super-boat-67f3; change it.

Under Token expiration, choose No expiration if this token will keep being used for future deploys. If it is for a single hand-over, pick 30 days and let it die on its own.

Token name field and the token expiration options
Name the token, then pick an expiry. Client IP filtering can be left empty.

1.6 — Review and create

Click Review token and check the summary against the table below before clicking Create token.

The review token screen listing all granted permissions
The review screen. The three lines under All zones are the ones to verify.
ScopeMust include
All zones Workers Routes Write, DNS Write, Zone Write
Entire account Pages Write, Workers Scripts Write, Workers R2 Storage Read + Write, Workers KV Storage Read + Write, Account Settings Read, and others from the template

If DNS Write and Zone Write are missing, go back and redo step 1.4.

Copy the token immediately. Cloudflare shows the secret exactly once, on the screen right after you click Create token. If you close that dialog without copying it, the token is unrecoverable and you must make a new one.

Send it to the agent through whatever channel you already trust. Do not commit it to a Git repository.

1.7 — Grab your account ID too

It is in the address bar the whole time. The URL looks like:

https://dash.cloudflare.com/70a939330833ed084613ded7287a7d8d/api-tokens

That 32-character string is your account ID. It is not a secret — it appears in every dashboard URL — but the agent needs it. Send it along with the token.

Part 2 — Deployment playbook

For the agent — not for you

This section is written for the AI agent doing the deployment. If you are the person who owns the website, you do not need to read, understand, or run any of it. Nothing here is a task for you. Skip to Part 3.

The agent receives three things: a ZIP of the site, the API token, and the domain name.

2.0 — Inputs and ground rules

InputExample
Site archivesite.zip
API tokenstored at ~/.ssh/CF_TOKEN — never echoed, never committed
Domainexample.com
Account ID70a939330833ed084613ded7287a7d8d
export CLOUDFLARE_API_TOKEN=$(tr -d ' \t\r\n' < ~/.ssh/CF_TOKEN)
export CLOUDFLARE_ACCOUNT_ID=70a939330833ed084613ded7287a7d8d
export DOMAIN=example.com
export PROJECT=example-site

2.1 — Verify the token

npx wrangler whoami

Expect the account name and ID.

Do not use /user/tokens/verify. It returns Invalid API Token for account-scoped tokens because it is a User-scope endpoint. That is not a fault. Judge the token by whether real calls succeed.

2.2 — Unpack the site and isolate what ships

mkdir -p site && unzip -o site.zip -d site
find site -type f | head -50

Establish a public/ directory holding only what should be world-readable. Move the site files there.

Never deploy the repository root. It will publish source archives, notes, tokens documentation and anything else lying around. Deploy an explicit directory containing only the site.

Check the HTML uses relative paths (href="styles.css"), not root-absolute ones (href="/styles.css"). Absolute paths work on a server but break when the file is opened directly from disk.

2.3 — Make sure the domain is a zone

List zones first; the domain may already be there.

curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN"

If it is absent, create it. This needs Zone Write:

curl -s -X POST \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data "{\"name\":\"$DOMAIN\",\"account\":{\"id\":\"$CLOUDFLARE_ACCOUNT_ID\"},\"type\":\"full\"}" \
  "https://api.cloudflare.com/client/v4/zones"

The response carries name_servers and original_name_servers. Record both. The first pair is what the user must set at their registrar in Part 3; the second tells you who currently runs their DNS.

2.4 — Preserve existing DNS before anything switches

This is the step that causes silent outages if skipped. Cloudflare's automatic import frequently finds nothing. If the nameservers change while records are missing, the website and — far worse — email stop working, and nobody notices for a day.

Check what Cloudflare actually imported:

curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?per_page=100"

Then query the real records straight from the current authoritative nameserver, taken from original_name_servers:

for t in A AAAA MX TXT CNAME NS; do
  echo "== $t =="
  nslookup -type=$t $DOMAIN ns.theircurrentprovider.com
done
nslookup -type=A www.$DOMAIN ns.theircurrentprovider.com

dig is not installed on Windows and returns empty output rather than an error — indistinguishable from a domain that does not resolve. Use nslookup.

Recreate anything missing, especially MX and TXT. Replicate faithfully — same values, proxied: false — so the nameserver switch changes nothing:

curl -s -X POST \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"type":"MX","name":"example.com","content":"mail.host.com","priority":10,"ttl":1,"proxied":false}' \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"

2.5 — Create the Pages project and deploy

npx wrangler pages project create $PROJECT --production-branch=main
npx wrangler pages deploy public --project-name=$PROJECT --branch=main --commit-dirty=true

Expect Success! Uploaded N files and a live <project>.pages.dev URL. A fatal: ambiguous argument 'HEAD' line is git complaining about a repository with no commits. Harmless.

2.6 — Attach the custom domains

There is no wrangler pages domain command. Use the API. Needs Pages Write.

for d in $DOMAIN www.$DOMAIN; do
  curl -s -X POST \
    -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    -H "Content-Type: application/json" \
    --data "{\"name\":\"$d\"}" \
    "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$PROJECT/domains"
done

Each returns status: initializing.

Order matters. Attach the domain in the Pages project before creating the DNS record. Doing it the other way round produces a 522 error that looks like a server fault and sends you debugging the wrong thing.

2.7 — Point DNS at the deployment

Only once the site should go live. Needs DNS Write.

for n in $DOMAIN www.$DOMAIN; do
  curl -s -X POST \
    -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    -H "Content-Type: application/json" \
    --data "{\"type\":\"CNAME\",\"name\":\"$n\",\"content\":\"$PROJECT.pages.dev\",\"ttl\":1,\"proxied\":true}" \
    "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"
done

The apex works as a CNAME only because Cloudflare flattens it. proxied: true is required for that.

2.8 — Wait for certificates, then verify properly

curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$PROJECT/domains"

Poll until both read status=active. Roughly 90 seconds after DNS resolves. Nothing needs to be done about certificates — Cloudflare issues and renews them itself.

# HTTP status
for u in https://$DOMAIN/ https://www.$DOMAIN/; do
  curl -s -o /dev/null -w "%{http_code} %{content_type}  $u\n" "$u"
done

# HTTP -> HTTPS redirect
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' http://$DOMAIN/

# certificate
echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

Finally, open the site in a browser and look at it. A 200 response does not prove the page renders.

2.9 — Report back

Tell the user the live URLs, the certificate issuer and expiry, and — most importantly — the two Cloudflare nameservers from step 2.3, because Part 3 cannot happen without them.

What the agent cannot do. Changing nameservers happens at the registrar, which Cloudflare has no authority over. That step is always the user's. Hand them the two nameserver names and stop.

Part 3 — Point your domain at Cloudflare

You do this — and this is the last step

Everything else is finished by now. This is the switch that makes your domain start using Cloudflare, and it can only be done where you bought the domain.

3.1 — What you are changing, and what you are not

You are not transferring your domain. It stays registered where you bought it. You keep paying and renewing there. Nothing about ownership changes.

You are only changing which servers answer questions about your domain. That is a free setting, and reversible.

3.2 — Before you switch: protect your email

If your domain receives email, read this. When nameservers change, any record that has not been copied to Cloudflare stops working. Missing MX records means incoming mail silently stops. It is the single most common way this migration goes wrong.

The agent should already have copied your records across in step 2.4. Ask them to confirm your MX and TXT records are present before you change anything.

Also check whether your provider has DNSSEC enabled for the domain. If it does, turn it off before switching. DNSSEC signed by the old provider makes the domain completely unreachable once Cloudflare starts answering. You can switch it back on through Cloudflare later.

3.3 — Set the nameservers

The agent will have given you two names that look like this:

coby.ns.cloudflare.com
noor.ns.cloudflare.com

They are specific to your account, so use the ones you were given, not these.

Log in where you bought the domain and find the nameserver setting. It is usually called Nameservers, DNS servers, Custom DNS or DNS management.

RegistrarWhere the setting lives
GoDaddyMy Products → Domain → DNS → Nameservers → Change → "I'll use my own nameservers"
NamecheapDomain List → Manage → Nameservers → Custom DNS
PorkbunDomain Management → Authoritative Nameservers → Edit
Squarespace (ex-Google Domains)Domains → your domain → DNS → Nameservers → Use custom nameservers
Zone.eeDomains → your domain → Nameservers
OVHDomain → DNS servers tab → Modify DNS servers

Replace, do not add. Delete the existing nameservers and put Cloudflare's two in their place. Leaving the old ones alongside causes intermittent failures where the site works for some visitors and not others — the hardest kind of problem to diagnose.

3.4 — Wait

Cloudflare checks periodically and emails you when the domain becomes Active. This usually takes one to two hours. Registrars quote "up to 48 hours" as a worst case; that is rare.

Cloudflare zone overview showing the waiting-for-nameserver-propagation state
While waiting, the domain's overview page in Cloudflare shows this status. It flips to Active on its own.

You can check independently at any time:

nslookup -type=NS example.com

When that returns the two *.ns.cloudflare.com names, it has propagated.

3.5 — Then check two things

  1. Visit your site at https://example.com and https://www.example.com. Both should load over HTTPS with no browser warning.
  2. Send yourself an email at your domain, if you use email there. If it does not arrive, an MX record did not get copied — tell the agent and it can be fixed in a minute.

Certificates need nothing from you. Not now, not at renewal, not ever. Cloudflare issues a certificate for each hostname automatically and renews it roughly every 90 days. There is no file to install, no key to store, no reminder to set.

If something goes wrong

SymptomCauseFix
522 error on your domainDNS record created before the domain was attached to the Pages projectAgent re-attaches it in the Pages project, then waits
Certificate stuck pending for hoursCAA records blocking Cloudflare's certificate authoritiesAdd CAA records allowing pki.goog, letsencrypt.org and ssl.com
Domain stuck Pending Nameserver UpdateNameservers not changed, or DNSSEC still on at the old providerRecheck the registrar; disable DNSSEC
Email stopped arrivingMX records were not copied before the switchAdd them in Cloudflare's DNS page — mail resumes within minutes
Site works for some people, not othersOld and new nameservers both listedRemove the old ones entirely
Bare domain will not work, www doesDNS is not on CloudflareAn apex domain requires Cloudflare nameservers. Complete Part 3.