Sending Marshal a webhook
Turn events from your other systems into agent work. A payment, a pull request or a failed deploy posts to Marshal, and a deputy starts the right mission with the event’s data, while you do something else.
A webhook endpoint is a URL and a signing secret. Your system posts signed events to the URL. Marshal checks the signature, keeps a record of the delivery, and can start a deputy mission for each event. You can have many endpoints, each with its own URL, secret and signature format. This page is the contract. Give it to whoever writes the sending side.
Available when your administrator enables webhooks. Starting missions from events also needs deputies.
What you can build
- Review every large invoice. Create an endpoint with the Stripe signature format and point a Stripe webhook at it. Stripe sends no
X-Event-Typeheader, so the trigger binds to*and sorts with a condition:event.type == "invoice.paid" && event.data.object.amount_paid > 100000. The input map passesdata.object.idto the mission as the invoice. Each matching invoice starts a reconciliation run, and Stripe’s retries start it only once, because each Stripe event carries anid. - Review each new pull request. Connect the GitHub app, then add a trigger with source
GitHuband eventpull_request.opened. Mappull_request.numberinto the mission’s input. Every new pull request starts a review run on your deputy, and the deputy’s questions reach you in your inbox. - Triage a failed deploy. Your deploy pipeline posts
deploy.failedwith Marshal’s own signature and a body such as{"id": "run-881", "service": "billing", "log_url": "..."}. A trigger ondeploy.*with the conditionevent.service == "billing"mapslog_urlinto the mission. The run reads the log and reports the cause before anyone opens a dashboard.
To set one up from start to finish, see Run a mission on a schedule or an event.
See it end to end: Triage alerts from your tools.
Your system posts a signed event to the relay. Your install pulls it, and a matching trigger starts a deputy mission run.
How events reach Marshal
Senders do not post to your cluster. They post to the webhook relay, which Marshal hosts by default. Your Marshal install pulls events out of the relay. Your cluster needs no inbound internet access.
The relay host in your delivery URL is the default hooks.marshal.codes, unless your administrator or an org admin points the org at another relay. Always copy the delivery URL from Marshal. Do not build it by hand.
Who can create an endpoint
Every endpoint has an owner. The owner decides who can see it and who can change it.
| Owner | Who can create it | Who can rotate the secret or change the format | Who can delete it |
|---|---|---|---|
| Just me (personal) | Any member of the org | The owner only | The owner, or an admin |
| A team | A lead of that team | A lead of that team | A lead of that team, or an admin |
| Org-wide | An admin or owner | Admins and owners | Admins and owners |
Other rules:
- An admin cannot rotate someone else’s personal secret. A new secret would be shown to the admin and not to the owner.
- An org holds up to 25 endpoints. Each person holds up to 5 personal endpoints.
- A name is required, at most 64 characters, and unique among your own endpoints (or among the org’s for an org-wide one).
- When a member leaves the org, their personal endpoints stop working and any trigger bound to them is disabled.
Create an endpoint
Org-wide endpoints are created on Settings > Webhooks, which only admins can open. Personal and team endpoints are created on Personal settings > Webhooks. The Owner field there lists Just me, and each team you lead.
- Open Settings > Webhooks (admins) or Personal settings > Webhooks (everyone).
- Select Add endpoint.
- Enter a Name, for example
billing. - On the personal page, choose the Owner if you lead a team.
- Choose the Signature format your sender already uses. The default is Marshal’s own. See Signature formats.
- Choose where the secret comes from. Leave the default and Marshal creates one. Pick Paste the sender’s secret when your sender makes its own. See Use the sender’s own secret.
- Select Create.
- Copy the Delivery URL and, when Marshal created it, the Signing secret.
The secret is shown once. Marshal keeps only what it needs to verify a signature. Put the secret in your own secret store before you close the panel. If you lose it, rotate it. See Rotating your secret.
What you should see: the new endpoint in the Receivers list, with its delivery URL, its signature format, and its activity for the last 24 hours and 7 days.
The delivery URL looks like this. The token in it identifies your endpoint, so treat the whole URL as a credential.
POST https://<relay-host>/webhook/conn/<token>Sign a request
Every request needs a signature that matches the endpoint’s format. For the default Marshal format, send three headers.
| Header | Value |
|---|---|
X-Timestamp | The current time as Unix seconds. |
X-Signature | HMAC-SHA256 of <X-Timestamp>.<raw body>, hex-encoded. sha256=<hex> is accepted too. |
X-Event-Type | Your name for the event, for example invoice.paid. Optional. See Naming your events. |
The key for the HMAC is the secret text exactly as Marshal showed it. Do not decode it.
Two rules decide whether your integration works:
- Sign the raw bytes you send. Do not sign a re-serialized copy. If your HTTP client re-encodes the body after you sign it, the signature will not match.
- Sign the timestamp with the body. The signed string is the timestamp, a literal
., and then the body. Marshal rejects a request whenX-Timestampis more than five minutes from its own clock, in either direction. A request captured off the wire cannot be replayed later.
The body can be any content type. The maximum size is 10 MiB.
curl
URL="REPLACE_WITH_DELIVERY_URL"
SECRET="REPLACE_WITH_SECRET"
BODY='{"invoice":"inv_123","amount":4200}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -hex \
| sed 's/^.* //')
curl -sS -X POST "$URL" \
-H "Content-Type: application/json" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "X-Event-Type: invoice.paid" \
--data-raw "$BODY"Signature formats
If the system that will post to Marshal already signs its webhooks, pick its format when you create the endpoint. You change nothing on the sending side.
| Format | Headers it reads | What it signs |
|---|---|---|
| Marshal (default) | X-Signature, X-Timestamp | <unix-seconds>.<body>, hex |
| Stripe | Stripe-Signature: t=...,v1=... | <t>.<body>, hex |
| Standard Webhooks | webhook-id, webhook-timestamp, webhook-signature: v1,... | <id>.<timestamp>.<body>, base64 |
| Slack | X-Slack-Signature, X-Slack-Request-Timestamp | v0:<timestamp>:<body>, hex |
| Linear | Linear-Signature | the body, hex; the timestamp is the body’s webhookTimestamp |
| Body only | X-Hub-Signature-256 (GitHub) or X-Shopify-Hmac-SHA256 (Shopify) | the body alone |
| Shared token (no signature) | Authorization: Bearer <token>, Basic auth with the token as the password, or X-Webhook-Token: <token> | nothing; the token itself is checked |
An endpoint verifies with exactly one format, the one you chose. A request that is signed correctly in a different format is refused. If an endpoint accepted every format, it would only be as strong as the weakest one a sender picked.
The formats other than Body only and Shared token check the timestamp. They refuse a request that is more than five minutes from Marshal’s clock.
Body only signs the body alone, so it carries no timestamp. Choose it when your sender offers no other format. Marshal asks you to confirm before it creates or switches to this format.
Shared token (no signature) is for senders that cannot sign a request and offer only Bearer, Basic auth or a custom header, such as Zendesk, Datadog, Grafana and ServiceNow. Marshal creates the token and shows it once, when you create the endpoint or rotate it. The sender attaches it in one of three ways: Authorization: Bearer <token>, Basic auth with the token as the password and any username, or an X-Webhook-Token: <token> header. The token proves who sent the request. It does not cover the body and carries no timestamp, so Marshal asks you to confirm before it creates the endpoint. When your sender offers a signing format, choose that instead.
Notes on the formats that belong to other specifications:
- Standard Webhooks. Marshal supports the
v1(shared secret) signatures only. It uses your secret text as the key. If the secret starts withwhsec_, Marshal decodes the part after it from base64 and uses the decoded bytes. The secrets Marshal issues do not start withwhsec_. - Stripe. The header may carry several
v1signatures. Marshal accepts the request if any one matches. It ignoresv0. - Linear. Marshal reads the time from the signed
webhookTimestampfield in the body, in milliseconds. Create the Linear webhook with Marshal’s signing secret: see Start a run from a Linear issue.
Use the sender’s own secret
Some senders create their own signing secret and do not accept one from you: Stripe (whsec_...), Slack apps, Shopify, and senders that follow Standard Webhooks. For these, choose Paste the sender’s secret when you create the endpoint or rotate its secret, and paste the secret the sender shows you.
- The secret must be at least 32 characters.
- The Stripe and Slack formats always use the sender’s secret. Rotating one of these endpoints always takes a pasted secret.
- Marshal never shows a pasted secret back. Keep the original in the sender, or in your own secret store.
Set up common senders
Zendesk
Zendesk authenticates its webhooks with a Bearer token, so use the Shared token format.
- In Marshal, create an endpoint with the format Shared token (no signature). Confirm the prompt, then copy the Delivery URL and the token.
- In Zendesk, open Admin Center > Apps and integrations > Webhooks and select Create webhook.
- Set Endpoint URL to the Marshal delivery URL, Request method to
POST, and Request format toJSON. - Set Authentication to Bearer token and paste the Marshal token.
- Save the webhook and send a test. The delivery shows as
Acceptedon the Deliveries tab.
Stripe
Stripe creates the signing secret when you add a webhook endpoint, so you give Stripe the URL first and bring its secret back to Marshal.
- In Marshal, create an endpoint with the format Stripe.
- Marshal shows the Delivery URL and a field, Paste the signing secret from the sender. Copy the URL.
- In the Stripe Dashboard, open Developers > Webhooks and select Add endpoint. Paste the delivery URL, pick the events you want, and save.
- On the new Stripe endpoint, select Reveal under the signing secret and copy it. It starts with
whsec_. - Paste it into the Marshal field and select Save secret.
You can also add the Stripe secret later: open the endpoint, select Rotate secret, and choose Paste the sender’s secret.
Slack
Slack shows an app’s signing secret on the app’s Basic Information page before you set any request URL. Copy it there, then create the Marshal endpoint with the format Slack and paste the secret in the same step.
Change the format later
The owner can change an endpoint’s format without a new URL or a new secret, with PATCH /api/webhooks/endpoints/{id} and a new signature_format. Use this when you move your sender to a different library. Every change is recorded in the audit log, including whether the endpoint still refuses replays, so a move to Body only or Shared token is always visible. After a move to Stripe or Slack, rotate the secret and paste the sender’s own.
Naming your events
Marshal reads the event name from the X-Event-Type header. All formats use the same header. If you send none, the event name is *.
This matters for senders that cannot add headers, such as Stripe, Slack or Linear. Their deliveries always arrive as *. A trigger can still bind to * and use a condition to sort the events.
Use names with a family prefix, such as invoice.paid, invoice.refunded, and deal.created. A trigger can then bind to:
- one event:
invoice.paid - a family:
invoice.*(a bareinvoicemeans the same) - everything:
*
Only a trailing .* and a bare * are wildcards. A name without a dot, such as push, has no family.
What comes back
| Status | Meaning |
|---|---|
202 | Accepted and queued. The body carries a message_id. |
404 | The token is unknown or revoked, or the request did not verify. |
400 | The body could not be read. |
413 | The body is over 10 MiB. |
500 | The relay could not queue the event. Retry later. |
A 404 does not say which problem it was. If it did, anyone with a guessed URL could confirm that the URL exists. While you build the sender, check in this order: the timestamp is in Unix seconds (not milliseconds), your clock is within five minutes, you signed <timestamp>.<body> and not the body alone, and you signed the exact bytes you sent.
See what happened to a delivery
Marshal records accepted and refused requests in one timeline, newest first. Open an endpoint on the Receivers tab to see its activity and recent history. The Deliveries tab lists deliveries across your endpoints, with filters for source, outcome, and event type.
Each refused request shows the real reason, so the owner can fix the sender even though the sender only sees a 404:
| Reason shown | What to change |
|---|---|
| Bad signature | The secret, or the bytes you signed. Sign the exact bytes you send. |
| Stale timestamp | Almost always the sender’s clock. The window is five minutes either way. |
| Unsupported format | Change the endpoint’s signature format to the one the sender uses, or send to a different endpoint. |
Rules for what Marshal keeps:
- Delivery records are kept for 7 days.
- The body of each accepted delivery is stored for every custom endpoint, so you can preview trigger conditions against real events. Stored bodies age out with the delivery record after 7 days.
- The stored body of a delivery is cut at 256 KiB. The record notes the original size.
- The body of a refused request is not kept unless the endpoint’s owner turns on Keep the body of refused requests. A refused request is unverified, so it can contain anything. Turn it on while you set up the sender, then turn it off.
- Marshal stores at most 50 refused requests per endpoint. The timeline says so when older ones were dropped.
- Who can open a stored body: any member for an org-wide endpoint, the owner for a personal one, and team members for a team endpoint. An admin sees a personal endpoint’s metadata but not its bodies. Opening a body is recorded in the audit log.
Rotating your secret
The owner can issue a new secret without changing the URL. Select the endpoint, then Rotate secret. Marshal shows the new secret once. To use a secret your sender made, choose Paste the sender’s secret and paste it. Stripe and Slack endpoints always rotate this way.
By default the old secret keeps working for 24 hours. During that time either secret verifies, so you can deploy the new one without losing a delivery. If you rotate again inside the 24 hours, the older secret stops working at once. There is only ever one retiring secret.
If a secret has leaked, select Revoke the old secret immediately when you rotate. The old secret stops working now. Your sender fails until it has the new secret. That is the point.
Delete an endpoint
Select the endpoint, then Delete. The URL and secret stop working for good, and this cannot be undone. Any trigger bound to the endpoint stops firing and is disabled. The trigger is not deleted. Point it at another endpoint to turn it on again.
Reaching a deputy
A delivery does nothing by itself. A mission trigger turns it into a run. Add the trigger in the mission’s trigger list with Event:
- Source:
Custom webhook - Endpoint: the endpoint to listen to. This is required. A trigger on a custom endpoint must name one.
- Event: the event name you send in
X-Event-Type, a family, or* - Condition (optional): a condition over the payload, for example
event.amount > 1000
Which deputy can listen to which endpoint:
- A personal endpoint can be used only by its owner’s own personal deputy. An org-wide deputy and a team deputy cannot listen to a personal endpoint.
- Every other endpoint, org-wide or team, can be chosen by a team deputy or an org-wide deputy.
Two deliveries with the same id, event_id, delivery_id, webhookId, or eventId start the mission once, so a retry on your side is safe. If a delivery has none of these, Marshal compares a hash of the whole payload, so two byte-identical deliveries count as one.
For schedules, conditions, and how event data reaches the agent, see Schedules and triggers.
Connected apps: GitHub and Slack
GitHub and Slack also deliver events through the relay, but they are not endpoints. Each org has one connection per app, and triggers on them do not name an endpoint.
-
GitHub. An admin connects GitHub from one GitHub card on Settings > Webhooks, with two paths: Marshal’s GitHub App or your own GitHub App. This needs the GitHub package installed from the Marketplace. See Integrations.
Marshal’s GitHub App. Marshal’s App sends all events to one shared relay address, and Marshal finds your org from the installation inside each event. Select Connect, sign in to GitHub if asked, and pick an installation to link. Webhook delivery and pull request reads for Impact start together. To use another account, select Install on another account. GitHub opens its install page and brings you back to the card.
Your own GitHub App. Select Install on GitHub, sign in if asked, and install. The card shows Active. See Use your own GitHub App.
Linked installations. On the Receivers tab, Linked installations shows the installations of Marshal’s App tied to your org. Marshal checks with your own GitHub sign-in that you can see the installation and that you own the GitHub account it is on: an owner of the GitHub organization, or the personal account itself. An installation links to one Marshal org at a time. A connection set up earlier shows Not verified with Verify and link. Select it to give Marshal read access. Unlink stops pull request reads through an installation and leaves webhook delivery as it is.
-
Slack. Slack delivers button clicks from approval messages. You connect the workspace itself on Settings > Slack, not here.
For connected apps, the Keep event payloads for 7 days option lets you test trigger conditions against real events. It is on by default. If you turn it off, Marshal stops storing new bodies and you cannot preview a condition against new events. Bodies already stored age out after 7 days.