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_added or testflight_upload_completed Release Management events?
  • 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 (default Authorization) and look it up case-insensitively.
  • Expected value: BITRISE_WEBHOOK_SECRET, the exact whole header value. If you entered Bearer abc123, store Bearer abc123. Don't strip or parse a Bearer prefix.
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON object, 200 once 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_eventSent When
app_version_addedAn app version is added to a release
testflight_upload_startedA TestFlight upload starts
testflight_upload_completedA TestFlight upload completes
testflight_upload_failedA TestFlight upload fails
testflight_processing_completedTestFlight finishes processing the build
testflight_processing_failedTestFlight processing fails
testflight_app_review_approvedA TestFlight app review is approved
testflight_app_review_rejectedA TestFlight app review is rejected
google_play_upload_completedA Google Play upload completes
google_play_upload_failedA Google Play upload fails
app_store_review_approvedAn App Store review is approved
app_store_review_rejectedAn App Store review is rejected
app_store_release_completedAn App Store release completes
google_play_release_completedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →