Before you start
- You are an owner or admin of the workspace.
- An endpoint on your server that accepts
POSTrequests with a JSON body, on a public https:// address using port 443 or 8443. Local and private addresses are blocked. - Your endpoint can read the raw request body before parsing it, so the signature check works.
Steps
- Step 1
Open Notifications
Under Settings in the left-hand menu, click Notifications and scroll to Slack, Discord and webhook.
Where to find it in Skylane- Settings
- Notifications
- Slack, Discord and webhook
- Step 2
Add your webhook URL
Paste your address into Your own webhook URL and click Add Your own webhook.
Diagram: on the Notifications screen, press Add Your own webhook. - Step 3
Copy the signing secret
Under Copy your webhook signing secret now, click Copy and save the secret in your server's secret settings. It is shown only once. Then click I have saved it.
- Step 4
Choose the events
Scroll up to the Alerts table. In the Your own webhook column, switch on each event you want, such as Post failed or Account needs reconnecting. These ticks save straight away.
- Step 5
Check the signature on every message
Each request carries a
Skylane-Signature: t=<unix time>,v1=<hex>header.v1is an HMAC-SHA256 of the timestamp, a full stop and the raw body, using your secret. Reject messages that do not match or are more than 5 minutes old.Verify in Node.jsjavascript import crypto from "node:crypto"; // rawBody must be the exact bytes Skylane sent. Do not parse and re-serialise the JSON first. export function verifySkylane(rawBody, header, secret, toleranceSeconds = 300) { const fields = Object.fromEntries(String(header || "").split(",").map((part) => part.split("=", 2))); const timestamp = Number(fields.t); if (!timestamp || !fields.v1) return false; if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false; const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(fields.v1, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } // Express example: keep the raw body for this route. // app.post("/skylane-webhook", express.raw({ type: "application/json" }), (req, res) => { // if (!verifySkylane(req.body, req.get("Skylane-Signature"), process.env.SKYLANE_WEBHOOK_SECRET)) return res.sendStatus(401); // const event = JSON.parse(req.body); // res.sendStatus(200); // reply first, then do the slow work // }); - Step 6
Or check it in Python
The same check in Python, matching Skylane's own reference code:
Verify in Pythonpython import hashlib import hmac import time def verify_skylane(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: """raw_body must be the exact bytes Skylane sent.""" try: fields = dict(item.split("=", 1) for item in header.split(",")) timestamp = int(fields["t"]) supplied = fields["v1"] except (AttributeError, KeyError, ValueError): return False if abs(time.time() - timestamp) > tolerance: return False message = str(timestamp).encode() + b"." + raw_body expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, supplied) - Step 7
Reply quickly
Reply with any
2xxstatus within 10 seconds, then do slow work afterwards. TheSkylane-Eventheader names the event so you can route it before parsing.
Good to know
- Event types:
post_published,post_failed,account_needs_reconnect,token_expiring_7d,queue_low,approval_requestedandapproval_decided. Digests are email and in-app only. data.titleanddata.messagehold the same plain-English text the in-app alert shows. Visitor IP addresses are never included.- A webhook that fails 20 times in a row is paused. Fix your endpoint, then click Turn back on.
- Settings, then Developer also has a Webhooks panel with Send test and a log of the last 20 deliveries. It signs messages the same way, with its own secret.
{
"id": "evt_EXAMPLE",
"type": "post_failed",
"createdAt": "2026-11-02T09:30:00Z",
"workspaceId": "YOUR_WORKSPACE_ID",
"data": {
"postId": "YOUR_POST_ID",
"platform": "instagram",
"platformName": "Instagram",
"accountName": "Your account name",
"reason": "The plain-English reason the post failed.",
"fixHint": "What to do about it.",
"errorCode": "THE_PLATFORM_CODE",
"link": "https://skylane.moneymasterhq.com/app/workspaces/YOUR_WORKSPACE_ID/posts/YOUR_POST_ID?retry=1",
"title": "A post to Instagram could not be published",
"message": "The plain-English reason the post failed. What to do about it. You can retry it from the post."
}
}Common problems
"Webhook URLs must start with https://"
Use an https address. Plain http, localhost, private IP addresses and ports other than 443 and 8443 are not accepted.
Every signature check fails
You are probably hashing parsed JSON. Hash the raw bytes exactly as received, and check you saved the secret for this webhook, not another one.
Checks fail only sometimes
Your server clock may be out. Sync it with NTP; the check allows 5 minutes either way.
I lost the signing secret
Click Remove next to Your own webhook, add it again and copy the new secret.
Still stuck? Contact support
Tell us which step you reached and what you see on screen. A screenshot helps.