Webhooks

Receive webhooks from external services with a public flow URL, and send signed outgoing webhooks from flow steps.

#Webhooks

By the end of this guide you'll have a flow with a public webhook URL that external services can call, and you'll know how to send signed webhooks to other systems from your own flows.

#Prerequisites

  • An EmuView project with the automation section available
  • Permission to create and execute flows
  • For the HTTP examples: an API key (sk-your-api-key)

#Steps

#1. Create a flow with a webhook trigger

Create a flow and choose the webhook trigger type. EmuView creates the public endpoint at the same time and assigns it a random token.

import { EmuView } from '@emuview/sdk';

const sdk = new EmuView({
	url: 'https://your-api.example.com',
	auth: { apiKey: 'sk-your-api-key' }
});

const { data: flow } = await sdk.automate.createFlow({
	name: 'Payment received',
	trigger_type: 'webhook',
	trigger_config: {
		allowed_methods: ['POST'], // add 'GET' to accept GET requests too
		require_auth: true, // callers must present the secret
		secret: 'whsec-a-long-random-string'
	}
});

The trigger settings:

Setting Default Description
allowed_methods ["POST"] HTTP methods the endpoint accepts. Other methods get a method-not-allowed error
require_auth false Require the shared secret on every request
secret none The shared secret callers must send in the Authorization header

Fetch the flow to see the endpoint token — the webhook URL is:

https://your-api.example.com/api/v1/automate/webhook/<token>

#2. Add operations that use the payload

The request body arrives as $trigger.body, and the HTTP method as $trigger.method. For GET requests, the query parameters become $trigger.body.

A minimal flow that records incoming payment events:

[
	{
		"key": "store_event",
		"type": "crud_create",
		"config": {
			"collection": "payment_events",
			"data": {
				"event_type": "{{$trigger.body.event}}",
				"amount": "{{$trigger.body.amount}}",
				"raw_payload": "{{$trigger.body}}"
			}
		}
	}
]
Important

Webhook-triggered flows run with a least-privilege webhook identity by default, so operations that touch protected collections will be denied. Set the flow's Run As policy to the flow owner or a specific role when the flow needs to write data.

#3. Publish and activate the flow

Triggers only fire for active flows with a published version:

await sdk.automate.publishFlow(flow.id, 'Initial version');
await sdk.automate.setFlowStatus(flow.id, 'active');

#4. Call the webhook

Point the external service at the URL, or test it yourself:

$ curl -X POST "https://your-api.example.com/api/v1/automate/webhook/9f3c2b1a-7e64-4d20-b8aa-51c07d3f2e96" \
    -H "Authorization: Bearer whsec-a-long-random-string" \
    -H "Content-Type: application/json" \
    -d '{"event": "payment.completed", "amount": 99.99}'

The endpoint responds immediately with 202 Accepted — the flow executes in the background:

{
	"success": true,
	"runId": "b7e0c9d4-2f81-4a36-9c15-8d4e6a7b0f23",
	"message": "Webhook received and flow execution started."
}

Use the runId with the runs API (or Automation → Runs in the dashboard) to inspect step-by-step results. The SDK also has a helper for triggering webhooks:

const result = await sdk.automate.triggerWebhook('9f3c2b1a-7e64-4d20-b8aa-51c07d3f2e96', {
	event: 'payment.completed',
	amount: 99.99
});
console.log(result.runId);

#5. Understand how the secret is checked

When require_auth is on, the caller sends the secret in the Authorization header — either Bearer <secret> or the bare secret. EmuView compares it in constant time, so the check doesn't leak information through response timing. Requests with a missing or wrong secret get a 401 and the flow does not run.

Warning

Without require_auth, anyone who knows the URL can run the flow. Treat the token as a secret, and enable require_auth for anything that writes data.

#Sending webhooks from a flow

To notify external systems, add a webhook_dispatch step. It POSTs a JSON payload and can sign the request with HMAC-SHA256:

{
	"key": "notify_partner",
	"type": "webhook_dispatch",
	"config": {
		"url": "https://hooks.partner.example.com/sveltesync",
		"payload": {
			"orderId": "{{$trigger.body.id}}",
			"status": "{{$trigger.body.status}}"
		},
		"secret": "whsec-shared-with-the-receiver"
	}
}

When a secret is set, each request carries two extra headers the receiver can verify. The signature header is the same one collection webhooks sign under, so one secret and one verifier serve both outbound channels:

Header Value
X-EmuView-Signature sha256=<hex> — the HMAC-SHA256 of the JSON payload, keyed with the secret
X-EmuView-Timestamp ISO timestamp of the dispatch

The payload defaults to the trigger body when omitted, and the request times out after 10 seconds unless you change timeout_ms. For requests that need other methods, headers, or auth schemes, use the http_request operation instead — see the operations reference.

The secret is write-only. Read a flow back and the secret reads •••••••• — a header value likewise, with its name kept. Send the mask again and the stored value is left as it is; send a real string to rotate it. GET /flows/:flowId/export masks for everyone, so an exported flow lands unsigned wherever it is imported until you supply the key there. See Credentials in a flow's config are masked.

#What you learned

  • A webhook trigger gives a flow a public URL at /api/v1/automate/webhook/<token>, created automatically with the flow.
  • The endpoint accepts the configured methods, optionally guards requests with a shared secret, and always responds 202 with a runId while the flow runs in the background.
  • The request body (or query string for GET) is available to every step as $trigger.body.
  • Webhook flows run with a least-privilege identity unless you configure Run As.
  • The webhook_dispatch operation sends outgoing webhooks with optional HMAC-SHA256 signatures.

#Next steps