Trigger Native Builds via Webhook
Copy a setup prompt with the install steps and the full markdown guide for this plugin.
Capgo Build is normally started from a laptop or CI job. Teams that ship from an admin dashboard, CMS, or internal portal often want a single HTTP webhook: press a button in their own UI, and a signed native build starts. This guide shows the standard pattern — a thin authenticated HTTP call into your code host, which runs the same Capgo Build workflow you already use.
Architecture
Section titled “Architecture”sequenceDiagram participant Admin as Admin dashboard participant Hook as Webhook endpoint participant CI as GitHub / GitLab participant Capgo as Capgo Build Admin->>Hook: POST /native-build (secret) Hook->>CI: repository_dispatch / pipeline trigger CI->>CI: checkout, npm ci, cap sync CI->>Capgo: build request Capgo-->>CI: signed binary / store upload
You do not need Capgo to expose a public “build webhook.” Your CI already has the signing secrets; the webhook’s only job is to start that CI job safely.
Prerequisites
Section titled “Prerequisites”- A Capgo Build workflow that already works from the CI UI or on push (GitHub Actions)
- Permission to create a fine-grained GitHub token, GitLab trigger token, or equivalent
- A shared secret your admin dashboard will send (header or body)
Option A — GitHub repository_dispatch (recommended)
Section titled “Option A — GitHub repository_dispatch (recommended)”GitHub accepts an authenticated API call that starts a workflow listening for repository_dispatch. Any dashboard that can POST JSON can fire it.
1. Workflow that listens for the webhook
Section titled “1. Workflow that listens for the webhook”Validate the payload before checkout and before any secret-bearing step. Pass accepted values through job outputs / env vars — never interpolate client_payload directly into run: scripts (script injection guidance).
name: Capgo Build (Webhook)
on: repository_dispatch: types: [capgo-native-build]
jobs: validate: runs-on: ubuntu-latest outputs: platform: ${{ steps.check.outputs.platform }} mode: ${{ steps.check.outputs.mode }} ref: ${{ steps.check.outputs.ref }} platforms_json: ${{ steps.check.outputs.platforms_json }} steps: - id: check env: RAW_PLATFORM: ${{ github.event.client_payload.platform }} RAW_MODE: ${{ github.event.client_payload.mode }} RAW_REF: ${{ github.event.client_payload.ref }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | PLATFORM="${RAW_PLATFORM:-android}" MODE="${RAW_MODE:-release}" REF="${RAW_REF:-$DEFAULT_BRANCH}" case "$PLATFORM" in ios|android|both) ;; *) echo "Invalid platform: $PLATFORM" >&2; exit 1;; esac case "$MODE" in debug|release) ;; *) echo "Invalid mode: $MODE" >&2; exit 1;; esac # Allowlist branches / tags / full SHAs only if [[ ! "$REF" =~ ^(main|master|production|release/[A-Za-z0-9._-]+|[0-9a-f]{40})$ ]]; then echo "Ref not allowlisted: $REF" >&2 exit 1 fi if [ "$PLATFORM" = "both" ]; then PLATFORMS_JSON='["ios","android"]' else PLATFORMS_JSON=$(printf '["%s"]' "$PLATFORM") fi { echo "platform=$PLATFORM" echo "mode=$MODE" echo "ref=$REF" echo "platforms_json=$PLATFORMS_JSON" } >> "$GITHUB_OUTPUT"
build: needs: validate runs-on: ubuntu-latest environment: ${{ needs.validate.outputs.mode == 'release' && 'production' || 'build-debug' }} strategy: fail-fast: false matrix: platform: ${{ fromJSON(needs.validate.outputs.platforms_json) }} steps: - uses: actions/checkout@v4 with: ref: ${{ needs.validate.outputs.ref }}
- uses: actions/setup-node@v6 with: node-version: '24' cache: 'npm'
- run: npm ci - run: npm run build - name: Sync native project env: PLATFORM: ${{ matrix.platform }} run: npx cap sync "$PLATFORM"
- name: Capgo Build env: CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }} BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} CAPGO_IOS_PROVISIONING_MAP: ${{ secrets.CAPGO_IOS_PROVISIONING_MAP }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }} ANDROID_KEYSTORE_FILE: ${{ secrets.ANDROID_KEYSTORE_FILE }} KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} PLAY_CONFIG_JSON: ${{ secrets.PLAY_CONFIG_JSON }} PLATFORM: ${{ matrix.platform }} MODE: ${{ needs.validate.outputs.mode }} run: | npx @capgo/cli@latest build request com.example.app \ --platform "$PLATFORM" \ --build-mode "$MODE"2. Create a GitHub token
Section titled “2. Create a GitHub token”Create a fine-grained personal access token (or GitHub App installation token) with Contents: Read and write on the repository (required for repository_dispatch). Store it only in your admin backend — never in the browser.
3. Call the webhook from your admin dashboard
Section titled “3. Call the webhook from your admin dashboard”Your backend (not the end-user browser) should send:
curl -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/OWNER/REPO/dispatches \ -d '{ "event_type": "capgo-native-build", "client_payload": { "platform": "both", "mode": "release", "ref": "main", "requested_by": "admin@example.com" } }'| Field | Purpose |
|---|---|
event_type | Must match types in the workflow (capgo-native-build) |
client_payload.platform | ios, android, or both |
client_payload.mode | debug or release |
client_payload.ref | Branch or tag to build (optional) |
Wire the same JSON POST to a button in your admin UI (“Build native apps”). The dashboard only needs to reach your backend; the backend holds GITHUB_TOKEN.
Option B — Tiny proxy webhook (any host)
Section titled “Option B — Tiny proxy webhook (any host)”If the admin tool can only POST to a URL you control (Zapier, Make, Cloudflare Worker, Express route), put a short proxy in front of GitHub:
// Example Cloudflare Worker / Node handler (sketch)export default { async fetch(request, env) { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }) } if (request.headers.get('x-webhook-secret') !== env.WEBHOOK_SECRET) { return new Response('Unauthorized', { status: 401 }) } const body = await request.json().catch(() => ({})) const platform = body.platform || 'both' const mode = body.mode || 'release' const ref = body.ref || 'main' if (!['ios', 'android', 'both'].includes(platform)) { return new Response('Invalid platform', { status: 400 }) } if (!['debug', 'release'].includes(mode)) { return new Response('Invalid mode', { status: 400 }) } if (!/^(main|master|production|release\/[A-Za-z0-9._-]+|[0-9a-f]{40})$/.test(ref)) { return new Response('Ref not allowlisted', { status: 400 }) } const res = await fetch( `https://api.github.com/repos/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/dispatches`, { method: 'POST', headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${env.GITHUB_TOKEN}`, 'X-GitHub-Api-Version': '2022-11-28', }, body: JSON.stringify({ event_type: 'capgo-native-build', client_payload: { platform, mode, ref }, }), }, ) return new Response(res.status === 204 ? 'Build queued' : await res.text(), { status: res.status === 204 ? 200 : res.status, }) },}Then configure the admin product:
| Setting | Value |
|---|---|
| URL | https://your-worker.example.com/native-build |
| Method | POST |
| Header | x-webhook-secret: <shared secret> |
| Body | { "platform": "both", "mode": "release" } |
This is the usual shape for connecting a webhook to an on-admin dashboard: the dashboard stores one URL and one secret; Capgo credentials stay in GitHub Actions.
GitLab Pipeline Trigger
Section titled “GitLab Pipeline Trigger”GitLab exposes pipeline trigger tokens that are natural webhook targets.
# .gitlab-ci.yml fragmentcapgo_native_webhook: stage: build script: - npm ci && npm run build - npx cap sync "${PLATFORM:-android}" - npx @capgo/cli@latest build request com.example.app --platform "${PLATFORM:-android}" --build-mode "${BUILD_MODE:-release}" rules: - if: '$CI_PIPELINE_SOURCE == "trigger"'Create a trigger token under Settings → CI/CD → Pipeline trigger tokens, then from the admin backend:
curl -X POST \ -F token=$GITLAB_TRIGGER_TOKEN \ -F ref=main \ -F "variables[PLATFORM]=android" \ -F "variables[BUILD_MODE]=release" \ https://gitlab.com/api/v4/projects/PROJECT_ID/trigger/pipelineFor both platforms, either fire two triggers or expand the job into a parallel matrix the same way as the GitHub example.
Bitbucket and Azure
Section titled “Bitbucket and Azure”| Platform | Webhook mechanism |
|---|---|
| Bitbucket | Pipeline trigger URL or custom pipeline + app password POST |
| Azure DevOps | Pipeline run REST API with a PAT; use the manual pipeline from Trigger from CI UI |
Pattern is identical: admin → your secret check → host API → Capgo Build job.
Payload Checklist for Admin Tools
Section titled “Payload Checklist for Admin Tools”When the dashboard form is built, collect at least:
- Platform — ios / android / both
- Mode — debug (QA) or release (store)
- Git ref — branch or tag to build
- Actor — email or user id for audit logs (pass through
client_payload)
Optional: after the run, have CI post back to the admin API with the download URL from --output-record / build last-output.
Security
Section titled “Security”- Authenticate every webhook (
x-webhook-secret, HMAC signature, or mTLS). - Allowlist
platform,mode, andrefin both the proxy and the workflowvalidatejob — treatclient_payloadas untrusted input. - Pass accepted values through env vars / job outputs; do not interpolate payload fields into
run:scripts. - Keep GitHub/GitLab tokens server-side only.
- Prefer tokens scoped to a single repository.
- Rate-limit the proxy; native builds cost build minutes.
- For store-submitting releases, map
releaseto a protected GitHub Environment (as in the sample) or require an extra confirmation flag checked invalidate.