AppForShare

Post new app build links to Slack and Discord automatically

· 6 min read

Post new app build links to Slack and Discord automatically

A new test build is only useful once the people testing it know it exists. Posting the link by hand in a team channel works until someone forgets, or posts the wrong build. There are two ways to automate it: let AppForShare post each build to Slack or Discord for you, or post the message yourself from CI. This article walks through both and when each one makes sense.

Option 1: AppForShare webhooks

Integrations are part of the Pro and Team plans. You add them under Dashboard, Integrations, and there are three kinds:

  • Slack: a Slack incoming webhook URL, which starts with https://hooks.slack.com/services/.
  • Discord: a Discord channel webhook URL, which starts with https://discord.com/api/webhooks/.
  • Webhook: any https endpoint of your own, which receives signed JSON.

Each integration has a name (for example #releases), can cover all projects or a single one, and subscribes to the events you tick. The Send test button delivers a ping event so you can check the channel before the next real build.

Events you can subscribe to

EventWhen it fires
build.readyA build finished processing and its install link works
build.failedAn upload could not be turned into a build
build.installedSomeone started an install (device, OS version, model, country)
bandwidth.warningMonthly bandwidth reached 80% or 100%
store.statusA release changed state on a connected App Store Connect or Google Play account
store.reviewA new customer review arrived on a connected app

New integrations start with build.ready and build.failed ticked, which is what most release channels want. Installs can be noisy on a busy project, so many teams send build.installed to a separate channel or leave it off.

What the message looks like

For Slack and Discord you do not write any formatting. The message shows the app icon, version, platform and size, the release notes, and a button to the install page. For installs it shows the device, OS and country instead. Slack limits incoming webhooks to about one message per second, so on a busy hook messages wait their turn rather than being dropped.

Setting it up in Slack

  1. In Slack, create an app for your workspace (or use an existing one), turn on Incoming Webhooks and add a webhook to the channel you want.
  2. Copy the URL. Treat it like a password: anyone who has it can post to that channel. AppForShare only shows a short hint of it after you save.
  3. In AppForShare, open Dashboard, Integrations, choose Slack, paste the URL, pick the project and events, and add it.
  4. Press Send test.

Setting it up in Discord

  1. In the Discord channel settings, open Integrations, then Webhooks, and create a webhook.
  2. Copy the webhook URL.
  3. Add it in AppForShare as a Discord integration and send a test.

Generic signed webhooks

For anything that is not Slack or Discord, such as an internal bot, Microsoft Teams through your own relay, or a status dashboard, use a generic webhook. Each request is a JSON POST with these headers:

X-AppForShare-Event: build.ready
X-AppForShare-Delivery: 1842
X-AppForShare-Signature: t=1791850000,v1=5f0c...e91a

The body has an id, the event, createdAt and a data object. For build.ready that includes the build's name, platform, bundle id, version, size, installUrl, iconUrl and release notes. The full example is on the webhooks docs page.

The signature is HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the signing secret shown when you add the webhook. Verify it before trusting the payload:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const got = Buffer.from(parts.v1 ?? '', 'hex');
  return got.length === 32 && timingSafeEqual(got, Buffer.from(expected, 'hex'));
}

Two details matter here. Use the raw body exactly as received, before any JSON parsing, or the hash will not match. And reject old timestamps, which stops someone from replaying a captured request.

Delivery rules worth knowing

  • Answer with any 2xx status within 10 seconds. Redirects are not followed.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. A 404 or 410 answer stops retries for that delivery.
  • A retry sends the same body with the same event id, so store ids you have handled and skip duplicates.
  • After 15 failures in a row, the webhook is switched off and you get an email.
  • Only https URLs on port 443 or 8443 that resolve to public addresses are accepted.

A common pattern for a slow handler: verify the signature, save the event, answer 200 at once, and do the real work in the background.

Option 2: post from CI yourself

If you upload builds from a pipeline, you can post the message in the same job. The AppForShare CLI prints the result as JSON with --json, so you can build any message you like:

npx --yes appforshare@1 upload app-release.apk --notes "$NOTES" --json > afs.json
URL=$(node -p "require('./afs.json').url")
VERSION=$(node -p "require('./afs.json').version")

curl -sS -X POST -H 'Content-type: application/json' \
  --data "$(node -p "JSON.stringify({text: 'Android build ' + process.argv[1] + ' is ready: ' + process.argv[2]})" "$VERSION" "$URL")" \
  "$SLACK_WEBHOOK_URL"

Building the JSON with JSON.stringify instead of string pasting matters: release notes and commit messages often contain quotes and line breaks that break hand written JSON. For Discord, the same request works with a content field instead of text, sent to the Discord webhook URL. Keep both URLs in CI secrets.

The JSON output contains url, slug, name, version and expiresAt. More pipeline examples are in uploading builds from CI.

Which one to use

NeedWebhooksPost from CI
Builds uploaded by hand as well as from CICovered, every build fires the eventOnly builds from that pipeline
Message formatFixed, with icon, version, notes and install buttonAnything you write
Failed uploads, installs, bandwidth, store status, reviewsSeparate events you can subscribe toOnly what the job knows
Per project channelsOne integration per projectOne secret per pipeline
PlanPro and TeamPro and Team for CI tokens; posting itself is your own code
Extra code to maintainNone for Slack and DiscordA script per pipeline

Many teams use both: a webhook for the shared release channel, and a CI message for something only the pipeline knows, such as the branch, the pull request or the person who merged.

Tips for a channel people actually read

  • Write release notes for testers: what changed and what to test. They appear in the message, so "fix" alone tells nobody anything. Test build distribution best practices has more on this.
  • Send build.failed to the people who can fix it, not to the whole QA channel.
  • Use one integration per project if several apps share a workspace, so each team sees only its builds.
  • Store release status and review events are useful in a channel for the people who handle store releases. They need a connected App Store Connect or Google Play account.

Not everyone lives in Slack. People who turn on notifications in the AppForShare web app also get a push notification for new builds in their team, next to the usual emails.

Connect your channel

Webhooks for Slack, Discord and your own endpoints are included in Pro at $12 a month and Team at $39 a month, together with CI upload tokens and release notes. Set them up under Dashboard, Integrations, check the webhooks reference for payload details, or compare plans on the pricing page. New here? Create an account first.

Keep reading

App Distribution Platform · · 9 min read

AppForShare.io vs Competitors - A Comparative Analysis

For small teams and startups, selecting a mobile app testing platform is a critical crossroad. This choice is more than a mere selection: it’s a strategic decision that directly impacts the...