Integrate
Macro Webhooks: Run Events, Signatures and Custom Events
Get a signed macro webhook POST from MAS when a run starts, completes, fails or stops, verify the X-MAS-Signature in Python or Node, and emit custom events.
- Windows
- Mac
- Emulator
- Cloud device
- Studio
- Python SDK
- API
On this page
- Before you start
- Create an endpoint
- Scope
- Macro webhook events
- The payload
- Headers
- Verify the automation webhook signature with HMAC
- Webhook delivery and retry
- Test and redeliver in the app
- Custom events from a script
- Troubleshooting
- Nothing arrives and the delivery log is empty
- Delivery shows exhausted
- Endpoint shows Auto-disabled
- Signature does not verify
- The URL is refused
A macro webhook tells you what your macros did without opening the app. Macro Automation Studio (MAS) posts a signed JSON event to a URL you choose when a run starts, completes, fails or stops, and when a script emits its own event. Delivery comes from MAS’s servers, so it works for emulators and cloud devices alike, whether or not the app is open. On a cloud device the run itself happens on MAS servers too, so the event arrives even when your computer is off.
Before you start
- A receiver on the public internet with an HTTPS URL. Plain
http, private addresses, loopback and link-local addresses are refused. - MAS installed and signed in. Endpoints are created on the Webhooks page; they can also be managed over the REST API.
- A macro that runs on one of your devices. The app reports local runs; the cloud runner reports cloud runs.
Create an endpoint
- In MAS, open Webhooks.
- Under Add an endpoint, enter the Endpoint URL.
- Enter a Description.
- Under Send me, tick the events you want: Everything, Macro started, Macro completed, Macro failed, Macro stopped or Custom events from my scripts. Pick at least one.
- Under Send me events from, choose All devices, Specific groups or Specific devices, and pick the targets for a narrowed scope.
- Click Add endpoint.
- In the Your signing secret dialog, click Copy, store the secret in your receiver’s configuration, then click Done.
The secret starts with whsec_ and is shown once; the list shows only its first twelve characters. If you lose it, click the Issue a new signing secret icon on the endpoint’s row. Deliveries are signed with the new secret straight away, so update the receiver first. An account can hold up to 10 endpoints. Everything on this page can also be done with an API key; see the REST API.
Scope
Scope decides which devices an endpoint hears about. All devices, the default, covers every device on the account, including ones you add later. Specific groups covers the devices in the chosen groups, including devices added later. Specific devices covers exactly the devices you pick. An event that names no device passes every scope. A narrowed endpoint whose targets were all deleted delivers nothing; its row shows “Covers no devices” so you can fix it with Devices. See Device groups.
Macro webhook events
| Event | When it fires |
|---|---|
macro.started | A run began |
macro.completed | A run ended normally |
macro.failed | A run ended with an error or a non-zero exit code |
macro.stopped | A run was stopped by you or by a stop request |
custom.<name> | A script called mas.webhook("<name>") |
custom.* | Every custom event, and no lifecycle events |
* | Everything, including event types added later |
webhook.test | The sample event sent by Test, delivered regardless of subscriptions |
Matching is exact per entry: an endpoint subscribed to macro.failed never receives macro.completed. Outcomes are separate types because most people running devices unattended want failures alone.
The payload
Every delivery is a POST with a JSON body of four fields: id, type, created_at (UTC) and data. For a run event, data carries:
| Field | Meaning |
|---|---|
execution_id | The run’s id, the same one the runs API shows |
status | running, completed, failed or stopped |
macro_name, macro_id | The macro; macro_id is omitted when unknown |
device_name, device_id, device_port | The device; device_id is omitted when unknown |
group_id | The device group, when the device is in one |
execution_type | block or python |
started_at | Start time, UTC |
ended_at, duration_ms | Present on completed, failed and stopped |
exit_code | The script’s exit code, when there is one |
error | The error message on a failure |
proxy_exit_ip, proxy_country | The exit IP and country of the proxy the run used, when a proxy was attached |
{
"id": "3c1c0a8e-9d2e-5f61-8b0a-2f7d4c1e9a10",
"type": "macro.failed",
"created_at": "2026-09-04T09:15:02Z",
"data": {
"execution_id": "a1b2c3d4",
"status": "failed",
"macro_name": "Daily tasks",
"macro_id": 123,
"device_name": "Farm 1",
"device_id": 45,
"device_port": 5555,
"group_id": 7,
"execution_type": "python",
"started_at": "2026-09-04T09:10:00Z",
"ended_at": "2026-09-04T09:15:02Z",
"duration_ms": 302000,
"exit_code": 1,
"error": "ImageNotFoundError: image 42 is not in your library",
"proxy_exit_ip": "203.0.113.7",
"proxy_country": "US"
}
}For a custom event, data is the dictionary your script passed, plus execution_id and, on a local run, device_name, added by MAS so a script cannot forge its origin. For webhook.test, data carries message, endpoint_id and requested_at. The payload never contains credentials. Delivery is at least once and the id of a run event is stable across re-reports, so treat id as your dedupe key.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | MacroAutomationStudio-Webhooks/1 |
X-MAS-Signature | t=<unix timestamp>,v1=<hex HMAC-SHA256> |
X-MAS-Event-Id | The event id, the same as id in the body |
X-MAS-Event-Type | The event type, the same as type in the body |
Verify the automation webhook signature with HMAC
The signature is HMAC-SHA256 with your endpoint’s secret over the string {t}.{body}, where t is the timestamp from the header and body is the raw request body, byte for byte. The timestamp sits inside the signed value, so a captured request cannot be replayed later. Reject a delivery whose timestamp is more than five minutes from your clock, compare digests in constant time, and verify the raw bytes before parsing the JSON.
Python, standard library only:
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify(secret: str, body: bytes, header: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
return False
if abs(time.time() - int(t)) > TOLERANCE_SECONDS:
return False
signed = t.encode() + b"." + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Node, built-in crypto only:
const crypto = require("node:crypto");
const TOLERANCE_SECONDS = 300;
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=", 2))
);
const { t, v1 } = parts;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Frameworks that parse JSON before your handler runs usually offer a raw-body option; use it for this route.
Webhook delivery and retry
Answer with any 2xx within 10 seconds and do the work after you respond; a slower receiver is treated as failed. Anything else, including a timeout, a 4xx, a 5xx or a connection error, is a failed attempt. MAS makes up to 6 attempts per delivery, waiting 30 seconds, then 2 minutes, 5 minutes, 15 minutes and 30 minutes between them. Redirects are followed up to three times, each hop checked against the same address rules as the original URL.
A delivery is pending until it succeeds or gives up, succeeded on a 2xx, failed after an attempt with retries remaining, and exhausted when all 6 attempts failed. After 15 exhausted deliveries in a row the endpoint is switched off automatically and shows Auto-disabled with the reason; a successful delivery resets the count. To recover, fix the receiver and switch the endpoint back on with its toggle, which clears the streak.
Test and redeliver in the app
Click Test on an endpoint’s row to queue a webhook.test event; it is sent even when the endpoint subscribes only to failures. Recent deliveries lists every attempt with its Event, Status, When, Tries and Result, and refreshes on its own while something is still sending. View response opens Delivery detail with What we sent, Response from your endpoint, the Event ID and a Send again button; Retry in a row does the same. Redelivery is your own recovery path after you fix a receiver.
Custom events from a script
A macro can send its own event in one line. MAS’s servers deliver it, signed, retried and logged like a run event, on local and cloud runs alike; the script never touches a secret or an HTTP client.
import mas
result = mas.webhook("level.reached", {"level": 40, "account": "alt-3"})
mas.log(f"webhook {result['event_type']} queued to {result['queued']} endpoints")The name is namespaced under custom., so level.reached arrives as custom.level.reached and a script can never fake a macro.failed. Names may contain letters, numbers, dots, dashes and underscores, up to 64 characters; data is optional, JSON-serialisable and up to 50 keys. The call returns event_id, event_type and queued, the number of endpoints the event went to; 0 means nothing is subscribed yet. Subscribe an endpoint to Custom events from my scripts (custom.*), to *, or to the exact name over the API. If the event cannot be queued, the call raises RuntimeError. The function is listed with the rest of the namespace on the apps and device page.
Troubleshooting
Nothing arrives and the delivery log is empty
The event did not match the endpoint. Check the Send me subscriptions, then the scope; a filtered event leaves no delivery row on purpose. Click Test to confirm the receiver itself works.
Delivery shows exhausted
All 6 attempts failed. Open View response to read the status code and body your receiver returned, fix the receiver, then click Send again. Common causes are a 4xx from the URL, a handler slower than 10 seconds, and a certificate error.
Endpoint shows Auto-disabled
Fifteen deliveries in a row were exhausted. Fix the receiver, switch the endpoint back on with its toggle, and confirm with Test.
Signature does not verify
Verify the raw request bytes, not a re-serialized body, with the t value from the header and the secret from the latest Your signing secret dialog; issuing a new secret invalidates the old one at once. A clock more than five minutes off also fails the check.
The URL is refused
Endpoint URLs must use https and resolve to a public address. Private, loopback, link-local and reserved addresses are rejected at save time and again at send time, including after a redirect.
Next steps
Related pages
Thanks. If something is wrong, tell us in Discord.
Questions? Ask in Discord