跳转到内容
Developer Preview — APIs and language features may change before 1.0

Surfaces: Webhooks

此内容尚不支持你的语言。

The webhook surface turns a machine into a receiver for external events. When Stripe sends a payment notification, GitHub sends a push event, or Microsoft Graph sends a mail notification, the webhook surface receives it, validates the signature, and runs the machine.

machine stripe_handler
accepts
event_type as text, is required
data as map, is required
responds with
status as text
action_taken as text
behaves
decide route_event
when input.event_type is "payment_intent.succeeded"
run process_success
when input.event_type is "charge.failed"
compute handle_failure
{status: "failed", action_taken: "alert sent to billing team"}
otherwise
compute skip
{status: "ignored", action_taken: "none"}
flows
flow process_success
ask process_payment, using: "anthropic:claude-haiku-4-5"
with task "Summarize this payment and decide next actions.\n\nEvent: ${input.data}"
returns
status as text
action_taken as text
expresses
webhook
path: "/hooks/stripe"
auth: signature
provider: "stripe"
ensures
permissions
allowed to
model
network.http
verifies
test "successful payment is processed"
assuming process_payment {status: "processed", action_taken: "receipt sent"}
given {event_type: "payment_intent.succeeded", data: {amount: 1000}}
expect {status: "processed", action_taken: "receipt sent"}
test "failed charge alerts billing"
given {event_type: "charge.failed", data: {}}
expect {status: "failed", action_taken: "alert sent to billing team"}
Config Required Default Description
path Yes - URL path for the webhook receiver
method No post Request method the endpoint admits. any (or *) admits every method
provider No - Named provider, which decides how a request is verified
secret / secret_env No - Name of the environment variable holding the shared secret
verify_token / verify_token_env No - Name of the environment variable holding the subscription verify token, for a provider that uses a separate credential for its handshake
events No - Event names this webhook expects
auth No signature Surface authentication (see Surface Authentication)

Naming a provider is asking for verification. mashin then knows how that provider proves a request is genuine, and refuses anything it cannot verify: no secret configured, no signature, a wrong signature, and the machine never runs.

Provider How a notification is verified
"stripe" Stripe-Signature, HMAC-SHA256 with timestamp
"github" X-Hub-Signature-256, HMAC-SHA256
"slack" X-Slack-Signature, HMAC-SHA256 with timestamp
"paddle", "shopify", "twilio" HMAC signature over the raw body
"telegram" X-Telegram-Bot-Api-Secret-Token, static shared token
"microsoft" clientState in the notification body equals your secret
"meta" X-Hub-Signature-256, HMAC-SHA256 keyed by your app secret

The secret: key names the environment variable that holds the shared secret. It is never the secret itself.

Microsoft Graph and Meta prove you own an endpoint before they will send anything to it. They send a GET carrying a challenge token, and the endpoint has to echo it back.

That handshake works without you declaring anything beyond the provider:

expresses
webhook
path: "/outlook"
provider: "microsoft"
secret: "OUTLOOK_CLIENT_STATE"
  • Microsoft Graph sends GET /outlook?validationToken=...; mashin answers 200 with the token as text/plain. Its notifications carry the same secret back as clientState, so one key covers both halves.
  • Meta sends GET /outlook?hub.mode=subscribe&hub.challenge=...&hub.verify_token=...; mashin checks hub.verify_token first, then echoes hub.challenge. A wrong verify token is refused.

The handshake never runs your machine. It is an ownership proof, answered by the runtime; your machine runs only on a notification that verifies. A GET to a webhook whose provider has no handshake (Stripe, GitHub) is not an endpoint at all, and neither is a GET to a webhook that did not declare method: get.

Meta is the case where one secret is not enough. The console gives you a verify token, which is what the handshake carries, and an app secret, which is what signs every notification. They are different values, so they get different keys:

expresses
webhook
path: "/instagram"
provider: "meta"
verify_token: "META_VERIFY_TOKEN"
secret: "META_APP_SECRET"
  • verify_token: is checked against hub.verify_token on the handshake.
  • secret: keys the HMAC that every X-Hub-Signature-256 is checked against.

Leave verify_token: off and the handshake is refused, with an error naming the key you are missing. It never falls back to secret:: authenticating a subscription with a credential the provider never sends there would be a hole, not a convenience. Your notifications still verify against secret: in the meantime, so the missing key costs you the subscription, not the surface.

Providers with one credential (Stripe, GitHub, Slack, Shopify, Twilio, Paddle, Telegram, Microsoft Graph) declare secret: alone and never see this key.

Registering webhooks with external services

Section titled “Registering webhooks with external services”

After declaring a webhook surface, you need a public URL for the external service to deliver to. Two options:

There is one address shape, everywhere: your host, then /webhooks, then the path you declared.

Your organization has its own cloud host, and the webhook address names it:

https://myorg.mashin.cloud/webhooks/hooks/stripe

The organization is part of the address, not decoration. A webhook carries no mashin login, so the host is the only thing in the request that says which organization it belongs to. The same path on the bare app host is refused.

On your own machine there is one computer, so the address is the same shape on the local host:

http://localhost:9000/webhooks/hooks/stripe

To let an external service reach a computer on your laptop, open a tunnel:

Terminal window
mashin tunnel stripe_handler.mashin

The CLI shows the public URL when the tunnel connects. The tunnel gets its own host and forwards the path through unchanged, so the address is the same shape again:

https://{subdomain}.tunnel.mashin.cloud/webhooks/hooks/stripe

A machine can register its own webhook URL using context.computer.webhook_urls:

machine outlook_bridge
accepts
event as map
responds with
subscription_id as text
behaves
ask register, from: "@mashin/actions/http/post"
url: "https://graph.microsoft.com/v1.0/subscriptions"
body: {
changeType: "created",
notificationUrl: context.computer.webhook_urls["/outlook"],
resource: "me/mailFolders('Inbox')/messages",
expirationDateTime: "2026-06-01T00:00:00Z"
}
returns
subscription_id as text
expresses
webhook
path: "/outlook"
provider: microsoft
verifies
test "registers the Graph subscription"
assuming register {subscription_id: "sub_123"}
given {event: {}}
expect {subscription_id: "sub_123"}

context.computer.webhook_urls is keyed by the path you declared, and each value is the full address for that path on whichever host this computer answers on: your organization’s cloud host, your local host, or the tunnel host. Hand it to the provider as-is.

If the computer is running in the cloud for an organization that has no cloud address yet, the key is absent rather than holding an address that answers nothing.

The webhook payload is mapped to the machine’s accepts section. The mapping depends on the provider:

// Incoming webhook payload
{
"type": "payment_intent.succeeded",
"data": {
"object": {
"amount": 5000,
"currency": "usd"
}
}
}

Maps to:

  • event_type = "payment_intent.succeeded"
  • data = {"object": {"amount": 5000, "currency": "usd"}}
// Incoming webhook payload (with X-GitHub-Event: push header)
{
"ref": "refs/heads/main",
"commits": [...]
}

The X-GitHub-Event header value is available as context.webhook.event_type.

For webhooks without a named provider, the entire request body is passed as input. Define your accepts to match the expected payload shape.

When the surface declares a provider:, every notification is verified before the machine runs:

  1. Reads the signature from the provider’s header
  2. Computes the provider’s signature over the raw request body, using the secret named by secret:
  3. Compares it (constant-time) with the one presented
  4. Rejects with HTTP 401 if it does not match, and records the refusal

There is no way past it. A request that cannot be verified, for any reason, is refused rather than admitted.

A machine can declare multiple webhook surfaces:

expresses
webhook
path: "/hooks/stripe"
provider: "stripe"
webhook
path: "/hooks/github"
provider: "github"

Or a single machine can handle multiple event types from one source using decide steps to route by event type.

Every webhook delivery is governed:

  1. Signature is verified (if configured)
  2. Payload is validated against the accepts contract
  3. Governance permissions are checked
  4. The machine executes
  5. A SurfaceAccess event is recorded with :webhook surface
  6. The external service receives an HTTP 200 acknowledgment

Rejected webhooks return HTTP 403 (governance denied) or HTTP 401 (signature invalid). Most webhook providers retry on non-2xx responses.