Webhooks: RMM, PSA & chat integration
Push every capture and deployment to another system the moment it finishes.
ImageForge can POST a signed JSON message to a URL you choose whenever a capture or deployment completes. That covers an RMM or PSA inbound webhook, a Slack/Teams/Discord channel, or any endpoint you write yourself.
HTTP 400 invalid_payload if you send it the raw event. Choosing the matching
format in the console is what makes those integrations work — see
payload formats below.Set it up
- Get an inbound webhook URL from the receiving system (see per-service notes).
- In the Forge console, open
activityand find the webhook (RMM / PSA integration) panel. - Paste the URL, tick enabled, and choose the payload format that matches the receiver.
- Click send test event. A message appears in the receiving system within a second or two, and the console reports the exact HTTP status it got back.
- Click apply to save.
- Click regenerate signing secret and copy the secret immediately — it is shown once. Configure it on the receiving end if that system verifies signatures.
Payload formats
| Format | Use it for | Body sent |
|---|---|---|
raw (default) | RMM, PSA, and receivers you write yourself | The event object itself — see payload reference |
slack | Slack incoming webhooks, and services that emulate them | {"text": "…"} |
teams | Microsoft Teams incoming webhooks | A MessageCard, colour-coded green for success and red for failure |
discord | Discord channel webhooks | {"content": "…"} |
The chat formats render one readable line, with the failure reason on a second line when there is one:
Deploy succeeded - win11-golden on FRONT-01 (Latitude 7090) by JB [4m12s]
Deploy failed - win11-golden on FRONT-01 (Latitude 7090) by JB [1m38s] DISM apply failed: exit status 112
Payload reference (raw format)
The raw body is the same object the audit trail stores. Optional fields are
omitted when empty, so treat anything except id, time,
type, and result as possibly absent.
{
"id": "9f2c1b7a",
"time": "2026-08-13T14:22:05Z",
"type": "deploy",
"result": "success",
"technician": "JB",
"serial": "ABC123",
"model": "Latitude 7090",
"mac": "00:11:22:33:44:55",
"hostname": "FRONT-01",
"image_id": "3f8a…",
"image_name": "win11-golden",
"profile": "front office",
"duration_s": 252,
"message": ""
}
| Field | Notes |
|---|---|
type | capture or deploy |
result | success or failure |
technician | Stamped by the Forge from the authenticated identity, not supplied by the PC |
hostname | The name assigned by the deployment profile, when one applied |
message | The failure reason on a failure; usually empty on success |
duration_s | Whole seconds |
Delivery behaviour
| Detail | Value |
|---|---|
| Method | POST, Content-Type: application/json |
| User agent | ImageForge-Webhook/1 |
| Fires on | Completion of a capture or a deployment — nothing else |
| Success | Any 2xx response |
| Retries | 3 attempts total, ~1s then ~2s apart, 5-second timeout each |
| Ordering | Not guaranteed; deliveries are independent |
Delivery is best-effort and deliberately cannot affect imaging: the audit trail is the durable record, and a webhook is a notification on top of it. A receiver that is down, slow, or rejecting messages never delays or fails a capture or deployment.
Verifying the signature
Every delivery carries:
X-ImageForge-Signature: sha256=<hex>
That is an HMAC-SHA256 of the exact raw request body, keyed with your signing secret. Compute it over the bytes as received — do not re-serialise the JSON first, since any change in key order or whitespace produces a different result. The header is only sent when a secret exists.
// Node.js
const crypto = require("crypto");
function valid(rawBody, header, secret) {
const want = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header || "");
const b = Buffer.from(want);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
# PowerShell
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($secret))
$hash = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($rawBody))
$want = "sha256=" + (($hash | ForEach-Object { $_.ToString("x2") }) -join "")
Regenerating the secret invalidates the old one immediately, so update the receiver in the same maintenance window.
Per-service notes
RMM / PSA (ConnectWise, Datto, NinjaOne, Atera, and similar)
Use the raw format. Create an inbound webhook or automation trigger that
accepts JSON, then map the fields above onto whatever the platform records. Most systems
key on serial to match an existing asset, and treat
result: "failure" as the condition worth alerting on. If the platform requires
its own authentication header rather than a signature, put that token in the URL's query
string where supported — ImageForge redacts query strings from its own logs.
Slack
Create an incoming webhook in your Slack app configuration and use the
slack format. The raw format will fail every delivery with
invalid_payload.
Microsoft Teams
Use the teams format with an incoming-webhook connector URL, or a Workflows
("Post to a channel when a webhook request is received") URL. ImageForge sends a
MessageCard, which both accept.
Discord
Channel settings → Integrations → Webhooks → copy the webhook URL, and use
the discord format.
When events are not arriving
| Symptom | Likely cause |
|---|---|
Test send reports invalid_payload or a 400 |
Wrong payload format for the service. Pick the matching one. |
| Test send times out or cannot connect | The Forge cannot reach the URL — outbound firewall, proxy, or an internal-only hostname. The Forge makes this call, not the PC being imaged. |
| 401/403 from the receiver | The receiving system wants its own auth token, separate from the ImageForge signature. |
| Test works, real events never arrive | Check enabled is ticked and the settings were saved with apply. Events only fire on completed captures and deployments. |
| Receiver rejects the signature | The HMAC must be computed over the raw body bytes, not a re-serialised copy, and the secret must match the current one. |
The panel's recent-deliveries table shows the last 20 real deliveries with their status codes, which is the first place to look. It is kept in memory, so it resets when the Forge service restarts.
image-forge.net