Agent skill
Bitrise Webhooks Skill
Receive and authenticate Bitrise outgoing webhooks. Use when setting up a Bitrise webhook endpoint, checking a secret custom header, debugging 401s, or handling CI build events and Release Management events like app_version_added, testflight_upload_completed and google_play_upload_completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill bitrise-webhooks
Bitrise (mobile CI/CD) can send an outgoing webhook when a CI build starts or ends, and when Release Management events happen (a version is added, a TestFlight or Google Play upload completes, a store review is approved, and so on). Webhooks are not signed. To authenticate them, add a secret custom header to the outgoing webhook's Headers section and compare it with the value you stored.
When to Use This Skill
- How do I receive Bitrise webhooks?
- Does Bitrise sign webhook payloads? (No: use a secret custom header)
- How do I authenticate a Bitrise outgoing webhook with a custom header?
- How do I handle
app_version_addedortestflight_Release Management events?upload_ completed - How do I tell a Bitrise CI build webhook from a Release Management webhook?
- Why does my Bitrise webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyBitriseRequest(
headers,
secret = process.env.BITRISE_WEBHOOK_SECRET,
headerName = process.env.BITRISE_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // Node lowercases header names
return Boolean(received) && safeEqual(received, secret); // WHOLE value, no prefix parsing
}
- Header name: whatever you typed in the webhook's Headers section. The examples read it from
BITRISE_WEBHOOK_HEADER(defaultAuthorization) and look it up case-insensitively. - Expected value:
BITRISE_WEBHOOK_SECRET, the exact whole header value. If you enteredBearer abc123, storeBearer abc123. Don't strip or parse aBearerprefix. - Return
401for a missing header or a mismatch,400for a body that isn't a JSON object,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - This is a static shared credential. It proves the request came from someone who knows the value; it does not authenticate the body and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the header value if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Release Management webhooks carry a trigger_event string. A selection:
trigger_event | Sent When |
|---|---|
app_version_added | An app version is added to a release |
testflight_ | A TestFlight upload starts |
testflight_ | A TestFlight upload completes |
testflight_upload_failed | A TestFlight upload fails |
testflight_ | TestFlight finishes processing the build |
testflight_ | TestFlight processing fails |
testflight_ | A TestFlight app review is approved |
testflight_ | A TestFlight app review is rejected |
google_ | A Google Play upload completes |
google_ | A Google Play upload fails |
app_ | An App Store review is approved |
app_ | An App Store review is rejected |
app_ | An App Store release completes |
google_ | A Google Play release completes |
Every Release Management body also has release_id, app_id, triggered_by, triggered_at, sent_at and webhook_event_unique_id (usable as an idempotency key). The full list is in references/overview.md.
CI build events (build started, build finished) have no documented payload: Bitrise publishes no schema, field names or type field for them. Treat a body without trigger_event as undocumented (the examples acknowledge it and log its field names) and inspect a real delivery under Recent deliveries to see exactly what your project sends before relying on any field.
Environment Variables
BITRISE_WEBHOOK_HEADER=Authorization # header name you added to the outgoing webhook
BITRISE_WEBHOOK_SECRET=your-header-value # the exact, whole value of that header
Local Development
Bitrise can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 bitrise --path /webhooks/bitrise
Use the printed URL as the outgoing webhook URL in Bitrise.
Resources
- references/overview.md - What Bitrise webhooks are, events and payload shape
- references/setup.md - Add an outgoing webhook and a secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests