Skip to main content
listen(...), input(...), and waitPay(...) block the current plugin run until a matching event arrives, the wait times out, or the user sends the exit code. Payment plugins should handle timeout, cancellation, and amount mismatch explicitly.

listen(waitTime, recallTime?, scope?)

Wait for the next user input in the same chat and account. Timeout returns an empty string "".
waitTime
number
required
Wait time in milliseconds. The runtime normalizes it: default 60000 ms, minimum 1000 ms, and usually maximum 900000 ms.
recallTime
number | 'group'
default:"0"
Milliseconds to delay before recalling the user’s message after input is received. If the second parameter is directly 'group', the SDK treats it as scope and sets recallTime to 0.
scope
'group' | string
Pass 'group' to allow any member in the same group to answer. If omitted, only the current user is waited for.
await sender.reply('Send the verification code within 60 seconds')
const code = await sender.listen(60000)

if (!code) {
  await sender.reply('Timed out. Please start again.')
} else {
  await sender.reply(`Received: ${code}`)
}

input(waitTime, recallTime?, scope?)

input(...) is an alias of listen(...) with the same parameters and return value.
await sender.reply('Any member in the group can reply y within 60 seconds to confirm')
const answer = await sender.input(60000, 0, 'group')

if (answer !== 'y') {
  await sender.reply('Not confirmed.')
}
If the waited message contains media, input(...) returns the text content. Read media from sender.getMediaItems().
const { downloadAdapterFile } = require('./middleware.js')

await sender.reply('Please send a CSV file')
const text = await sender.input(60000)
const items = await sender.getMediaItems()
const csv = items.find((item) => item.type === 'file')

if (!csv) {
  await sender.reply(`No file received. Text received: ${text || 'empty'}`)
  return
}

const saved = await downloadAdapterFile(csv, { path: 'imports' })
await sender.reply(`Saved: ${saved.file_name}, size: ${saved.file_size} bytes`)

waitPay(exitcode, timeout, amount?)

Wait for a payment event or exit code. It uses the IM, account, chat, and user from the current runtime context to register the payment wait. If payment configuration is unavailable, it falls back to legacy payment-event waiting.
exitcode
string
required
Exit code. During the wait, if the user sends exactly the same text, the method returns that string and ends the wait. Pass an empty string to disable the user exit code.
timeout
number
required
Wait time in milliseconds. The runtime normalizes it by the input wait limit, usually up to 900000 ms.
amount
string | number
Expected payment amount. The SDK sends it to the runtime as expected_amount. undefined, null, or an empty string does not send this field.

What amount changes

After you pass a valid amount, the runtime:
  1. Writes the amount to payment wait metadata ExpectedAmount.
  2. Writes the amount to payment record expected_amount when payment storage is enabled.
  3. Adds expected_amount and amount_matched to the success object.
  4. Sets the transfer link amount to the expected amount for forced Alipay transfer link scenarios. If no amount is passed, the default is 0.01.
amount is not a universal hard filter for all payment channels. Different channels may still deliver payment events to the wait flow. Before delivery, access grant, or card issuing, check amount_matched === true in the returned object.

What amount does not change

  • Timeout still returns the string "timeout".
  • Concurrency conflicts still return the string "busy".
  • If the user sends exitcode, the method still returns that exit code string.
  • Without a successful payment object, it will not invent expected_amount or amount_matched.

Return values

"timeout"
Returned when the wait time is exhausted. Tell the user to start the flow again.
money
number | string
Received payment amount. The runtime also fills compatible Money. If an event only has received_amount, it uses that to fill money / Money.
Money
number | string
Uppercase amount field kept for legacy plugins. It is usually the same as money.
received_amount
number | string
Actual received amount in the payment event. Not all channels return this field.
expected_amount
number
Returned only when the normalized amount is greater than 0. It is the expected amount for this wait.
amount_matched
boolean
Whether the received amount matches the expected amount. The runtime compares money and expected_amount; a difference smaller than 0.001 is treated as matched. If either amount is missing or less than or equal to 0, this is false.
order_id
string
Order number generated by the payment wait flow or returned by the channel. When payment configuration is enabled, it usually looks like pay_<hex>.
trade_no
string
Payment channel transaction number. Its presence depends on the channel event.

Safe handling template

const result = await sender.waitPay('q', 120000, 19.8)

if (result === 'timeout') {
  await sender.reply('Payment timed out. Please start again.')
  return
}

if (result === 'busy') {
  await sender.reply('A payment wait is already active. Try again later.')
  return
}

if (result === 'q') {
  await sender.reply('Payment canceled.')
  return
}

if (!result || typeof result !== 'object') {
  await sender.reply('No valid payment result was received.')
  return
}

if (result.expected_amount && result.amount_matched !== true) {
  await sender.reply(`Amount mismatch. Expected ${result.expected_amount}, received ${result.money || result.received_amount || 'unknown'}.`)
  return
}

await sender.reply('Payment succeeded. Delivering now.')

FAQ

You only see expected_amount and amount_matched after a successful payment returns an object. If the method returns "timeout", "busy", or the exit code string, the return body is unchanged.
Yes. The runtime normalizes numbers, numeric strings, amounts prefixed with ¥ / , and comma-separated amounts into numbers. The JS helper sends expected_amount whenever amount is not undefined, null, or an empty string. If the runtime cannot parse it or the parsed value is less than or equal to 0, it will not add a valid expected_amount to the success object.
Do not assume that. It records the expected amount and returns amount_matched on success. Some channels or order flows may use the expected amount to help match payment events, but your plugin should still check amount_matched before delivery.

atWaitPay()

Check whether the current runtime has a payment wait flow.
MethodParametersReturns
atWaitPay()NonePromise<boolean>
if (await sender.atWaitPay()) {
  await sender.reply('A payment wait is already active. Please wait.')
}

Next steps

Events and media items

Learn how to read user-uploaded files after waiting for input.

Payment plugin permissions and config

See manifest headers and parameter declarations.
Last modified on June 3, 2026