Surfaces: Webhooks
Esta página aún no está disponible en tu idioma.
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.
Declaring a webhook surface
Section titled “Declaring a webhook surface”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"}Configuration options
Section titled “Configuration options”| 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) |
Providers
Section titled “Providers”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.
Subscription handshakes
Section titled “Subscription handshakes”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 answers200with the token astext/plain. Its notifications carry the same secret back asclientState, so one key covers both halves. - Meta sends
GET /outlook?hub.mode=subscribe&hub.challenge=...&hub.verify_token=...; mashin checkshub.verify_tokenfirst, then echoeshub.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.
Two credentials, two keys
Section titled “Two credentials, two keys”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 againsthub.verify_tokenon the handshake.secret:keys the HMAC that everyX-Hub-Signature-256is 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.
Cloud computer
Section titled “Cloud computer”Your organization has its own cloud host, and the webhook address names it:
https://myorg.mashin.cloud/webhooks/hooks/stripeThe 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.
Local computer
Section titled “Local computer”On your own machine there is one computer, so the address is the same shape on the local host:
http://localhost:9000/webhooks/hooks/stripeLocal computer with tunnel
Section titled “Local computer with tunnel”To let an external service reach a computer on your laptop, open a tunnel:
mashin tunnel stripe_handler.mashinThe 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/stripeAutomatic registration example
Section titled “Automatic registration example”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.
Payload mapping
Section titled “Payload mapping”The webhook payload is mapped to the machine’s accepts section. The mapping depends on the provider:
Stripe
Section titled “Stripe”// 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"}}
GitHub
Section titled “GitHub”// 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.
Generic
Section titled “Generic”For webhooks without a named provider, the entire request body is passed as input. Define your accepts to match the expected payload shape.
Signature verification
Section titled “Signature verification”When the surface declares a provider:, every notification is verified before the machine runs:
- Reads the signature from the provider’s header
- Computes the provider’s signature over the raw request body, using the secret named by
secret: - Compares it (constant-time) with the one presented
- 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.
Multiple webhook sources
Section titled “Multiple webhook sources”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.
Governance
Section titled “Governance”Every webhook delivery is governed:
- Signature is verified (if configured)
- Payload is validated against the
acceptscontract - Governance permissions are checked
- The machine executes
- A
SurfaceAccessevent is recorded with:webhooksurface - 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.
Next steps
Section titled “Next steps”- WebSocket - Real-time connections
- REST API - HTTP endpoints
- Surfaces overview - All surface types