Automatically Purge StackPath CDN Cache with GitHub Actions

by Fahim

You push an urgent bug fix to production, your build passes, and your origin server deploys cleanly. Ten minutes later, users are still pinging you about a broken layout because their browsers are fetching a cached JavaScript chunk from three hours ago. The CDN edge nodes are just doing what you told them to do: holding cached responses until the TTL expires. That leaves you logging into the control portal just to click a purge button.

I got tired of doing that manually, so I wired up a GitHub Actions workflow that grabs an OAuth2 bearer token from the StackPath API and wipes the edge cache on every push to main. You can drop this into an existing CI/CD pipeline or run it as a standalone trigger.

Automatically Purge StackPath CDN Cache with GitHub Actions
Automatically Purge StackPath CDN Cache with GitHub Actions

The Stale Asset Problem on Push

Edge caching is great for speed—it serves static assets from whatever node is closest to the visitor. If you run a static site or a headless setup like our stack in deploying a React SPA with StackPath CDN, the edge ends up hanging onto your HTML, CSS, and hashed JS chunks.

If you release new code without clean cache invalidation, visitors end up trapped with mismatched files—a new index.html trying to load old script bundles that no longer exist, or an old HTML file loading stale scripts. Logging into the StackPath portal to purge by hand works fine if you ship once a month. If you ship multiple times a day, someone’s going to forget.

StackPath has a straightforward REST API for CDN edge services. A quick curl request in GitHub Actions takes care of the whole thing: the build completes, the CDN dumps its stale cache in about a second, and the next visitor gets the actual new build.

Generate StackPath API Credentials

StackPath uses an OAuth2 client credentials flow for API requests. You can’t use your portal login credentials here; you need a dedicated API Client configured for CDN access.

  1. Log into the StackPath Control Portal.
  2. Click your account name up in the top right corner and head to API Management.
  3. On the API Clients tab, click Generate Credentials.
  4. Give it a descriptive name like github-actions-cache-purge.
  5. Copy both the Client ID and the Client Secret right then and there.

StackPath only shows that Client Secret once. If you close the modal before copying it to your clipboard, you’ll have to delete the credential and generate a fresh pair.

Grab Your Stack ID and Site ID

StackPath nests sites inside Stacks. To hit the purge endpoint, the API expects both your stack_id and your site_id. If you try passing your actual domain name into the endpoint instead of these UUIDs, you’ll get a 404 Not Found every time.

The easiest place to find both is right in your browser’s address bar while viewing the site in the StackPath portal:

  1. In the portal, click CDN in the left sidebar.
  2. Click the site you want to purge automatically.
  3. Look at the current URL in your browser. It looks like this:

The URL layout lays out both IDs plainly:

https://control.stackpath.com/stacks/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d/cdn/sites/f9e8d7c6-b5a4-3210-fedc-ba9876543210/overview

The string right after /stacks/ is your Stack ID. The string right after /sites/ is your Site ID. Copy both UUIDs onto your scratchpad with your Client ID and Secret.

Store Secrets in Your GitHub Repository

Don’t commit these API keys or UUIDs into your workflow YAML. If your repo is public, scraping bots will pick them up almost instantly. If it’s private, they still dirty your git history. Instead, use GitHub Actions encrypted secrets.

  1. Open your repo on GitHub.
  2. Go to Settings > Secrets and variables > Actions.
  3. Click New repository secret and add these four entries:
  • STACKPATH_CLIENT_ID: The Client ID generated in the portal.
  • STACKPATH_CLIENT_SECRET: The matching Client Secret.
  • STACKPATH_STACK_ID: The UUID for your stack.
  • STACKPATH_SITE_ID: The UUID for your CDN site.

GitHub automatically redacts these values in run logs, so you won’t accidentally leak tokens in your workflow console output.

Write the GitHub Actions Workflow File

The workflow does two things: it requests a temporary OAuth2 Bearer token from StackPath’s auth endpoint, then fires a POST request to the CDN purge endpoint using curl and jq.

Drop this into your repo at .github/workflows/purge-cdn.yml:

name: Purge StackPath CDN Cache
on:
push:
branches:
- main
workflow_dispatch:
jobs:
purge-cache:
runs-on: ubuntu-latest
steps:
- name: Request StackPath OAuth Token
id: auth
run: |
RESPONSE=$(curl -s -X POST https://gateway.stackpath.com/identity/v1/oauth2/token 
-H "Content-Type: application/json" 
-d '{ "client_id": "'"$STACKPATH_CLIENT_ID"'", "client_secret": "'"$STACKPATH_CLIENT_SECRET"'", "grant_type": "client_credentials" }')
TOKEN=$(echo "$RESPONSE" | jq -r .access_token)
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ];
then
echo "::error::Failed to obtain StackPath access token"
echo "API Response: $RESPONSE"
exit 1
fi
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
env:
STACKPATH_CLIENT_ID: $ { { secrets.STACKPATH_CLIENT_ID }
}
STACKPATH_CLIENT_SECRET: $ { { secrets.STACKPATH_CLIENT_SECRET }
}
- name: Send Purge Request to Edge Nodes
run: |
PURGE_RESPONSE=$(curl -s -w "nHTTP_STATUS:%{http_code}" 
-X POST https://gateway.stackpath.com/cdn/v1/stacks/$ { { secrets.STACKPATH_STACK_ID }
}
/sites/$ { { secrets.STACKPATH_SITE_ID }
}
/purge 
-H "Authorization: Bearer ${{ steps.auth.outputs.token }}" 
-H "Content-Type: application/json" 
-H "Accept: application/json" 
-d '{ "items": [ { "recursive": true } ] }')
BODY=$(echo "$PURGE_RESPONSE" | sed -e 's/HTTP_STATUS:.*//g')
STATUS=$(echo "$PURGE_RESPONSE" | tr -d 'n' | sed -e 's/.*HTTP_STATUS://')
echo "HTTP Status Code: $STATUS"
echo "Response Body: $BODY"
if [ "$STATUS" -ne 200 ] && [ "$STATUS" -ne 201 ];
then
echo "::error::StackPath purge failed with HTTP status $STATUS"
exit 1
fi
echo "Edge cache successfully purged across all nodes."

I included workflow_dispatch alongside the push trigger, following standard GitHub Actions workflow syntax. That lets you test the purge manually from the Actions tab without pushing a junk commit just to verify your secrets work.

Selective Path Purging vs Full Site Purge

The workflow above sends {"recursive": true}, which invalidates every single cached asset across your entire site. On small static sites or blogs, that’s fine. But on sites with heavy traffic or huge media libraries, wiping everything can trigger a cache stampede that hammers your origin server all at once.

Setting up Origin Shield in StackPath CDN gives you a buffer against that, but purging selectively is still better practice. StackPath accepts specific paths, files, and wildcards in the items array.

Here’s how to target only your compiled scripts and your homepage while leaving images alone:

{ "items": [ { "path": "/index.html" }, { "path": "/assets/*" }, { "path": "/wp-content/themes/my-theme/style.css" } ]
}

Targeting specific paths keeps your heavy assets—like hero images and webfonts—warm on the edge nodes so your origin server doesn’t take an unnecessary beating.

What Broke: Common API Gotchas and Token Expiry

The first time I set this up, my Action showed green checkmarks across the board, but the edge nodes were still serving stale assets. Here are three gotchas that tripped me up:

1. The 401 Unauthorized Error

If your token request passes but the purge request fails with {"code":16,"message":"Unauthenticated"}, your header syntax is likely off. Make sure the Authorization header has the exact literal Bearer prefix followed by your token variable. Dropping that single space between Bearer and the token will reject the request every time.

2. Malformed JSON Body (400 Bad Request)

StackPath requires the items array to contain objects, not plain strings. If you send a simple string array like ["/assets/*"], the API fails with this validation error:

{ "code": 3, "message": "invalid items[0] input"
}

Every path entry has to be an explicit object: {"path": "/assets/*"}.

3. Shell Quoting Issues with Secrets

Interpolating secrets directly inside an inline bash string can break if the secret contains characters that bash tries to expand. Passing them through the step’s env block like we did above prevents bash from mangling the tokens.

Verify That the Purge Actually Succeeded

Don’t just assume an HTTP 200 means your files dropped from edge cache. Verify it from your terminal by checking the response headers on an asset:

curl -I https://example.com/assets/app.js

Check the headers StackPath sends back:

HTTP/2 200
date: Wed, 24 Oct 2024 14:15:32 GMT
content-type: application/javascript
x-cache: MISS
x-sp-edge: ord-edge-01
age: 0
cache-control: public, max-age=86400

Look for x-cache: MISS. Right after a purge, the edge shouldn’t have the file cached. It hits your origin, returns the fresh file, and reports age: 0. Run that same curl command a second later and you’ll see x-cache: HIT with the age counter ticking up. If you manage a CMS and want to keep admins from fighting CDN rules entirely, check out our guide on how to bypass StackPath CDN cache for logged-in users.

For more details on caching headers and directives, consult the MDN Cache-Control specifications.

Chaining the Purge After Auto-Deployment

Purging only works if it happens after your origin server has finished updating. If GitHub Actions purges StackPath before your hosting environment finishes pulling code or running its build script, the edge will just re-fetch and re-cache the old version.

If you’re using webhooks or git hooks on your host—similar to what we covered in Git auto-deployment on Hostinger—run the purge after your deployment command completes, or add a short pause step.

- name: Deploy Code via SSH
run: |
ssh -o StrictHostKeyChecking=no $ { { secrets.DEPLOY_USER }
}
@$ { { secrets.DEPLOY_HOST }
}
"cd public_html && git pull origin main"
- name: Wait for disk write
run: sleep 3
- name: Send Purge Request to Edge Nodes
# Purge step runs here only after deploy succeeds
run: ...

A simple 15-second buffer ensures the host finishes writing files to disk and clearing opcache before StackPath comes knocking again.

Frequently Asked Questions

Does purging the CDN cache cost money on StackPath?

No. StackPath includes API and dashboard purges at no extra charge. But if you run full-site purges constantly on high-traffic sites, you’ll see higher origin bandwidth costs because the edge has to re-pull everything from your server.

Can I purge wildcards with the StackPath API?

Yes. You can pass wildcards like /wp-content/themes/* in the path key. StackPath invalidates all matching edge files across its global PoPs.

How long does it take for a StackPath API purge to propagate?

In practice, it usually takes between 150 milliseconds and two seconds for the invalidation to hit all edge locations.

Why did my purge step fail with a 404 Site Not Found error?

You probably passed your domain name (like example.com) into the STACKPATH_SITE_ID secret instead of the UUID found in the portal URL.

Once you have this workflow in place, you won’t have to think about cache invalidation during deploys. Ship your changes, let the pipeline clear the edge nodes, and your visitors will see the updates right away.

all_in_one_marketing_tool