Skip to content

03 · 回调通知(Webhook)

充值到账与提现完成由平台主动推送到商户登记的回调地址。回调是商户入账的权威数据源。


1. 推送方式

方法POST
地址商户在控制台登记的 callback_url(必须公网可达,禁止内网地址)
Content-Typeapplication/json
扫描周期每 5 秒扫描一次待推送队列

回调地址会做 SSRF 校验:必须是 http/https,且解析出的 IP 不能落在私有/回环网段。


2. 报文格式

json
{
  "verify_data": {
    "project_uid": "Ab3xY9Kq7Z",
    "type": "deposit",
    "uid": "0x60a7904f2299519a2952c53a330a03cede4c55e9158e89fc132463130965bcaf",
    "tx_hash": "0x60a7904f2299519a2952c53a330a03cede4c55e9158e89fc132463130965bcaf",
    "from_address": "0x93c18812f4955b713304abc3f90652d8b93cddde",
    "to_address": "0xf9c66b4c44943345881ade7bad4e096f6550b7e9",
    "address_uid": "Ab3xY9Kq7Z-100286",
    "amount": "100500000",
    "float_amount": "100.5000",
    "decimal": 6,
    "asset_id": 2,
    "asset_name": "usdt",
    "network_id": 1,
    "network_name": "Ethereum Mainnet",
    "status": "confirmed",
    "confirmed": true,
    "confirmed_at": "2026-08-10T03:00:40.086062Z"
  },
  "sig": "3ce7a1b0...9f4e1c00"
}

verify_data 字段说明

字段类型说明
project_uidstring项目标识
typestringdeposit(充值)或 withdraw(提现)
uidstring业务唯一键。充值时 = tx_hash;提现时 = 商户发起提现时传的 uid
tx_hashstring链上交易哈希。提现在交易未广播前可能为空
from_addressstring转出地址
to_addressstring转入地址(充值时是用户充值地址,提现时是收款地址)
address_uidstring充值地址绑定的用户 uid(提现回调为空字符串)
amountstring金额,最小单位(整数字符串)
float_amountstring金额,可读单位(如 "100.5000")。入账建议直接用这个
decimalnumber该币种在该网络的精度
asset_id / asset_namenumber / string币种,如 2 / usdt
network_id / network_namenumber / string网络,如 1 / Ethereum Mainnet
statusstring充值:pending / confirmed / timeout;提现:success / confirmed
confirmedbool是否已达到确认数
confirmed_atstring | null确认时间(RFC3339),未确认为 null

注意报文的 JSON 是缩进格式(pretty-printed),不是紧凑格式。 验签时不要直接对原始报文体做哈希,见第 4 节。


3. 应答规范(最重要)

平台根据你返回的 HTTP 状态码决定后续行为:

你返回平台行为
201判定为处理完成,停止推送该笔
200判定为尚未完成,按退避策略继续推送,直到收到 201 或达到最大次数
4xx判定为永久失败,立即停止推送(不再重试)
5xx / 超时 / 连接失败判定为临时失败,按退避策略重试

这套语义让商户可以「等到确认再确认收货」:

充值回调第 1 次:status=pending, confirmed=false   → 商户返回 200(还不入账)
充值回调第 2 次:status=pending, confirmed=false   → 商户返回 200
充值回调第 N 次:status=confirmed, confirmed=true  → 商户入账后返回 201(停止推送)

推荐的处理逻辑

1. 解析 verify_data + sig
2. 用平台公钥验签 → 失败则返回 401(4xx,平台停止推送并告警)
3. 幂等检查:该 (type, uid) 是否已处理 → 已处理直接返回 201
4. if type == "deposit":
       if status == "confirmed" && confirmed == true:
           入账(同一事务内写入流水 + 余额)
           return 201
       else:
           return 200          // 让平台继续推,直到确认
   if type == "withdraw":
       if status == "confirmed":
           标记提现完成
           return 201
       else:                    // status == "success",已广播未最终确认
           return 200
5. 处理过程中出现内部异常 → 返回 5xx,让平台重试

切勿在 confirmed=false 时就给用户加余额——链重组会导致这笔充值消失。


4. 验签

sig 是平台用平台私钥verify_data 的签名。验证步骤:

  1. 取出 verify_data 对象(不是整个报文,也不是原始字节)。
  2. 对其做递归键名排序后序列化为紧凑 JSON(无空格、无缩进)。
  3. sha256 得到 32 字节摘要。
  4. 用平台公钥做 secp256k1 验签(签名为 65 字节 r||s||v,验签时取前 64 字节)。

Go(使用 SDK,一行搞定):

go
ok, err := client.VerifyPlatformSignature(client.GetPlatformPublicKey(), callbackReq.VerifyData, callbackReq.Sig)

其它语言的实现见 04-签名规范.md 第 3 节「验证平台签名」。

sig十六进制、无 0x 前缀(验签实现应兼容带前缀的情况)。


5. 重试策略

退避公式10s × 2^retries,即 10s、20s、40s、80s、160s、…
最大间隔5 分钟(超过后固定为 5 分钟)
最大重试次数50 次
总时长4 小时

超过 50 次后该笔回调标记为 failed,平台不再自动推送。此时可以联系平台在控制台手动补推。


6. 触发时机

充值

  • 索引器在链上扫描到入账交易、写入充值记录后立即开始推送(此时 status=pendingconfirmed=false)。
  • 达到确认数(Ethereum / BSC / TRON 均为 6 个区块)后,后续推送的 status 变为 confirmed
  • 归集产生的内部转账(充值地址 → 归集地址)不会推送给商户。

提现

  • 只有 withdraw_status 变为 successconfirmed 时才会推送。
  • 归集到冷钱包产生的内部提现单不会推送。
  • ⚠️ 被拒绝(rejected)、失败(failed / error)的提现单不会产生回调。 商户需要通过控制台或人工对账处理这类单据;如需失败通知请联系平台评估。

7. 接收端示例(Go)

go
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"

	linkepay "github.com/linkepay/linkepay-sdk-go"
	"github.com/linkepay/linkepay-sdk-go/types"
)

var client *linkepay.Client

func init() {
	client = linkepay.NewClient(&types.Config{
		BaseURL:              "https://linkepay-api.bestxx.com",
		ProjectID:            "<PROJECT_UID>",
		PrivateKey:           "<MERCHANT_PRIVATE_KEY>",
		PublicKey:            "<MERCHANT_PUBLIC_KEY>",
		ApiKey:               "<API_KEY>",
		PayPlatformPublicKey: "0xc0b2388188f35c087400575393c124e9c459550f1dfcf848214f47637b599ee08a6b64876c4687e869d070d892b97071fd4c90b0a3491814823b4e97f8e7d50f",
	})
}

type callbackReq struct {
	VerifyData map[string]interface{} `json:"verify_data"`
	Sig        string                 `json:"sig"`
}

func handler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError) // 5xx → 平台重试
		return
	}
	defer r.Body.Close()

	var req callbackReq
	if err := json.Unmarshal(body, &req); err != nil {
		w.WriteHeader(http.StatusBadRequest) // 4xx → 平台停止推送
		return
	}

	// 1. 验签
	ok, err := client.VerifyPlatformSignature(client.GetPlatformPublicKey(), req.VerifyData, req.Sig)
	if err != nil || !ok {
		log.Printf("invalid callback signature: %v", err)
		w.WriteHeader(http.StatusUnauthorized) // 4xx,同时应触发告警
		return
	}

	typ, _ := req.VerifyData["type"].(string)
	uid, _ := req.VerifyData["uid"].(string)
	status, _ := req.VerifyData["status"].(string)
	confirmed, _ := req.VerifyData["confirmed"].(bool)

	// 2. 幂等
	if alreadyProcessed(typ, uid) {
		w.WriteHeader(http.StatusCreated) // 201,停止推送
		return
	}

	// 3. 只在确认后入账
	if typ == "deposit" && status == "confirmed" && confirmed {
		if err := creditUser(req.VerifyData); err != nil {
			log.Printf("credit failed: %v", err)
			w.WriteHeader(http.StatusInternalServerError) // 5xx → 平台重试
			return
		}
		w.WriteHeader(http.StatusCreated)
		return
	}

	if typ == "withdraw" && status == "confirmed" {
		if err := finishWithdraw(uid, req.VerifyData); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusCreated)
		return
	}

	// 还没到终态:返回 200,让平台继续推送
	w.WriteHeader(http.StatusOK)
}

func alreadyProcessed(typ, uid string) bool          { /* 查库去重 */ return false }
func creditUser(d map[string]interface{}) error      { /* 事务内加余额 + 写流水 */ return nil }
func finishWithdraw(uid string, d map[string]interface{}) error { return nil }

func main() {
	http.HandleFunc("/linkepay/callback", handler)
	log.Fatal(http.ListenAndServe(":8081", nil))
}

8. 排查清单

现象排查方向
收不到任何回调控制台的 callback_url 是否填写、callback_enabled 是否为 true;地址是否公网可达;是否被 SSRF 校验拦截(内网地址)
回调一直重复推送是否返回了 200 而不是 201;或返回体导致平台判定为未完成
回调突然不推了是否某次返回了 4xx(永久失败);或已达 50 次上限
验签失败是否对 verify_data 而不是整个报文验签;是否做了递归键排序;是否用了正确的平台公钥
收到重复入账商户侧幂等键是否用了 (type, uid);充值的 uid 就是 tx_hash

本文档描述 LinkePay 生产环境(主网)接口。接口如有变更以本文档为准。