# Webhook

Screenshots on this page are taken from the Chinese UI. Menu, field and button names in the text use the English UI labels.

Webhooks let third-party systems proactively notify this system: the platform generates a dedicated invocation address for each Webhook, the third-party system calls that address with the agreed HTTP method, and the platform runs the corresponding dynamic logic and returns the logic's return value as a JSON response.

Typical scenarios: an e-commerce platform pushing an order notification after payment, a payment platform pushing payment results, a device gateway pushing alarms, and so on.

TIP

Since 1.0, the old "system integration" (Incoming/Outgoing integrations, Enable Logic, integration execution user) has been removed. Incoming integrations are replaced by the Webhooks on this page; outgoing integrations have no direct replacement. For how to migrate, see Migrating from Outgoing Integrations.

# Target Audience

The target audience of this document is: developers and implementers of this system

# Webhook List

Menu: Development > Integration > Webhook

Webhook list

A Webhook has the following fields:

UI label Property Description
Name name Name, unique within the tenant, cannot be changed after creation (read-only in the edit form)
Description description Description
Url url Identifier in the invocation address, generated automatically on creation (UUID), unique within the tenant
Http method httpMethod Invocation method; each Webhook can use only one: GET or POST
Active active Whether the Webhook is active
Effective date effectiveDate Start of the validity period, can be empty
Expiry date expiryDate End of the validity period, can be empty
Core logic coreLogic The dynamic logic run when a call is received
Exec records execRecords Execution records, see Execution Records

A Webhook runs only when all of the following hold: Active is on; Effective date is empty or earlier than the current time; Expiry date is empty or later than the current time.

The Http method column in the list currently shows -. This is a UI issue in the current version; the actual value is the one in the creation request.

# Creating a Webhook

UI issue in the current version

In 1.0.0-beta18, the Http method dropdown in the create and edit forms loads no options, while the form marks Url as required, so a new Webhook currently cannot be saved from the UI (the screenshot in step 2 below shows this state). Until the platform fixes this, create it through the REST API as described after step 2.

  1. In Development > Logics > Logics, create the dynamic logic that the Webhook will run. There is no logic type dedicated to Webhooks, so any logic type will do; choose GROOVY_CODE as the engine. For how to write the logic, see Injected Variables and Return Value below.
  2. On the Webhook list page, click Create, fill in the name, description and HTTP method, turn on Active, select the core logic created in the previous step, and save. The platform generates the Url automatically on save.

Create Webhook

In the screenshot, Http method is empty and Url is marked as required, so clicking Save / Close cannot save; this is the UI issue described above. The dialog title shows the domain class name DynamicWebhook, which is also a UI issue in the current version. Until the platform fixes this, create the Webhook through the REST API (first log in as described in Platform API to get a token):

curl -X POST http://<server address>/api/data/DynamicWebhook \
  -H 'Authorization: Bearer <access_token>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"ๆŽฅๆ”ถ่ฎขๅ•้€š็Ÿฅ","description":"็”ตๅ•†ๅนณๅฐๅœจ่ฎขๅ•ๆ”ฏไป˜ๅŽๆŽจ้€้€š็Ÿฅ","httpMethod":"POST","active":true,"coreLogic":{"id":<dynamic logic id>}}'
1
2
3
4

data.url in the response is the automatically generated address identifier.

# Invocation Address

The invocation address of a Webhook is:

http://<server address>/api/webhook/<Url>
1

where <Url> is the value of the Url column in the list. The invocation method must match Http method. The Webhook endpoint does not require login, and anyone who knows the address can call it, so keep the address as secret as a key and regenerate it when necessary.

Taking "ๆŽฅๆ”ถ่ฎขๅ•้€š็Ÿฅ" as an example (Http method is POST):

curl -X POST http://<server address>/api/webhook/9a72bdc1-bc78-469d-83e3-8fa976afc4ce \
  -H 'Content-Type: application/json' \
  -d '{"orderNo":"SO20260923001","amount":1280.00}'
1
2
3

Returns:

{"webhook":"ๆŽฅๆ”ถ่ฎขๅ•้€š็Ÿฅ","orderNo":"SO20260923001","received":true,"receivedAt":"2026-09-23T13:33:39.078338160Z[UTC]"}
1

# Injected Variables

When the core logic runs, the following injected variables are available:

Variable Type Description
webhook tech.muyan.dynamic.integration.DynamicWebhook The Webhook being called
headers Map<String, String> Request headers, with lowercase keys, for example headers['x-signature']
parameters Map<String, Object> Request parameters, see the notes below
requestTime java.time.ZonedDateTime Time the request was received
requestUrl String The request address received by the backend, without the queryString and without the /api prefix. The host name comes from the Host forwarded by nginx, without the port, for example http://localhost/webhook/<Url>
application grails.core.GrailsApplication The current grails application context
log Closure<?> log closure for printing execution logs; logs are written to the execution record
  • When Http method is GET, parameters holds the URL parameters, of type grails.web.servlet.mvc.GrailsParameterMap. Besides the parameters passed by the caller, it also contains three keys added by platform routing: controller, action and webhookKey (in practice, ?orderNo=SO1&amount=10 yields [orderNo:SO1, amount:10, controller:dynamicWebhook, action:get, webhookKey:<Url>]). Skip them when iterating over the parameters.
  • When Http method is POST, parameters is the JSON request body, of type org.grails.web.json.JSONObject. The caller must send Content-Type: application/json; a form-encoded (application/x-www-form-urlencoded) request body is not parsed and parameters is empty.

WARNING

The Webhook endpoint does not require login, so there is no current user in the logic (no userContext). If you need to verify the caller, agree on a request header (for example a signature), read and verify it through headers in the logic, and throw an exception if verification fails.

# Return Value

The core logic should return a Map<String, Object>. The platform converts it to JSON and returns it to the caller as is, with HTTP status code 200. When the logic throws an exception, the platform returns HTTP 500 with the exception message text as the response body.

Example core logic used by "ๆŽฅๆ”ถ่ฎขๅ•้€š็Ÿฅ":

log("ๆ”ถๅˆฐ่ฎขๅ•้€š็Ÿฅ๏ผš${parameters}")
String orderNo = parameters.orderNo
if (!orderNo) {
  throw new IllegalArgumentException('็ผบๅฐ‘่ฎขๅ•ๅท orderNo')
}
// Put the business processing here, for example updating the order status
return [
  received  : true,
  webhook   : webhook.name,
  orderNo   : orderNo,
  receivedAt: requestTime.toString(),
]
1
2
3
4
5
6
7
8
9
10
11
12

# HTTP Status Codes

The following responses were observed in each scenario:

Scenario Status code Response body
Address not found, or the invocation method does not match Http method 404 Webhook not found
Webhook is not active or outside its validity period 500 Webhook is not active or expired
Core logic throws an exception 500 The exception message, for example ็ผบๅฐ‘่ฎขๅ•ๅท orderNo
Core logic runs successfully 200 JSON of the core logic's return value

WARNING

Do not base business decisions on the message text in the response body; these texts are not guaranteed to stay the same after platform upgrades.

# Regenerating the Invocation Address

If an invocation address leaks, you can regenerate it: in the Webhook list, select the Webhooks to handle, click Regenerate URL, and click OK in the confirmation dialog. The platform generates a new Url for the selected Webhooks and the old address becomes invalid (with a delay of up to 10 minutes, see Caching and When Changes Take Effect). All callers must switch to the new address.

Regenerate invocation address

The success message after the operation is a half sentence in Chinese, "ๅทฒ้‡ๆ–ฐ็”Ÿๆˆไผ ๅ…ฅ้›†ๆˆๆŽฅๅฃ็š„" (also shown in Chinese in the English UI). This is a UI issue in the current version and does not affect functionality.

# Caching and When Changes Take Effect

The platform caches Webhook definitions by address, and the cache expires 10 minutes after the first call. Therefore, after you modify a Webhook (including regenerating the address, turning off Active or changing the validity period), it can take up to 10 minutes for the change to take effect. In practice, after regenerating the address, the old address could still be called until the cache expired, and only then returned 404. Take these 10 minutes into account when an address leaks.

Changes to the core logic's code are not affected by this cache and take effect on the next call after saving.

# Execution Records

Except for requests whose address is not found (404), every call produces an execution record; an inactive Webhook also records a failed record. In the Webhook list, click the button in a row's Exec records column to view that Webhook's execution records:

Webhook execution records

UI label Description
Webhook The corresponding Webhook. In the current version this column shows the Webhook's id, not its name
Status Execution result: Success or Failed
Exec params Execution parameters (empty in the current version; see Exec result for the request parameters)
Start time Start time
Finish time Finish time
Exec result Request method, request parameters, request headers and the core logic's return value; a notice message when the Webhook is inactive. In the current version the "Request headers" part is empty in practice
Exec log Logs printed with log in the logic
Stack Trace Exception stack trace when an error occurs

There is no Webhook entry under the Exec History menu; the records can only be reached from the Webhook list.

WARNING

By design in the platform source code, the execution result records all request headers, including Authorization, Cookie and signature headers, and stores them in plain text in the database. In the current version this part is empty in practice, but it will be recorded once the platform fixes this, so still do not let callers carry long-lived credentials in Webhook requests.

# Migrating from Outgoing Integrations

The old outgoing integration (Outgoing) notified external systems when objects were created, updated or deleted. It was removed in 1.0 and has no direct replacement. To migrate: create object hooks of type AFTER_CREATE / AFTER_UPDATE / AFTER_DELETE for the corresponding domain class, and send the HTTP requests yourself in the hook's dynamic logic. For how to use object hooks, see Object Hooks.

TIP

The platform does not provide a migration script that converts old integration definitions to Webhooks. Before upgrading, recreate each old incoming integration as a Webhook in the new version, and notify callers of the new invocation addresses.

Last Updated: 9/24/2026, 2:27:35 PM