Skip to content

Webhooks

One HTTPS URL per workspace. Without events you get all of them. The signing secret is only returned when it is created: keep it.

Terminal window
curl -X PUT "https://api.marsial.dev/v1/webhook-endpoint" \
-H "Authorization: Bearer $MARSIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.acme.com/webhooks/marsial",
"events": [
"post.published",
"post.failed",
"account.disconnected"
]
}'
Event When data
post.scheduled Scheduled or queued post
post.published Live on every network post
post.partial Live on some, failed on others post
post.failed Live nowhere, also when one network fails on its own (an image it could not download) post
post.cancelled Cancelled post
post.removed Taken down from a network (also when deleting it with live copies, before post.deleted) post
post.rejected Rejected in review and back to draft post
post.recycled A recycled copy was created post (the copy), recycledFrom
post.draft_saved Saved as a draft post
post.deleted Deleted from Marsial post with status: "deleted"; each target keeps its own (a copy still on Instagram or TikTok, published)
account.connected An account was connected account
account.disconnected Disconnected or needs reconnecting account
message.received Someone sent a direct message accountId, conversationId, message
comment.received Someone commented on a post accountId, postId, comment
review.received A new review arrived (Google Business) accountId, review

To try your endpoint without waiting for a real event, POST /v1/webhook-endpoint/test with { "type": "post.failed" } (or any other type it subscribes to) sends a signed sample with test: true and made-up ids.

Each status of a post is announced once: Marsial sends post.scheduled or post.published as soon as it knows, even when the network says nothing, and never repeats it. Deliveries can cross: sort them by createdAt (when deleting, post.deleted comes a millisecond after post.removed).

Body
{
"id": "evt_9a8s7d",
"type": "post.published",
"createdAt": "2026-10-23T10:00:05.000Z",
"data": {
"post": {
"id": "pst_7q1w0e",
"status": "published",
"targets": [
{ "accountId": "acc_3k9x2m", "platform": "instagram", "status": "published", "url": "https://www.instagram.com/p/…" }
]
}
}
}

Each delivery carries Marsial-Event, Marsial-Event-Id and Marsial-Signature: t=<seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<body> with your secret. Compare it against the unparsed body and reject signatures older than 5 minutes.

Node
import { createHmac, timingSafeEqual } from 'node:crypto';
export const verifyMarsialSignature = (rawBody, header, secret) => {
const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
};
Python
import hmac, hashlib, time
def verify_marsial_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
  • Answer with a 2xx within 10 seconds. Do the heavy work afterwards.
  • If you fail, we retry several times with growing waits.
  • The same event may arrive more than once: use Marsial-Event-Id to process it only once.

© 2026 Marsial by TWOFACES ·marsial.dev ·Dashboard ·soporte@twofacesco.com