Add a webhook endpoint to a store. Each store can have up to 20 webhooks across all channels.
POST /v1/actions/store/add-webhook
Authentication: API Key (owner or admin role required)
Request Body
| Field | Type | Required | Description |
|---|
storeId | string | Yes | Store ID (Short ID format STO_xxx) |
channel | string | Yes | One of http, feishu, discord, telegram, slack |
url | string | Yes | Target webhook URL (HTTPS; merchant ensures URL matches the chosen channel) |
events | string[] | Yes | Subscribed event types — e.g. ["order.completed", "refund.succeeded"]. Empty array means no events fire to this webhook. |
testMode | boolean | Yes | true = fires for test transactions; false = fires for production |
secret | string | null | No | Channel-specific credential (e.g. Telegram chat_id). Stored as opaque text. |
Example Request
import { WaffoPancake } from "@waffo/pancake-ts";
const client = new WaffoPancake({
merchantId: process.env.WAFFO_MERCHANT_ID!,
privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
// Standard HTTPS webhook (RSA-signed envelope)
const { webhook } = await client.webhooks.add({
storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw",
channel: "http",
url: "https://example.com/webhooks/pancake",
events: ["order.completed", "refund.succeeded"],
testMode: false,
});
Success Response (200)
{
"data": {
"webhook": {
"id": "11111111-2222-3333-4444-555555555555",
"storeId": "uuid-of-store",
"channel": "http",
"url": "https://example.com/webhooks/pancake",
"events": ["order.completed", "refund.succeeded"],
"testMode": false,
"secret": null,
"createdAt": "2026-05-07T00:00:00.000Z",
"updatedAt": "2026-05-07T00:00:00.000Z"
}
}
}
The returned webhook.id is a UUID, not a Short ID — webhook IDs are not in the IdPrefix scope. Pass it as-is to update-webhook and remove-webhook.
Response Fields
| Field | Type | Description |
|---|
id | string | Webhook UUID |
storeId | string | Owning store UUID |
channel | string | Webhook channel (http, feishu, discord, telegram, or slack) |
url | string | Target webhook URL |
events | string[] | Subscribed event types |
testMode | boolean | true for test transactions, false for production |
secret | string | null | Channel-specific credential (opaque text), null if unset |
createdAt | string | Creation timestamp (ISO 8601) |
updatedAt | string | Last update timestamp (ISO 8601) |
Errors
Retry policy: Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts).
| Status | errors[0].message | What it means | Recommended handling |
|---|
| 400 | Missing required field: storeId | storeId was not provided | Fix the request body, resubmit |
| 400 | Expected format: STO_xxx, got "..." | storeId Short ID could not be decoded | Fix the storeId format, resubmit |
| 400 | testMode must be a boolean | testMode is not a boolean | Pass a boolean value, resubmit |
| 400 | Invalid channel: must be one of http, feishu, discord, telegram, slack | channel is not in the allowed list | Use one of the allowed channels |
| 400 | Invalid URL format | url is not a valid URL | Fix the URL, resubmit |
| 400 | events must be a string array | events is not an array of strings | Pass a string array, resubmit |
| 400 | secret must be a string or null | secret is not string or null | Pass string or null, resubmit |
| 400 | Webhook limit reached (max 20 per store) | Store already has 20 webhooks | Remove an existing webhook first |
| 401 | Missing merchantId in request context | API Key authentication did not resolve a merchant | Verify API Key headers and signature |
| 403 | Not authorized to manage webhooks for this store | Merchant is not owner or admin of the store | Verify the merchant’s role on this store |
| 500 | Internal server error | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) |