Guide to Bitrise Webhooks: Features and Best Practices
Bitrise outgoing webhooks tell other systems what is happening in your mobile CI/CD pipeline: a build has started, a build has finished, a version has been uploaded to TestFlight, an App Store review has been approved. If you post build results to a chat tool, drive an in-house release dashboard, or kick off downstream steps when a store submission changes state, webhooks let those services hear about it without polling the Bitrise API.
This guide covers the two families of Bitrise webhooks, what Bitrise does and doesn't document about their payloads, how to authenticate requests that Bitrise doesn't sign, and the best practices for a production receiver.
What are Bitrise webhooks?
Bitrise sends HTTP POST requests to a URL you register. There are two families, configured in different places:
- CI build events. An outgoing webhook on a project fires when a build is started and when it ends. You can pick Send me everything or Select individual events. Bitrise says that, for now, only build events are supported here.
- Release Management events. Release Management, Bitrise's app-store release product, reuses the same outgoing webhook configurations. You attach a webhook to a release or a release preset and choose which release automation events trigger it. Connecting webhooks in a Release Management project needs a standard license.
Neither family is signed. The only authentication is a custom header you add to the webhook, which Bitrise suggests for tracking requests "with API keys set as a custom header". Your receiver compares that header against the value you stored.
Bitrise webhook features
| Feature | Details |
|---|---|
| Configuration | Project settings > Integrations > Webhooks > Outgoing Webhooks, or POST /apps/{app-slug}/outgoing-webhooks |
| Permissions | Project Admin, or a custom role with Edit workflows |
| API event values | all, build and pipeline |
| CI build events | Build started and build ended. No body schema or type field is published |
| Release Management events | 68 documented trigger_event values, chosen per release or release preset |
| Body format | JSON |
| Authentication | Custom headers you define. Values can be hidden after saving |
| Signature | None |
| Deliveries | Recent deliveries shows each request, response and status code, with manual redelivery |
| Retries and timeout | Not documented |
| Source IPs | Not published for webhook delivery |
Common events
Release Management webhooks carry the event name in trigger_event. Bitrise documents 68 of them. Some of the ones release tooling usually needs:
trigger_event | Fires when |
|---|---|
app_version_added | An app version is added |
release_ | A build is selected as the release candidate |
testflight_ | A TestFlight upload completes |
testflight_upload_failed | A TestFlight upload fails |
testflight_ | A TestFlight app review is approved |
google_ | A Google Play upload completes |
google_ | A Google Play upload fails |
approval_task_completed | An approval task is completed |
app_ | An App Store review is rejected |
app_ | An App Store release completes |
google_ | A Google Play release completes |
Watch for the one irregular spelling: auto-upload_enabled and auto-upload_disabled use a hyphen where every other name uses underscores.
Every Release Management body contains these common fields, plus fields that depend on the event:
| Field | Notes |
|---|---|
trigger_event | The event name |
app_id, project_id, workspace_id | Identifiers |
release_id, release_, store_app_id | Release identifiers |
triggered_by | A user slug, or Automation, Apple App Store or Google Play Store |
triggered_at, sent_at | Nanosecond-precision UNIX timestamps |
webhook_config_id | The outgoing webhook configuration |
webhook_event_unique_id | WEBHOOK_ |
Upload, review and release-candidate events add file_name, version and version_code. app_ also adds reason. TestFlight review and distribution events add testflight_beta_build_id. Bitrise documents these as field tables only, with no example body, so check the nesting of the extra fields against a real delivery.
CI build webhooks are less well documented. Bitrise describes a build event as a build being started or ending, and publishes no body, no event type field and no Bitrise-set headers for them. Open Recent deliveries on the webhook to see exactly what your project sends, and tell the two families apart by the presence of trigger_event.
Inspect Bitrise webhooks as they arrive. Point Bitrise at a Hookdeck Inspect URL to inspect and replay real deliveries, with no account or setup required. It's a quick way to see the CI build body that Bitrise doesn't document.
Setting up Bitrise webhooks
Pick a header name and a long random value first (openssl rand -hex 32 works), and store the value in your app's environment.
- Open your project on Bitrise as an Admin, or with a custom role that has Edit workflows.
- Click Project settings, select Integrations, then the Webhooks tab.
- Scroll to Outgoing Webhooks and click Add webhook.
- Enter your HTTPS endpoint in URL.
- Choose Send me everything or Select individual events.
- In Headers, enter your header name (for example
X-Webhook-Secret) and the secret value, then click Add header. - Click Create webhook.
If you tick Hide next to the header, you can't view or change its value again, so keep your own copy before saving.
The API equivalent takes the same headers as a map:
curl -X POST "https://api.bitrise.io/v0.1/apps/APP-SLUG/outgoing-webhooks" \
-H "Authorization: ACCESS-TOKEN" \
-H "Content-Type: application/json" \
-d '{"events": ["build"], "url": "https://example.com/webhooks/bitrise", "headers": {"X-Webhook-Secret": "your-long-random-secret"}}'
When you update a webhook through the API, send every mandatory field again, including events, even if you're only changing the URL.
For Release Management, create the outgoing webhook as above first. Then open Release Management, select your app and a release that hasn't finished (or Release presets for a template), go to Configuration, open the Outgoing webhooks card with Show details, click Add webhook, choose the configuration from Webhook configuration, and select the events.
For local development, use the Hookdeck CLI: hookdeck listen 3000 bitrise --path /webhooks/bitrise gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as the webhook URL, then start a build.
With Hookdeck. Create a Bitrise source in Hookdeck, enter the Header Name and the Header Value exactly as you set them on the webhook, including any Bearer prefix, and use the source URL as the webhook URL.
Securing Bitrise webhooks
Authenticating a Bitrise request means comparing your custom header against the stored value. Look the header up case-insensitively, compare the whole value (prefix included) in constant time, and check lengths before calling crypto.timingSafeEqual, which throws on buffers of different lengths.
const crypto = require('crypto');
const express = require('express');
const app = express();
function safeEqual(a, b) {
const x = Buffer.from(String(a));
const y = Buffer.from(String(b));
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function verifyBitriseRequest(headers) {
const secret = process.env.BITRISE_WEBHOOK_SECRET;
if (!secret) return false; // fail closed when unconfigured
const name = (process.env.BITRISE_WEBHOOK_HEADER || 'X-Webhook-Secret').toLowerCase();
const received = headers[name];
return typeof received === 'string' && safeEqual(received, secret);
}
app.post('/webhooks/bitrise', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyBitriseRequest(req.headers)) {
return res.sendStatus(401);
}
let body;
try {
body = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
res.sendStatus(200);
if (typeof body.trigger_event === 'string') {
// Release Management: route on body.trigger_event,
// deduplicate on body.webhook_event_unique_id
} else {
// CI build event: no documented type field, so inspect the body
}
});
The same check in Python:
import hmac
import os
def verify_bitrise_request(headers) -> bool:
secret = os.environ.get("BITRISE_WEBHOOK_SECRET")
if not secret:
return False
name = os.environ.get("BITRISE_WEBHOOK_HEADER", "X-Webhook-Secret")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))
A static header proves the sender knows the secret, but not that the body is unchanged, and it offers no replay protection: nothing is signed and Bitrise documents no timestamp header. Serve the endpoint over HTTPS only, keep the secret out of logs and source control, and rotate it if it might have leaked. Bitrise publishes no webhook egress IPs, so IP allowlisting isn't an option either. For services behind a firewall, Bitrise recommends making the request from inside the build instead.
Make Bitrise webhooks production-ready. Hookdeck Event Gateway checks your secret header on every request, durably queues each build and release event, and adds automatic retries and replay on top of Bitrise's manual redelivery.
Bitrise webhook limitations and pain points
No signature on any request
The Problem: Bitrise requests carry no signature. A custom header you invented is the only thing separating a real build notification from a forged one.
Why It Happens: Bitrise outgoing webhooks offer custom headers and nothing else for authentication.
Workarounds:
- Use a long random header value and compare it in constant time.
- Rotate the value by editing the webhook, then update your receiver.
- Treat the payload as a notification and confirm anything important through the Bitrise API.
How Hookdeck Can Help: Hookdeck's Bitrise source compares the header you name against the exact value you enter. A request without the right header still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination. Every request is logged with its full headers and body.
Undocumented build payloads
The Problem: You can't write a CI build handler from the docs alone, because Bitrise publishes no body or type field for build started and build finished events.
Why It Happens: The Release Management page documents its fields as tables, but the CI pages only describe when build events fire.
Workarounds:
- Capture a real delivery from Recent deliveries and build your parser from it.
- Branch on the presence of
trigger_eventto separate Release Management events from build events. - Read fields defensively and log bodies you don't recognise.
How Hookdeck Can Help: Hookdeck stores every request with its headers and body, so you can capture a build-started and build-finished pair once and replay them against your handler while you develop it.
Recovery is manual
The Problem: Bitrise doesn't document automatic retries or a response timeout. If your endpoint is down, you may only find out by checking Recent deliveries and clicking redeliver.
Why It Happens: The deliveries view records the status code of each attempt and lets you resend it, but the docs say nothing about what Bitrise does on its own after a failure.
Workarounds:
- Return
200quickly so slow processing doesn't count as a failure. - Check Recent deliveries after any outage and redeliver what failed.
- Reconcile build state from the Bitrise API for anything critical.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with automatic retries on a schedule you control. Issues alert you when deliveries fail, and any stored event can be replayed.
Duplicates from redelivery
The Problem: A manual redelivery, or any retry Bitrise makes, sends the same event again.
Why It Happens: Redelivery resends the original payload, and nothing in the request marks it as a repeat.
Workarounds:
- For Release Management, deduplicate on
webhook_event_unique_id, which Bitrise builds from the webhook ID and the trigger timestamp. - For build events, derive a key from fields you see in a captured delivery.
How Hookdeck Can Help: Hookdeck deduplication can drop repeated deliveries based on a field such as webhook_event_unique_id before they reach your handler.
Best practices
Keep a copy of hidden header values
A hidden header can't be viewed or edited later. Store the value in your secrets manager before you tick Hide, or you'll have to replace the header to rotate it.
Route on trigger_event, and expect two shapes
Release Management bodies have trigger_event. Build bodies don't have a documented type field. Check for trigger_event first, then handle build events as a separate shape.
Watch the timestamp units
triggered_at and sent_at are UNIX timestamps with nanosecond precision. Divide by 1,000,000 before passing them to JavaScript's Date, and use a type that won't lose precision if you store them.
Acknowledge fast and process asynchronously
Return 200 as soon as the header checks out and hand the work to a queue. Bitrise doesn't document its timeout, so don't depend on it being generous. See why to process webhooks asynchronously.
Make handlers idempotent
Store webhook_event_unique_id before you act and skip repeats. See our guide to webhook idempotency.
Conclusion
Bitrise sends unsigned JSON POST requests for CI build events and, on standard-license projects, for 68 Release Management events identified by trigger_event. Authenticate them with a custom header compared in constant time over HTTPS, capture a real build delivery before relying on its fields, and deduplicate Release Management events on webhook_event_unique_id.
Because Bitrise documents no automatic retry policy, Hookdeck Event Gateway is a useful buffer: it checks the header, stores every event durably, retries delivery to your service and lets you replay anything that failed.
Get started with Hookdeck for free and handle Bitrise webhooks reliably in minutes.