# 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

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.
- 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; chooseGROOVY_CODEas the engine. For how to write the logic, see Injected Variables and Return Value below. - On the Webhook list page, click
Create, fill in the name, description and HTTP method, turn onActive, select the core logic created in the previous step, and save. The platform generates theUrlautomatically on save.

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>}}'
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>
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}'
2
3
Returns:
{"webhook":"ๆฅๆถ่ฎขๅ้็ฅ","orderNo":"SO20260923001","received":true,"receivedAt":"2026-09-23T13:33:39.078338160Z[UTC]"}
# 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 methodisGET,parametersholds the URL parameters, of typegrails.web.servlet.mvc.GrailsParameterMap. Besides the parameters passed by the caller, it also contains three keys added by platform routing:controller,actionandwebhookKey(in practice,?orderNo=SO1&amount=10yields[orderNo:SO1, amount:10, controller:dynamicWebhook, action:get, webhookKey:<Url>]). Skip them when iterating over the parameters. - When
Http methodisPOST,parametersis the JSON request body, of typeorg.grails.web.json.JSONObject. The caller must sendContent-Type: application/json; a form-encoded (application/x-www-form-urlencoded) request body is not parsed andparametersis 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(),
]
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.

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:

| 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.