Webhook是系统中发生的事件的通知。当特定事件发生时,艾克索拉会向您的应用程序发送HTTP请求,其中会传输事件数据。它通常是JSON格式的POST请求。
事件示例:
当发生设定事件时,艾克索拉会通过Webhook通知您的系统。然后,您可以执行以下操作:
支付处理Webhook工作流示例:
注:
根据所使用的解决方案及其集成类型,Webhook集和交互顺序可能与示例中的不同。
艾克索拉Webhook集成视频指南:
使用艾克索拉产品和解决方案时的Webhook设置:
产品/解决方案 | 必需/可选 | Webhook的用途是什么 |
---|---|---|
付款 | 必需 |
|
游戏内商店 | 必需 |
|
游戏销售 | 可选 | 对于游戏密钥销售,用户验证和商品记入不是必需。如果想接收有关事件的信息(例如付款或订单取消),可以连接webhook。 如果连接Webhook,则必须处理所有传入的必需Webhook。 |
订阅 | 可选 | 接收有关创建、更新或取消订阅的信息。您也可以通过API请求信息。 |
网页商城 | 必需 |
|
Digital Distribution Hub | 必需 |
请参阅文档,了解如何为Digital Distribution Hub设置Webhook。 |
登录管理器 | 可选 |
接收事件信息:
有关设置Webhook的详细信息,请参阅登录管理器文档。 |
如果使用需要与Webhook交互的产品和解决方案,请在您的发布商帐户中启用并测试Webhook并设置Webhook处理。当特定事件发生时,Webhook会按顺序发送。因此,如果您不处理其中一个Webhook,则不会发送后续Web hook。下面列出了必需Webhook的列表。
在艾克索拉侧已设置了2种Webhook发送选项,用于处理网站上的商品购买和退货——支付和交易数据信息以及已购商品信息可以分开发送,也可以合并为一个Webhook 发送。
在合并Webhook中接收信息:
如果您在2025年1月22日之后在发布商帐户注册,您将在订单成功支付(order_paid
)
和订单取消(order_canceled
)
Webhook中收到所有信息。在这种情况下,您无需处理支付(payment
)和退款(refund
) Webhook。
在单独Webhook中接收信息:
如果您在2025年1月22日或之前在发布商帐户注册,您将收到以下Webhook:
payment
)和退款(refund
) Webhook,包含支付数据和交易详细信息。order_paid
)和订单取消(order_canceled
) Webhook,包含已购商品信息。您需要处理所有收到的Webhook。如需切换到新的合并Webhook接收方式,请联系您的客户成功经理或发送邮件至csm@xsolla.com。
为确保游戏内商店和支付管理功能正常运行,必须实现主要Webhook的处理。
如果接收合并Webhook:
Webhook名称和类型 | 描述 |
---|---|
用户验证 > 用户验证 (user_validation ) |
在支付流程的不同阶段发送,用于确保用户已在游戏中注册。 |
游戏服务 > 合并Webhook > 订单成功支付(order_paid ) |
包含支付数据、交易详情和已购商品信息。请使用Webhook中的数据为用户添加商品。 |
游戏服务 > 合并Webhook > 订单取消(order_canceled ) |
包含已取消支付的数据、交易详情和已购商品信息。请使用Webhook中的数据移除已购商品。 |
如果接收单独Webhook:
Webhook名称和类型 | 描述 |
---|---|
用户验证 > 用户验证 (user_validation ) |
在支付流程的不同阶段发送,用于确保用户已在游戏中注册。 |
付款 > 支付(payment ) |
包含支付数据和交易详细信息。 |
游戏服务 > 单独Webhook > 订单成功支付(order_paid ) |
包含已购商品信息。请使用Webhook中的数据为用户添加商品。 |
付款 > 退款 (refund ) |
包含支付数据和交易详细信息。 |
游戏服务 > 单独Webhook > 订单取消(order_canceled ) |
包含已购商品信息和已取消交易的ID。请使用Webhook中的数据移除已购商品。 |
如果您的应用程序侧实现了商品目录个性化,请设置对合作伙伴侧的目录个性化Webhook的处理。
要自动管理订阅计划,需要实现主要Webhook的处理:
user_validation
) —
在支付过程的不同阶段发送,以确保用户已在游戏中注册。payment
) —
在支付订单后发送,包含付款数据和交易详细信息。create_subscription
) — 支付Webhook已成功处理或用户购买了具有试用期的订阅时发送。它包含所购买的订阅
的详细信息和用户数据。使用该Webhook数据向用户添加订阅。update_subscription
) — 续订或更改订阅以及支付Webhook已成功
处理后发送。它包含所购买的订阅的详细信息和用户数据。使用该Webhook数据来延长用户的订阅或更改订阅参数。refund
) —
订单被取消后发送,包含取消的付款数据和交易详细信息。cancel_subscription
) — 退款Webhook已成功处理或订阅因其他原因被取消时发送。它包含有关订阅和用户数据的
信息。使用该Webhook数据扣除用户购买的订阅。要启用接收Webhook:
https://example.com
。您还可以指定在测试Web
hook的工具中找到的URL。注:
请使用HTTPS协议传输数据,不支持HTTP协议。
注:
要测试Webhook,可以选择任何专用网站(例如webhook.site)或平台(例如ngrok)。
注:
无法同时将Webhook发送到不同的URL。您可以在发布商帐户中执行的操作是先指定一个用于测试的URL,然后将其替换为真实的URL。
要禁用接收Webhook:
付款和商店部分的Webhook提供高级设置。单击获取Webhook按钮后,这些设置将自动显示在常规设置区块下方。
注意
如果未显示高级设置,请确保已在常规设置中连接Webhook接收,且您位于测试 > 付款和商店选项卡中。
在此部分,您可以设置在Webhook中接收额外信息。要实现此目的,请将相应开关设为启用状态。每个权限的行都会标明设置变更将影响哪些Webhook。
开关 | 描述 |
---|---|
显示保存的支付帐户的信息 | 有关保存的支付方式的信息在payment_account 自定义对象中传递。 |
显示通过保存的支付方式进行的交易的信息 | 信息在Webhook的以下自定义参数中传递:
|
将订单对象添加到Webhook | 有关订单的信息在支付Webhook的order 对象中传递。 |
仅发送不含敏感数据的必要用户参数 | Webhook中仅传递用户的以下信息:
|
发送自定义参数 | 自定义令牌参数的信息在webhook中传递。 |
显示银行卡BIN和后缀码 | Webhook中传递以下银行卡号的信息:
|
显示银行卡品牌 | 用于付款的银行卡的品牌。例如,Mastercard或Visa。 |
测试Webhook有助于确保己侧和艾克索拉侧的项目设置都正确。
如果Webhook设置成功,Webhook 设置部分下方会显示一个Webhook测试部分。
发布商帐户中的测试部分会根据Webhook接收选项而有所不同。
如果接收合并Webhook:
Webhook测试的选项卡名称 | Webhook名称和类型 |
---|---|
付款和商店 | 用户验证 > 用户验证 (user_validation ) |
游戏服务 > 合并Webhook > 订单成功支付(order_paid ) |
|
游戏服务 > 合并Webhook > 订单取消(order_canceled ) |
|
订阅 | 用户验证 > 用户验证 (user_validation ) |
付款 > 支付(payment ) |
如果接收单独Webhook:
Webhook测试的选项卡名称 | Webhook名称和类型 |
---|---|
商店 | 游戏服务 > 单独Webhook > 订单成功支付(order_paid ) |
游戏服务 > 单独Webhook > 订单取消(order_canceled ) |
|
付款 | 用户验证 > 用户验证 (user_validation ) |
付款 > 支付(payment ) |
|
订阅 | 用户验证 > 用户验证 (user_validation ) |
付款 > 支付(payment ) |
注:
如果测试部分出现测试未通过的警告,请在您的Webhook侦听器中检查Webhook响应设置。测试结果中指出了测试错误的原因。
示例:
您使用专门的网站webhook.site来进行测试。
测试对无效签名的响应部分显示了一个错误。
发生这种情况是因为艾克索拉发送了带有错误签名的Webhook,并期望您的处理程序用一个指出INVALID_SIGNATURE
错误代码的4xx
HTTP代码进行响应。
webhook.site对所有Webhook的响应中都发送一个200
HTTP代码,包括签名不正确的Webhook。由于无法获取预期的4xx
HTTP代码,因此测试结果报错。
下文将介绍合并Webhook使用场景的测试流程。
在付款和商店选项卡中,您可以测试以下Webhook:
要进行测试:
在Webhook测试部分,前往付款和商店选项卡。
在下拉菜单中,选择商品类型。如果所选类型的商品未在发布商帐户中设置,请单击:
填写以下必填字段:
单击测试Webhook。
系统会将包含指定数据的用户验证、订单成功支付和订单取消Webhook发送到提供的URL。每种Webhook类型的测试结果将显示在测试Webhook按钮下方。
如果在您的项目中启用了公共用户ID,还将看到用户搜索检查的结果。
对于每个Webhook,您需要配置处理两种情况:成功的情况和出现错误的情况。
在订阅选项卡中,您可以测试以下Webhook:
注:
要测试Webhook,您应该在发布商帐户>订阅>订阅计划部分中至少有一个已创建的订阅计划。
测试步骤:
0
。在指定的URL中,您将收到包含所填数据的Webhook。每个Webhook的测试结果(成功场景和错误场景)都在测试Webhook按钮下方显示。
Webhook侦听器是一个程序代码,允许在指定URL地址接收传入的Webhook、生成签名以及发送响应到艾克索拉Webhook服务器。
注:
您可以使用Pay Station PHP SDK 库,它包含用于处理Webhook的现成类。
在您的应用程序侧,实现从以下IP地址接收Webhook:
185.30.20.0/24
185.30.21.0/24
185.30.22.0/24
185.30.23.0/24
34.102.38.178
34.94.43.207
35.236.73.234
34.94.69.44
34.102.22.197
如集成了登录管理器 产品,请另外添加对来自以下IP地址的Webhook的处理:
34.94.0.85
34.94.14.95
34.94.25.33
34.94.115.185
34.94.154.26
34.94.173.132
34.102.48.30
35.235.99.248
35.236.32.131
35.236.35.100
35.236.117.164
限制:
为确保数据传输安全,您必须验证Webhook确实来自艾克索拉服务器,且在传输过程中未被篡改。为此,需要基于请求正文负载生成您自己的签名,并将其与传入请求的au thorization
标头中提供的签名进行比较。如果签名匹配,则Webhook是真实的,可以安全处理。
验证步骤:
从Webhook请求的authorization
标头中检索签名。标头格式为Signature <signature_value>
。
检索JSON格式的Webhook请求正文。
注意
请完全按照接收到的JSON负载使用。不要解析或重新编码负载,因为这会改变 格式并导致签名验证失败。
生成您自己的签名进行比较:
将您生成的签名与authorization
标头中的签名进行比较。如果匹配,则Webhook是真实的。
以下是C#、C++、Go、PHP和Node.js语言的签名生成实现示例。
POST /your_uri HTTP/1.1
host: your.host
accept: application/json
content-type: application/json
content-length: 165
authorization: Signature 52eac2713985e212351610d008e7e14fae46f902
{
"notification_type":"user_validation",
"user":{
"ip":"127.0.0.1",
"phone":"18777976552",
"email":"email@example.com",
"id":1234567,
"name":"Xsolla User",
"country":"US"
}
}
curl -v 'https://your.hostname/your/uri' \
-X POST \
-H 'authorization: Signature 52eac2713985e212351610d008e7e14fae46f902' \
-d '{
"notification_type":
"user_validation",
"user":
{
"ip": "127.0.0.1",
"phone": "18777976552",
"email": "email@example.com",
"id": 1234567,
"name": "Xsolla User",
"country": "US"
}
}'
using System;
using System.Security.Cryptography;
using System.Text;
public static class XsollaWebhookSignature
{
public static string ComputeSha1(string jsonBody, string secretKey)
{
// Concatenation of the JSON from the request body and the project's secret key
string dataToSign = jsonBody + secretKey;
using var sha1 = SHA1.Create();
byte[] hashBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
return Convert.ToHexString(hashBytes).ToLower();
}
public static bool VerifySignature(string jsonBody, string secretKey, string receivedSignature)
{
string computedSignature = ComputeSha1(jsonBody, secretKey);
return string.Equals(computedSignature, receivedSignature, StringComparison.OrdinalIgnoreCase);
}
}
#include <string>
#include <sstream>
#include <iomanip>
#include <openssl/sha.h>
class XsollaWebhookSignature {
public:
static std::string computeSha1(const std::string& jsonBody, const std::string& secretKey) {
// Concatenation of the JSON from the request body and the project's secret key
std::string dataToSign = jsonBody + secretKey;
unsigned char digest[SHA_DIGEST_LENGTH];
// Create SHA1 hash
SHA1(reinterpret_cast<const unsigned char*>(dataToSign.c_str()),
dataToSign.length(), digest);
// Convert to lowercase hexadecimal string
std::ostringstream hexStream;
hexStream << std::hex << std::setfill('0');
for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
hexStream << std::setw(2) << static_cast<unsigned int>(digest[i]);
}
return hexStream.str();
}
static bool verifySignature(const std::string& jsonBody, const std::string& secretKey, const std::string& receivedSignature) {
std::string computedSignature = computeSha1(jsonBody, secretKey);
// Timing-safe comparison
if (computedSignature.length() != receivedSignature.length()) {
return false;
}
volatile unsigned char result = 0;
for (size_t i = 0; i < computedSignature.length(); ++i) {
result |= (computedSignature[i] ^ receivedSignature[i]);
}
return result == 0;
}
};
package main
import (
"crypto/sha1"
"crypto/subtle"
"encoding/hex"
"strings"
)
type XsollaWebhookSignature struct{}
func (x *XsollaWebhookSignature) ComputeSha1(jsonBody, secretKey string) string {
// Concatenation of the JSON from the request body and the project's secret key
dataToSign := jsonBody + secretKey
// Create SHA1 hash
h := sha1.New()
h.Write([]byte(dataToSign))
signature := h.Sum(nil)
// Convert to lowercase hexadecimal string
return strings.ToLower(hex.EncodeToString(signature))
}
func (x *XsollaWebhookSignature) VerifySignature(jsonBody, secretKey, receivedSignature string) bool {
computedSignature := x.ComputeSha1(jsonBody, secretKey)
receivedSignatureLower := strings.ToLower(receivedSignature)
// Use constant time comparison to prevent timing attacks
return subtle.ConstantTimeCompare([]byte(computedSignature), []byte(receivedSignatureLower)) == 1
}
<?php
class XsollaWebhookSignature
{
/**
* Compute SHA1 signature from webhook JSON body and secret key
*
* @param string $jsonBody The raw JSON body from webhook
* @param string $secretKey The project's secret key
* @return string The lowercase SHA1 signature
*/
public static function computeSha1(string $jsonBody, string $secretKey): string
{
// Concatenation of the JSON from the request body and the project's secret key
$dataToSign = $jsonBody . $secretKey;
// Generate SHA1 signature
$signature = sha1($dataToSign);
return strtolower($signature);
}
/**
* Verify webhook signature using timing-safe comparison
*
* @param string $jsonBody The raw JSON body from webhook
* @param string $secretKey The project's secret key
* @param string $receivedSignature The signature from authorization header
* @return bool True if signature is valid, false otherwise
*/
public static function verifySignature(string $jsonBody, string $secretKey, string $receivedSignature): bool
{
$computedSignature = self::computeSha1($jsonBody, $secretKey);
// Use hash_equals for timing-safe comparison
return hash_equals($computedSignature, strtolower($receivedSignature));
}
}
?>
const crypto = require('crypto');
class XsollaWebhookSignature {
// IMPORTANT: jsonBody must be the raw JSON string exactly as received from Xsolla
static computeSha1(jsonBody, secretKey) {
// Concatenation of the JSON from the request body and the project's secret key
const dataToSign = jsonBody + secretKey;
// Create SHA1 hash
const hash = crypto.createHash('sha1');
hash.update(dataToSign, 'utf8');
// Convert to lowercase hexadecimal string
return hash.digest('hex').toLowerCase();
}
static verifySignature(jsonBody, secretKey, receivedSignature) {
const computedSignature = this.computeSha1(jsonBody, secretKey);
const cleanReceivedSignature = receivedSignature.toLowerCase();
// Check if signatures have the same length before using timingSafeEqual
if (computedSignature.length !== cleanReceivedSignature.length) {
return false;
}
try {
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'hex'),
Buffer.from(cleanReceivedSignature, 'hex')
);
} catch (error) {
// Return false if there's any error (e.g., invalid hex characters)
return false;
}
}
}
要确认收到Webhook,您的服务器必须返回:
200
、201
或204
HTTP代码。400
HTTP代码和问题描述。如果您的服务器出现临时问题,您的Webhook处理程序还可
以返回5xx
HTTP 代码。如果艾克索拉服务器未收到订单成功支付和订单取消Webhook的响应,或收到5xx
代码的响应,系统将按以下计划重新发送Webhook:
在首次尝试后的12小时内最多尝试发送20次Webhook。
如果艾克索拉服务器未收到支付Webhook或退款Webhook的响应,或收到5xx
代码的响应,系统也会以递增的时间间隔重新发
送Webhook。12小时内最多尝试12次。
注:
如果退款是由艾克索拉发起的,并且收到带有`5xx` HTTP代码的退款Webhook响应,付款仍会被退还。
如果艾克索拉服务器未收到用户验证Webhook的响应,或收到400
或5xx
代码的响应,则不会重新发送用户验证Webhook。在这种情况下,用户会看到错误提示,且系统不会发送支付和订单成功支付Webhook。
HTTP代码400的错误代码:
代码 | 消息 |
---|---|
INVALID_USER | 无效用户 |
INVALID_PARAMETER | 无效参数 |
INVALID_SIGNATURE | 无效签名 |
INCORRECT_AMOUNT | 金额不正确 |
INCORRECT_INVOICE | 发票不正确 |
HTTP/1.1 400 Bad Request
{
"error":{
"code":"INVALID_USER",
"message":"Invalid user"
}
}
注:
通知类型在notification_type
参数中发送。
Webhook | 通知类型 | 描述 |
---|---|---|
用户验证 | user_validation |
发送以检查用户是否存在于游戏中。 |
用户搜索 | user_search |
发送以根据公共用户ID获取用户信息。 |
支付 | payment |
用户完成支付流程时发送。 |
退款 | refund |
出于某些原因需要取消支付时发送。 |
部分退款 | partial_refund |
出于某些原因需要部分取消支付时发送。 |
AFS拒绝交易 | afs_reject |
交易在AFS检查过程中被拒绝时发送。 |
AFS更新的拦截列表 | afs_black_list |
AFS拦截列表发生更新时发送。 |
创建了订阅 | create_subscription |
用户创建订阅时发送。 |
更新了订阅 | update_subscription |
订阅发生续订或更改时发送。 |
取消了订阅 | cancel_subscription |
取消订阅时发送。 |
非续订订阅 | non_renewal_subscription |
状态设置为非续订时发送。 |
添加支付账户 | payment_account_add |
当用户添加或保存支付帐户时发送。 |
删除支付账户 | payment_account_remove |
用户从已保存的帐户中删除了支付帐户时发送。 |
Web商店中的用户验证 | - |
从Web商店网站发送以检查游戏中是否存在该用户。 |
合作伙伴侧目录个性化 | partner_side_catalog |
用户与商店交互时发送。 |
订单成功支付 | order_paid |
订单付款后发送。 |
订单取消 | order_canceled |
订单取消时发送。 |
争议 | dispute |
当提出新争议时发送。 |