Michał Miler
Michał Miler
Senior Software Engineer

Passing Body Data in Strapi Webhooks: Triggering GitHub Workflows Example

Nov 27, 20246 min read

Introduction

Strapi is a powerful headless CMS that allows developers to create webhooks for various integrations. However, a common limitation is the inability to set the body of the request in the webhook. This can be a significant hurdle when trying to trigger external services that require specific payloads, such as GitHub Actions.

In this guide, we'll demonstrate how to overcome this limitation by using Strapi as a proxy server to trigger GitHub workflows. This tutorial is based on Strapi v5 and it’s assumed a Strapi application is already set up. If you're using Strapi v4, the setup is slightly different, and we'll cover those differences as well.

Overview

The solution involves creating a custom route in Strapi that acts as a proxy. This route will take query parameters and convert them into a request body, which is then sent to the GitHub API to trigger a workflow. Here's a high-level overview of the process.

high-level-process-overview.png

While our example focuses on GitHub Actions integration, this proxy pattern can be adapted for various webhook customizations. The same approach can be used to:

  • Transform webhook payloads for different HTTP methods (e.g. PUT, PATCH, DELETE)
  • Add custom headers or authentication mechanisms
  • Modify request bodies for other third-party service integrations
  • Implement request validation or transformation logic

For example, if your service requires a PUT request instead of POST, you can simply modify the proxy endpoint's method and adjust the payload structure accordingly. This makes our solution a flexible template for any webhook customization needs.

Setting Up GitHub

First, ensure you have a GitHub workflow set up in your repository. The workflow should be configured to allow manual triggering using workflow_dispatch. Here's an example workflow file.

# .github/workflows/hello-strapi.yml name: Hello Strapi Workflow on: repository_dispatch: types: [strapi_triggers_github_workflow] jobs: say-hello: runs-on: ubuntu-latest steps: - name: Print greeting run: echo "Hello, Strapi!"

This workflow uses the repository_dispatch event trigger so say-hello job (printing “Hello, Strapi”!) will be started on strapi_triggers_github_workflow event type. More about creating repository dispatch events can be found in the GitHub API documentation.

You'll also need a Personal Access Token (PAT) to authenticate the request from Strapi to GitHub. This can be created in GitHub under Settings → Developer settings → Personal access tokens. Learn more about PATs in the GitHub documentation.

Ready to streamline your development process?

Our experts design efficient CI/CD workflows tailored to tools like Strapi, GitHub Actions, GitLab, AWS CodePipeline, or Jenkins. Let’s talk about your pipeline needs!

A person sitting and typing on a laptop keyboard

Setting Up Strapi

1. Set up environment variables

Our implementation requires several environment variables to manage the connection between Strapi and GitHub, including the GitHub Personal Access Token and repository details. Create a .env file based on the provided example:

# .env # Strapi Server HOST=0.0.0.0 PORT=1337 # GitHub GITHUB_PAT=github_pat_{TOKEN} GITHUB_URL=https://api.github.com/repos/{OWNER}/{REPO} # e.g. https://api.github.com/repos/uninterrupted-tech/strapi-webhook GITHUB_EVENT_TYPE=strapi_triggers_github_workflow

⚠️ WARNING

Never commit your actual .env file to version control system. Always use .env.example as a template.

2. Install Koa Types

Since Strapi is built on Koa, we’ll simply extend the server to reach our goal. For better code maintainability and DX (developer experience) we’ll use typing so let’s start by installing the Koa types:

npm i --save-dev @types/koa

3. Create a Custom Route and Controller

Now we can create a new route and controller in Strapi in order to set up a new proxy endpoint. In our example, the endpoint will handle the GitHub API call. This involves creating four files:

  • Shared config file: src/config.ts
  • GitHub auth builder util: src/util/get-github-auth.ts
  • Route definition: src/api/github/routes/trigger-pipeline.ts
  • Controller: src/api/github/controllers/trigger-pipeline.ts
// src/config.ts if ( !( process.env.GITHUB_URL && process.env.GITHUB_PAT && process.env.GITHUB_EVENT_TYPE ) ) { throw new Error( "Missing GITHUB environment variables. Please check `.env.example` file." ); } export const CONFIG = { PORT: process.env.PORT || "1337", GITHUB: { URL: process.env.GITHUB_URL, PAT: process.env.GITHUB_PAT, EVENT_TYPE: process.env.GITHUB_EVENT_TYPE, }, } as const;
// src/util/get-github-auth.ts import { CONFIG } from "../config"; export const getGithubAuth = () => `Bearer ${CONFIG.GITHUB.PAT}`;
// src/api/github/routes/trigger-pipeline.ts export default { routes: [ { method: "POST", path: "/github", handler: "trigger-pipeline.post", config: { auth: false, policies: [], middlewares: [], }, }, ], };
// src/api/github/controllers/trigger-pipeline.ts import { Context, Next } from "koa"; import { getGithubAuth } from "../../../util"; import { CONFIG } from "../../../config"; export default { post: async (ctx: Context, next: Next) => { const auth = ctx.request.headers["x-authorization"]; if (auth !== getGithubAuth()) { ctx.response.status = 403; return next(); } const headers = new Headers({ Accept: "application/vnd.github.everest-preview+json", Authorization: auth, }); const requestParams = new URLSearchParams(ctx.request.search); const event_type = requestParams.get("event_type"); if (!event_type || event_type !== CONFIG.GITHUB.EVENT_TYPE) { ctx.response.status = 400; return next(); } const body = { event_type, }; try { await fetch(`${process.env.GITHUB_URL}/dispatches`, { method: "POST", headers, body: JSON.stringify(body), }); ctx.response.status = 200; return next(); } catch (err) { ctx.body = err; ctx.response.status = 500; return next(); } }, };

The flow of our implementation works as follows:

  1. Strapi webhook makes a request to our proxy endpoint
  2. The proxy endpoint validates the authentication token
  3. It extracts the event type from query parameters
  4. Transforms the request into the format GitHub expects
  5. Forwards the request to GitHub's API
  6. GitHub receives the request and triggers the workflow

4. Set Up the Webhook

While Strapi provides a user interface for webhook management (accessible via Admin Panel → Settings → Webhooks), maintaining webhooks through the UI can become cumbersome, especially in multi-environment setups. Instead, we'll take a programmatic approach by managing our webhook configuration directly in the codebase. This approach ensures that your webhook configuration remains version-controlled and automatically synchronized with your application deployments.

Our implementation will:

  • Automatically remove any existing webhook with the same name
  • Create a fresh webhook with the latest configuration
  • Ensure consistent webhook setup across all environments
  • Support both Strapi v4 and v5 implementations

💡 Version

Note that Strapi v4 and v5 use different methods to access the webhook store. The code below includes version detection to handle these differences.

// src/util/set-up-github-webhook.ts import type { Core } from "@strapi/strapi"; import { getGithubAuth } from "./get-github-auth"; import { CONFIG } from "../config"; const name = "GitHub Action"; export const setUpGithubWebhook = async (strapi: Core.Strapi) => { const webhookStore = "webhookStore" in strapi ? strapi.webhookStore // v4 : await strapi.get("webhookStore"); // v5 try { const webhooks = await webhookStore.findWebhooks(); const oldWebhook = webhooks.find((webhook) => webhook.name === name); if (oldWebhook) { await webhookStore.deleteWebhook(oldWebhook.id); } } catch (error) { console.error(`Unable to prepare "${name}" webhook`, error); } try { await webhookStore.createWebhook({ id: "", events: [], headers: { "x-authorization": getGithubAuth(), }, isEnabled: true, name, url: `http://localhost:${CONFIG.PORT}/api/github?event_type=${CONFIG.GITHUB.EVENT_TYPE}`, }); } catch (error) { console.error(`Unable to create "${name}" webhook`, error); } };
// src/index.ts import { Core } from "@strapi/strapi"; import { setUpGithubWebhook } from "./util"; export default { register() {}, async bootstrap({ strapi }: { strapi: Core.Strapi }) { await setUpGithubWebhook(strapi); }, };

Testing the Implementation

Let's verify that our webhook setup works correctly…

1. Server Startup Verification

When you start your Strapi server, you should see confirmation logs in your terminal:

GitHub Action does not exist yet. GitHub Action webhook created.

These logs confirm that our programmatic webhook setup is working as expected.

2. Webhook UI Verification

Navigate to your Strapi Admin Panel → Settings → Webhooks. You should see our "GitHub Action" webhook listed with:

  • The configured endpoint URL
  • Empty events list (since we're using manual triggers)
  • Status showing as "Enabled"

strapi-webhooks-ui.png

3. Trigger Test

Now for the exciting part! Click the "Trigger" button next to the webhook in the Strapi admin panel. This should:

  • Send a request to our proxy endpoint
  • Forward the request to GitHub
  • Trigger our workflow in GitHub Actions

github-actions-workflow-1.png

github-actions-workflow-2.png

💡 TIP

If the trigger fails, check your config (.env file) browser's network tab and Strapi logs for any error messages. Common issues include incorrect PAT configuration or mismatched event types.

Security Considerations

You might notice that we set auth: false in our route configuration. While this disables Strapi's built-in authentication, we're not leaving our endpoint unsecured. Instead, we implement a custom authentication mechanism using the x-authorization header. This header must contain a valid GitHub Personal Access Token (PAT), which serves two purposes:

  • It ensures that only authorized requests can trigger our GitHub workflows
  • It provides the necessary authentication for the GitHub API call

Here's how the security flow works:

  • Strapi webhook includes the PAT in the x-authorization header
  • Our controller validates this token against our stored PAT
  • If the tokens don't match, the request is rejected with a 403 status
  • Only if authentication succeeds does the controller proceed with the GitHub API call

This approach provides robust security while maintaining flexibility in our implementation. The same PAT is then reused for the GitHub API call, ensuring proper authorization throughout the entire flow.

Conclusion

By following this guide, you can effectively use Strapi to trigger GitHub workflows by passing the necessary request body through a custom proxy route. This approach leverages Strapi's flexibility and Koa's middleware capabilities to overcome the limitations of Strapi's native webhook functionality. For more detailed information on Strapi webhooks, refer to the Strapi documentation. For GitHub API details, see the GitHub API documentation.

The complete working example of this implementation is available in our public GitHub repository

RELATED POSTS
Kacper Drzewicz
Kacper Drzewicz
Senior Software Engineer

Manage User Cookie Consent with Google Tag Manager: Adapting to CookieConsent v3

May 23, 20243 min read
Article image
Tomasz Fidecki
Tomasz Fidecki
Managing Director | Technology

Templating Values in Kustomize: Unlocking the Potential of Dynamic Naming for Kubernetes Resources

Mar 13, 20247 min read
Article image
Tomasz Fidecki
Tomasz Fidecki
Managing Director | Technology

Maximizing Efficiency with Dev Containers: A Developer's Guide

Feb 22, 202419 min read
Article image