Listener Configuration
Limited Preview: Webhook Triggers & Listener
Webhook Triggers and the Webhook Listener are currently in Limited Preview and may change without notice. Access is invite-only. Self-nomination form is here. Production use is approved but may be subject to interruptions or breaking changes.
New to webhooks? Use the Webhook Triggers Quickstart for end-to-end setup and a minimal example. This page focuses on listener capabilities — especially the Advanced Configurations.
Overview
A Listener is Moveworks’ receiver for incoming webhook requests. Creating a listener gives you a Webhook URL that external systems can send a request to; Moveworks validates the request, turns it into an event, and can trigger one or more plugins. When you create a listener, you get a Webhook URL to configure in the external system.
Recommended Settings
Protect the endpoint so only trusted senders can trigger your workflows. Secured listeners avoid stricter rate limits.
Security & rate limits. When first created, the listener is not secured. Anyone who knows the URL can attempt to send a request. Unsecured listeners are heavily rate-limited when compared to secured listeners to protect your org. An unsecured listener may be acceptable for demo and testing purposes but you should eventually secure your listener to lift these limits. What are the rate limits?
- Secured listener: 5 requests / second
- Unsecured listeners: 1 request / 10 seconds
Verification (secure your listener)
Protect the endpoint so only trusted senders can trigger your workflows. Why this matters? Without verification, anyone who knows your URL can attempt to trigger your workflow; unsecured listeners are heavily rate-limited compared to secured ones to protect against malicious attacks.
What you can configure
Moveworks supports two verification options:
- Verification Rules. Add one or more rules that either (a) validate request fields (“Rule-based”) or (b) verify a cryptographic signature (“Signature Verification”).
- Signature verification (HMAC) is recommended. Configure a shared secret to verify origin and integrity. Signature verification is the recommended, provider-standard way to validate origin and integrity for apps like Slack, ServiceNow, and Jira. [Examples below](Rate limits: Unsecured listeners are heavily rate-limited (e.g., 1 req / 10s) vs. much higher limits for secured listeners. Secure your listener before production.).
- Credential Verification Require a credential (e.g., API Key, OAuth 2.0 Client, JWT OAuth) on every request. Create a credential in Moveworks, then enable Enable Credential Verification in the Verification card. Typical header placements are shown in the table (X-Webhook-Token, Authorization: Bearer, or a OAuth token in the authorization header).
If a credential check fails, Moveworks rejects the request (401 Unauthorized) and surfaces an
ErrorSignatureVerificationFailed
class of error in logs and responses.

Creating and sending Moveworks Credentials
When Enable Credential Verification is selected every webhook request to a Moveworks listener must include authentication. You can authenticate using either an API Key or an OAuth2 Client Credentials token generated in Moveworks Setup → Credentials → Create.

API Key Credentials
Include your Moveworks API key as a Bearer token in the Authorization header of your webhook request:
Authorization: Bearer <YOUR_API_KEY>
Example (curl):
curl -X POST "https://api.moveworks.ai/webhooks/v1/listeners/{LISTENER_ID}/notify" \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"example":"payload"}'
OAuth2 Client Credentials
Obtain an access token by making a request to the Token URL with your Client ID and Client Secret:
- Token URL: https://api.moveworks.ai/oauth/v1/token
- Client ID: The client ID you generated in Moveworks Setup.
- Client Secret: The client secret you generated in Moveworks Setup.
- Grant Type:
client_credentials
Request (curl):
curl -X POST "https://api.moveworks.ai/oauth/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "client_secret=<YOUR_CLIENT_SECRET>"
Response:
{
"access_token": "<ACCESS_TOKEN>",
"token_type": "Bearer",
"expires_in": 3600
}
Use the returned access_token as a Bearer token in the Authorization header when calling your listener:
curl -X POST "https://api.moveworks.ai/webhooks/v1/listeners/{LISTENER_ID}/notify" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"example":"payload"}'
Field guide: mapping to the UI
When you add a Verification Rule > Validation Type > Signature Verification:
- Secret Shared By External System. The shared secret configured in the webhook provider (never logged)
- Signature Verification Hash Mode. Usually SHA-256 for modern providers; Inferred is available for WebSub’s X-Hub-Signature format.
- Verification Payload. The exact string to sign (often the raw request body). Some providers require concatenating the version/timestamp prefix before the body -- see Github examples.
- Verification Received Signature. How to extract the signature from headers (e.g., split sha256=abc…). Examples below.

Example: GitHub (HMAC SHA-256)
Let's walk through a specific example of how you would set this up with GitHub. See Github's docs for validating webhook deliveries for more detail.
A) In the GitHub UI (repo or org → Settings → Webhooks → Add webhook)
- Payload URL → paste your Moveworks Listener URL (from the Listener page).
- Content type → choose application/json (GitHub will POST the JSON payload as the body).
- Secret → enter a long, random string; save it somewhere safe (you’ll paste the exact same value into Moveworks). GitHub signs every delivery with this secret using HMAC-SHA256, putting the result in the
X-Hub-Signature-256
header assha256=<hex>
. - SSL verification → leave Enable SSL verification on.
- Events → pick what you need (e.g., “Just the push event” or “Let me select individual events”).
- Leave Active checked → Add webhook.
Tip for testing: On the webhook page, open a Recent delivery, view headers/body, and Redeliver as needed.

B) In the Moveworks Listener UI (Verification card → Add New)
Goal: Verify GitHub’s signature from X-Hub-Signature-256 against the raw request body using the same secret.
- Validation Type → Signature Verification
- Secret Shared By External System When Registering The Webhook → paste the exact Secret you set in GitHub.
- Signature Verification Hash Mode → SHA-256
- Verification Payload (what GitHub signed) → enter:
raw_body
- Verification Received Signature (strip the sha256= prefix) → enter:
headers["x-hub-signature-256"]
- _Note: Header names are case-insensitive; lowercase is fine. GitHub always sends the value in the form
sha256=<hex>
- Click Publish.
C) Quick end-to-end test
- In GitHub → Webhooks, click a recent delivery (or Redeliver a “ping”).
- You should see X-Hub-Signature-256 in the request headers; Moveworks should return 2xx if the signature matched. If it fails from logs, confirm the same secret on both sides and that you selected SHA-256.
Example: Rule-based validation (non-cryptographic)
Use Rule-based checks to assert properties about the incoming request. These are great as additional guardrails (e.g., anti-replay, header presence, content-type) and as a fallback for providers that don’t sign payloads. Rule-based checks do not replace signature verification.
In the Moveworks Listener UI: Verification card → Add New → Validation Type: Rule-based → paste one of the expressions below. Add multiple rules with Add New; all rules must pass.
- Anti-replay window (Slack/Stripe-style timestamp). Reject requests older than 5 minutes.
$TIME() - $INTEGER(headers["x-slack-request-timestamp"]) <= 60 * 5
- Change the header name to your provider’s timestamp header (e.g., t from Stripe-Signature).
- Require JSON content type. Only accept JSON bodies.
$LOWERCASE(headers["content-type"]) == "application/json"
- Require signature header to be present and well-formed. Fail fast if the expected signature header is missing or empty.
headers["x-hub-signature-256"] != null
AND "sha256=" IN $LOWERCASE(headers["x-hub-signature-256"])
One-Time Verification Challenge
This is a way for your provider to verify the webhook listener. This will not impact rate-limiting to protect your tenant.
Some providers (Okta, Zoom, Slack, etc.) send a one-time challenge during webhook registration. Your listener must 1) detect that the request is a challenge and 2) return the exact response the provider expects (often echoing or signing a token) within their timeout window.
How it maps to the UI
- Challenge Detection (DSL): Write a rule that’s true only for challenge requests (e.g., special header or payload field).
- Challenge Response Configuration: Set the HTTP Status, any Response Headers, and the Response Body (often JSON). Tip: set
Content-Type: application/json
when the provider expects JSON.

Example A — Okta (echo a header value)
Okta One-Time Verification Request Docs
What Okta does: After you create an Event Hook, Okta sends a single GET to your endpoint with header x-okta-verification-challenge
. Your service must return that value in the body as JSON:
{ "verification": "<value_from_header>" }
.
Configure in Moveworks:
Challenge Detection (DSL)
headers["x-okta-verification-challenge"] != null
Challenge Response Configuration
-
HTTP Response Status Code:
200 OK
-
HTTP Response Headers:
Content-Type: application/json
-
HTTP Response Body:
{ "verification": headers['x-okta-verification-challenge']
Notes
- Okta only uses this GET once; subsequent event deliveries are POSTs. Use the method + header to cleanly separate the two.
- If Okta reports “failed to validate,” double-check the header name, that you returned JSON, and that there’s no extra whitespace.
Example B — Zoom (CRC with HMAC SHA-256)
Validate your webhook and point docs with Zoom.
What Zoom does: When you validate or re-validate a webhook, Zoom sends a POST with event: "endpoint.url_validation"
and a payload.plainToken
. You must respond within 3 seconds with JSON containing:
- plainToken: the same value from the request, and
- encryptedToken: the HMAC-SHA256 of
plainToken
using your Zoom webhook secret (hex encoded).
Configure in Moveworks:
Challenge Detection (DSL)
$LOWERCASE(parsed_body.event) == "endpoint.url_validation"
AND parsed_body.payload.plainToken != null
Challenge Response Configuration
-
HTTP Response Status Code:
200 OK
-
HTTP Response Headers:
Content-Type: application/json; charset=utf-8
-
HTTP Response Body: (Assumes the response body supports expression interpolation and hashing.)
{ "plainToken": "${parsed_body.payload.plainToken}", "encryptedToken": "${$HMAC_SHA256($SECRET('ZOOM_WEBHOOK_SECRET'), parsed_body.payload.plainToken).$HEX()}" }
Notes:
- Use the same secret you configured for the Zoom app (store it as a Moveworks secret like
ZOOM_WEBHOOK_SECRET
). - Keep latency low—Zoom expects the CRC response in ~3 seconds.
- You can also add a Verification Rule (Signature Verification) for regular Zoom deliveries; the CRC handling is in addition to that.
Advanced settings (OPTIONAL)
Most orgs don’t need the settings below on day one; add them as your integration matures.
Event Filtering (ignoring noisy events)
What it does: We support filtering at both the listener level (drop noise before it is processed as an event in Moveworks) and the plugin level (conditional execution of a plugin). At the listener level, DSL filters act as a gatekeeper: if the filter evaluates to true
, the event is discarded before before being processed by Moveworks.
How it works: Write expressions against the request payload, headers, and query params to decide if Moveworks should create an event.
To understand how to use the DSL filters here let's start with an example payload. Suppose we’re listening to a Jira webhook for newly created issues. The log might look like this for an incoming request:
{
"error_message": "",
"headers": {
"map": {
"X-Jira-Event": "issue_created",
"X-Jira-Delivery": "def456",
"Content-Type": "application/json",
"User-Agent": "Atlassian-Jira-Hookshot/7.0"
}
},
"http_method": "POST",
"listener_metadata": {
"request_id": "aaaaa-bbbbb-cccc",
"listener_id": "xxxxx-yyyyy-zzzzz",
"listener_name": "My_Jira_Issue_Listener"
},
"parsed_body": {
"event_type": "issue_created",
"issue": {
"id": "JIRA-1234",
"summary": "Critical bug in login flow",
"priority": "High",
"status": "Open",
"assignee": "jdoe",
"created_at": "2025-08-17T12:30:00Z"
},
"project": {
"key": "ENG",
"name": "Engineering"
},
"user": {
"id": "56789",
"name": "Jane Doe",
"email": "[email protected]"
}
},
"query_params": {},
"raw_body": "<RAW_WEBHOOK_PAYLOAD>",
"received_at": "2025-08-17T12:30:00Z"
}
Paths in the event model
Moveworks represents the entire incoming HTTP request as a structured object. Given that, that the DSL paths you’ll reference look like this:
parsed_body
→ the parsed JSON payload (most typical)headers
→ all HTTP headersquery_params
→ any query string valuesraw_body
→ the unparsed body string (you likely will not use this)
Based on this some common listener-level filters might be:
Below are some specific event filter examples but see our full DSL Reference for more details on the syntax for MW DSL and other common examples.
Example filters on parsed_body
(most typical)
parsed_body
(most typical)Only process newly created issues
parsed_body.event_type == "issue_created"
Only open issues
parsed_body.issue.status == "Open"
High-priority tickets only
parsed_body.issue.priority == "High"
Tickets assigned to a specific user
parsed_body.issue.assignee == "jdoe"
Only tickets from the Engineering project
parsed_body.project.key == "ENG"
Filter by creation date (last 7 days)
parsed_body.issue.created_at.$PARSE_TIME() > $TIME().$ADD_DATE(0, 0, -7)
Example Filters on headers
headers
Why use headers?
- Some providers encode event type, signatures, or content-type in headers.
- Good for coarse routing before parsing payloads (or to enforce security checks).
Require a Jira delivery ID for traceability
headers.["X-Jira-Delivery"] != null
Block specific sender agent
"hookshot" NOT IN $LOWERCASE(headers.["User-Agent"])
Examples: Compounds
High-priority, open issues in ENG project only
parsed_body.issue.priority == "High"
and parsed_body.issue.status == "Open"
and parsed_body.project.key == "ENG"
Env = prod AND summary contains “security”
query_params.env == "prod"
AND "security" IN $LOWERCASE(parsed_body.issue.summary)
Why filter at the Moveworks listener-level at all? Some webhook providers can’t pre-filter events. If that is the case, listener event filters in Moveworks eliminate noise and keep downstream config simple.
Redaction (Logs)
Redact secrets and PII from webhook logs. Use this to hide auth headers or sensitive fields while retaining enough context to debug
(Add: similar to the plugin redaction. Say the same thing)
Deduplication
Prevent retries or duplicate sends from triggering the same workflows. (Google Docs example)
How it works You choose one or more Key Paths. For each request, Moveworks reads those fields from the incoming payload (in order), concatenates the values, and hashes them to form a dedup key. If the same key is seen again within the configured window, the request is dropped as a duplicate.
Use deduplication with Verification (HMAC + optional timestamp rule). HMAC proves origin/integrity; dedup protects against provider retries and benign replays.
Mapping to the UI
- Deduplication key(s) → Key Paths Add 1–N JSON paths (relative to the request payload’s
parsed_body
). Choose fields that uniquely and stably identify the event across retries. - Deduplication Window Choose how long Moveworks should remember a seen key (e.g., 5 mins, 10 mins, 30 mins, 1 hour).

Tip: Prefer a single provider-supplied event ID (or delivery ID) if available. If the provider does not supply one, combine a small set of fields that together make the event unique (e.g.,
action
+issue.id
+repository.full_name
).
Good key choice examples (by webhook provider)
Below, enter the Key Paths exactly as shown (one per row). Then pick a sensible Window.
Slack (Events API)
event_id
- Window: 5 - 10 mins.
- Note: Slack includes
event_id
at the top level of the Events API payload.- Edits: This isn't the best example. What is better? I only care if there was a message in the slack channel in the last 10 minutes. More frequently I don't care about.
ServiceNow (example: Incident updates)
event
data.sys_id
- Window: 5 - 10 mins.
- Use the record
sys_id
with the event type you’re subscribing to.
Zoom (Event Notifications)
event
event_ts
payload.object.id
- Window: 5 - 10 mins.
- Zoom includes an event type and timestamp; include a stable object ID for uniqueness.
Do / Don’t
Do
- Prefer a single, provider-assigned event ID or delivery ID when available.
- Keep the key minimal but unique; 1–3 fields is typical.
- Set a window that comfortably covers the provider’s retry schedule (10–30 mins is a safe default).
Don’t
- Don’t include fields that change on retry (e.g., “received_at”, request UUIDs assigned by intermediaries).
- Don’t use very short windows (e.g., 30s) unless you’re sure retries won’t extend past it.
- Don’t rely on large free-form objects (e.g., full body) unless necessary—small, stable fields are faster and less error-prone.
Payload Schema (Optional)
- Edits: we want to include the details about the mapper. Payload mapper is what people will want to use. Payload mapper.
What it does: Validates the structure and types of the incoming body (LP focus: JSON) so requests that don’t match fail fast. Also supports mapping fields for downstream use.
Here’s an example of what a minimal schema could look like for Github issue events:
Example schema: Here’s an example of what a minimal schema could look like for Github issue events:
JSON
{
"action": "string",
"issue": {
"title": "string",
"html_url": "string",
"user": { "login": "string" }
},
"repository": { "full_name": "string" }
}
JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"action": {
"type": "string"
},
"issue": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"html_url": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"login": {
"type": "string"
}
},
"required": [
"login"
]
}
},
"required": [
"title",
"html_url",
"user"
]
},
"repository": {
"type": "object",
"properties": {
"full_name": {
"type": "string"
}
},
"required": [
"full_name"
]
}
},
"required": [
"action",
"issue",
"repository"
]
}
Start from an actual payload (see Quickstart for how to grab it) and keep the schema minimal— only what your plugins need.
Updated 3 days ago