Skip to content

New feature: Our API is live: Webhook, MCP, or API - you decide. Try it out!Join beta

Webhooks

Reploya posts a short message to an address you choose when a version waits for approval, a build or delivery fails, a rollout is halted, or a release completes. Send it to a Teams or Slack channel, or to any system that accepts JSON over HTTPS.

How webhooks work

A webhook is an address Reploya calls when something happens. Reploya always makes the call; nothing calls into Reploya, so a webhook opens no way into your organization.

  • Who manages them: owners and admins (the tenant.manage permission) add, edit, pause, enable, test and delete webhooks. Every member sees the list (name, host, format, events, status) and the delivery log, never the full address or the secret.
  • How many: up to 10 webhooks per organization, each with its own format and events.
  • When: every minute Reploya collects what happened and sends one message per webhook and event type. A message usually arrives one to three minutes after the event. Webhooks are not real time.
  • From when: a webhook only reports events after it was created or last enabled. Nothing from before, and nothing from while it was paused or disabled, is sent later.

Create a webhook

  1. In the dashboard, open External access, then Webhooks, and choose Add webhook.
  2. Name: say where it posts, for example "IT team channel" (1 to 80 characters, unique in your organization).
  3. Format: JSON for your own system, Microsoft Teams or Slack for a channel (see Formats).
  4. URL: the address to post to. For Teams, add a Workflows webhook to the channel and paste its URL. For Slack, add an incoming webhook to a channel and paste its URL. The URL is stored encrypted and never shown again; the list shows its host only.
  5. Events: pick one or more of the five events.
  6. Save. For a JSON webhook the dialog now shows the signing secret (it starts with whsec_) once, with a copy button. Store it now; it is not shown again. Teams and Slack webhooks have no secret.
  7. Open the row's menu and choose Send test event to check it works (see Test event).

Events

Each webhook subscribes to one or more event types. The type is the type field of a JSON message and its X-Reploya-Event header.

Webhook events
TypeShown asSent when
approval_waitingVersion waits for approvalA new version waits for someone to approve it. A version that is approved or declined before the message goes out is left out.
build_failedBuild failedPackaging a version failed.
delivery_failedDelivery failedA release could not be assigned in Intune, for one group or for a ring.
rollout_haltedRollout haltedA ring rollout was halted and waits for a person.
release_completedRelease completedA release reached its devices: a single target release once it is assigned, a ring rollout once its last ring is assigned.

Grouping

Messages are grouped, so 30 versions released at once are one channel post, not 30. Each minute, a webhook gets at most one message per event type, listing every event of that minute, oldest first. A message lists at most 50 items; moreCount ("and N more" in Teams and Slack) counts the rest, and the summary counts them all. A system reading JSON should therefore handle a list of items, not one event per request.

Formats

Every format carries the same facts: a summary, your organization, and per item the app (its id, name and platform), the version, the reason for a failure, and a link into the dashboard. Links need a sign in, so they are safe in a channel. A message never holds a person's name or email, a tenant or group id, or a token.

JSON

A versioned body, signed (see Verify the signature). reason appears for build_failed and delivery_failed only. test is true for a test event. Fields may be added later within version: 1; ignore fields you do not know.

JSON body, one failed build
{
  "version": 1,
  "deliveryId": "019a0b6e-41f2-7c3d-9a51-6d2f0e8b7c14/build_failed/2026-10-20T09:15:00.000Z",
  "type": "build_failed",
  "test": false,
  "sentAt": "2026-10-20T09:15:02.000Z",
  "organization": {
    "id": "019a0b6e-3c11-7e42-8b0a-55e1c9d2f003",
    "name": "Contoso"
  },
  "summary": "1 build failed",
  "url": "https://app.reploya.com/dashboard/builds",
  "items": [
    {
      "eventId": "019a0b6d-f7a8-7b10-8c3e-2a9d41e6b5f0",
      "occurredAt": "2026-10-20T09:12:40.000Z",
      "app": {
        "id": "019a0b6c-2e95-7a4f-b1d3-7f80c4a9e261",
        "name": "Notepad++",
        "platform": "windows"
      },
      "version": "8.7.1",
      "reason": "The package could not be uploaded to Intune.",
      "url": "https://app.reploya.com/dashboard/builds/019a0b6d-e1c4-7d92-a6f5-30b8e2c71d4a"
    }
  ],
  "moreCount": 0
}

The wire form is compact JSON on one line; the examples here are indented for reading.

Microsoft Teams

An Adaptive Card for a Teams Workflows webhook: the summary as its title, your organization, one linked line per item, and an "Open in Reploya" button for the list.

Teams body, two versions waiting for approval
{
  "type": "message",
  "attachments": [
    {
      "contentType": "application/vnd.microsoft.card.adaptive",
      "contentUrl": null,
      "content": {
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "type": "AdaptiveCard",
        "version": "1.4",
        "body": [
          {
            "type": "TextBlock",
            "text": "2 versions wait for approval",
            "weight": "Bolder",
            "size": "Medium",
            "wrap": true
          },
          {
            "type": "TextBlock",
            "text": "Contoso",
            "isSubtle": true,
            "spacing": "None",
            "wrap": true
          },
          {
            "type": "TextBlock",
            "text": "[7-Zip 24.09](https://app.reploya.com/dashboard/approvals/019a0b6d-a3f0-7e55-b4c2-8d7e1a6f9b03) · Windows",
            "wrap": true
          },
          {
            "type": "TextBlock",
            "text": "[Google Chrome 141.0.7390.66](https://app.reploya.com/dashboard/approvals/019a0b6d-b8a1-7c63-9d15-4f0b2e7a8c39) · macOS",
            "wrap": true
          }
        ],
        "actions": [
          {
            "type": "Action.OpenUrl",
            "title": "Open in Reploya",
            "url": "https://app.reploya.com/dashboard/approvals"
          }
        ]
      }
    }
  ]
}

Slack

A message for a Slack incoming webhook: the summary in bold, one line per item with its link, and "Open in Reploya".

Slack body, the same two versions
{
  "text": "2 versions wait for approval (Contoso)",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*2 versions wait for approval*\nContoso"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "• <https://app.reploya.com/dashboard/approvals/019a0b6d-a3f0-7e55-b4c2-8d7e1a6f9b03|7-Zip 24.09> · Windows\n• <https://app.reploya.com/dashboard/approvals/019a0b6d-b8a1-7c63-9d15-4f0b2e7a8c39|Google Chrome 141.0.7390.66> · macOS"
      }
    },
    {
      "type": "context",
      "elements": [
        {
          "type": "mrkdwn",
          "text": "<https://app.reploya.com/dashboard/approvals|Open in Reploya>"
        }
      ]
    }
  ]
}

Teams and Slack sometimes answer 200 with an error in the body. Reploya cannot see that, so if a channel stays quiet, check the channel's webhook in Teams or Slack too.

Request headers

Every request is a POST with these headers. Only JSON webhooks carry the X-Reploya-* headers.

Webhook request headers
HeaderValue
Content-Typeapplication/json
User-AgentREPLOYA-Webhooks/1
X-Reploya-EventThe event type, or test for a test event.
X-Reploya-DeliveryThe message's delivery id, also deliveryId in the body. It stays the same on every retry of one message, so use it to ignore a message you already handled.
X-Reploya-TimestampWhen this attempt was sent, in Unix seconds.
X-Reploya-Signaturev1= followed by the hex HMAC SHA-256 of the timestamp and the body.
A JSON request, headers
POST /your/path HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: REPLOYA-Webhooks/1
X-Reploya-Event: build_failed
X-Reploya-Delivery: 019a0b6e-41f2-7c3d-9a51-6d2f0e8b7c14/build_failed/2026-10-20T09:15:00.000Z
X-Reploya-Timestamp: 1792487702
X-Reploya-Signature: v1=2cb95c4d09bfa9383e5c49fba9ae2f57f3c29efd1169435233f273201099a946

Verify the signature

Anyone who knows your URL could post to it. The signature proves a request came from Reploya and was not changed. Compute

Signature
v1=hex( HMAC_SHA256( secret, "<X-Reploya-Timestamp>.<raw body>" ) )

and compare it with X-Reploya-Signature in constant time. Use the whole secret as the key, including whsec_. Sign the body exactly as it arrived, before any JSON parsing. Refuse a timestamp more than a few minutes away from your clock, so an old request cannot be replayed.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// The secret shown once in Reploya, including its "whsec_" prefix.
const SECRET = process.env.REPLOYA_WEBHOOK_SECRET;

/**
 * rawBody: the request body exactly as received, as a string.
 * headers: the request headers, names in lower case.
 */
export function isFromReploya(rawBody, headers) {
  const timestamp = headers["x-reploya-timestamp"];
  const signature = headers["x-reploya-signature"] ?? "";
  // Refuse old requests, so a captured one cannot be replayed later.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!timestamp || !(age <= 300)) return false;

  const expected =
    "v1=" +
    createHmac("sha256", SECRET)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Rotating the secret replaces it at once, with no overlap: update your receiver right after you rotate.

Address rules

Reploya only posts to public addresses. A URL is accepted when it

  • starts with https:// and uses port 443 (or none);
  • has a host name, not an IP address;
  • has no user name or password in it, and at most 2048 characters;
  • is not localhost, not a single label name, and does not end in .local, .internal or .localhost.

Before each request, the host name is resolved and every address must be public. A host that resolves to a private, loopback, link local or other reserved address is refused as Address not allowed, without a request. Redirects are never followed. If your endpoint accepts only known addresses, contact us for the address Reploya sends from.

Retries and failures

  • Any 2xx answer counts as delivered. Answer quickly: a request times out after 10 seconds.
  • A timeout, a connection, DNS or certificate error, a redirect, or any other status is retried 5 times, starting 30 seconds later and doubling the wait each time, over about 16 minutes in all. Every retry has the same delivery id; its timestamp and signature are new, and an approval decided meanwhile may have dropped out.
  • 410 Gone, an address that is not allowed, or stored settings that cannot be read fail at once, without retries.

Auto disable

A message that still fails after its retries counts as one failure; a delivered message resets the count. After 10 failed messages in a row, or at once on 410 Gone, Reploya disables the webhook. The list then shows Disabled after failures, and every owner and admin gets one email. Fix the endpoint, then choose Enable: the count starts at zero, and events from while it was disabled are not sent.

Delivery log

Recent deliveries in a webhook's menu shows its newest 50 attempts, tests included, newest first: time, event, item count, attempt, result, status code and duration. Every member can open it. It never holds the URL or the body. A failed attempt names its cause:

Delivery errors
Shown asMeaning
Timed outNo answer within 10 seconds.
Host not foundThe host name did not resolve.
Could not connectThe connection was refused or dropped.
Certificate errorThe certificate is invalid for the host.
Redirect not followedThe endpoint answered with a redirect, which is not followed.
HTTP <code>The endpoint answered with this status, not 2xx.
Address not allowedThe host resolved to a private or reserved address.
Stored settings unreadable, recreate the webhookThe stored URL or secret can no longer be read. Delete and recreate the webhook.

Test event

Send test event posts one message of type test right away, with "test": true and one sample item ("Example app" 1.0.0 on Windows), formatted and signed like any other message. The dialog shows the result and status code within about 11 seconds, and the attempt appears in the delivery log.

A test is not retried and never changes the failure count or the status. Each webhook allows 10 tests per hour.

Pause, rotate and delete

  • Edit changes name, format, URL and events. Leave the URL empty to keep the current one. Switching to JSON creates a new secret, shown once; switching away from JSON deletes it. Editing a disabled webhook does not enable it.
  • Pause stops messages until you choose Enable. Events in between are not sent later.
  • Rotate secret (JSON only) replaces the secret at once and shows the new one once.
  • Delete removes the webhook and its delivery log.

Every change is recorded in the dashboard's Activity, without the URL or the secret.

Recipe: n8n

An n8n flow that receives Reploya messages and checks their signature:

  1. Add a Webhook trigger node. Set HTTP Method to POST and Respond to Immediately, so Reploya gets its 200 before the 10 second timeout, however long the flow runs.
  2. Copy the node's Production URL (not the test URL) and activate the workflow. In Reploya, add a webhook with format JSON and that URL, and store the secret in an n8n credential or variable.
  3. Add a Crypto node: action Hmac, type SHA256, encoding HEX, your secret, property name expected, and as value:
    Crypto node, value
    {{ $json.headers['x-reploya-timestamp'] }}.{{ JSON.stringify($json.body) }}
  4. Add an If node that continues only when v1={{ $json.expected }} equals {{ $json.headers['x-reploya-signature'] }}, and stops the flow otherwise.
  5. Use {{ $json.body.items }} from there, for example to open one ticket per item, and skip a deliveryId you already handled.

Re serialising the parsed body matches the signature because Reploya sends compact JSON. If you prefer the raw body, turn on the Webhook node's Raw Body option and sign that instead, as in the Node.js example.

Want to ask Reploya for details after an event? See the API docs.