Tevau-Api文档
    • Get started
    • 签名说明
    • 交易说明
    • FAQ
    • 全量异常编码
    • 交易拒绝原因
    • 01. 客户
      • 1.1 创建客户
        POST
      • 1.2 查询客户
        POST
    • 02. KYC
      • 2.1 V1提交客户KYC信息
        POST
      • 2.2 获取活体认证URL
        POST
      • 2.3 查询活体认证结果
        POST
      • 2.4 查询客户KYC状态和信息
        POST
      • 2.5 [Sandbox] 模拟KYC审核
        POST
      • 2.6 创建文档文件
        POST
      • 2.7 上传文档文件
        POST
      • 2.8 V2提交客户KYC信息
        POST
    • 03. 卡片
      • 3.1 创建卡
        POST
      • 3.2 查询客户的卡
        POST
      • 3.3 调整卡余额
        POST
      • 3.4 查询卡详情
        POST
      • 3.5 绑定实体卡
        POST
      • 3.6 注销卡
        POST
      • 3.7 冻结卡
        POST
      • 3.8 解冻卡
        POST
      • 3.9 激活实体卡
        POST
      • 3.10 查询卡PAN-HTML
        POST
      • 3.11 查PIN码
        POST
      • 3.12 查询卡限额
        POST
      • 3.13 调整卡费用
        POST
      • 3.14 查询卡费用
        POST
      • 3.15 卡交易3DS授权确认、取消
        POST
      • 3.16 更新卡手机号
        POST
      • 3.17 更新卡邮箱
        POST
      • 3.18 修改卡Pin
        POST
      • 3.19 查询卡片Token列表
        POST
      • 3.20 激活卡片Google/Apple Token
        POST
      • 3.21 卡片交易限额(废弃)
        POST
      • 3.22 查询卡账户流水
        POST
      • 3.23 查询卡操作处理结果
        POST
      • 3.24批量卡充值任务提交
        POST
      • 3.25批量卡充值任务状态查询
        POST
    • 04. 物流信息查询
      • 4.1 物流信息查询
    • 05. 预付款
      • 5.1 查询企业账户余额
      • 5.2 查询预付款账户交易列表
      • 5.3 查询企业账户余额(Fiat)
    • 06. 交易
      • 6.1 查询交易记录
      • 6.2 查询单笔交易详情
    • 07. 模拟交易
      • 7.1 模拟授权 (Authorisation)
      • 7.2 模拟结算 (Clearing)
      • 7.3 模拟退款 (Refund)
      • 7.4 模拟退单 (Reversal)
      • 7.5 模拟3DS (3DS)
      • 7.6 模拟卡片状态
    • 08. Webhook
      • 8.1 KYC审核通知
      • 8.2 卡片结果通知
      • 8.3 调整卡余额的通知
      • 8.4 授权3DS通知
      • 8.5 交易通知
      • 8.6 物流状态变化通知
      • 8.7 Token通知
      • 8.8 代收代付WebHook通知
      • 8.9 风控冻结卡片结果通知
      • 8.10 绑定Google、Apple失败通知
      • 8.11 资金池充值到账通知
    • 09. Fiat代收代付
      • 9.1 查询代收/代付银行列表
      • 9.2 创建代收订单
      • 9.3 创建代付订单
      • 9.4 提现USDT
      • 9.5 查询代收/代付订单
      • 9.7 模拟代收/代付到账
    • 其他
      • [Sandbox] 修改WebHook-Url
      • [Sandbox] 查询手续费配置
      • [Sandbox] 修改手续费配置
      • JIT 授权交易
    • Schemas
      • 请求体
      • 请求体
      • BatchRechargeSubmitReq
      • RPageDataGetCardAccountDetailPageResp
      • FeeItem
      • BatchRechargeSubmitResp
      • PageDataGetCardAccountDetailPageResp
      • FeeConfigTestResp
      • BatchRechargeQueryResp
      • GetCardAccountDetailPageResp
      • FeeItemDetail
      • 响应体

    签名说明

    📌
    目前编码格式固定:utf-8
    API描述
    1.
    签名字符串为除了sign、x-nexus-api-key、versions之外的其他非空字段组成键值对;
    2.
    所有参与签名的字段,按字段名的ASCII码从小到大排序后,使用URL的键值对的格式(即key1=value1&key2=value2)拼接成字符串string
    如
    appId=xxx
    nonce=V6BC6WHMU1D2NGT17D959C4W6RQP3I0D
    timestamp=20250421111104
    userCode=54
    排序后的字符串:
    string="appId=companyAppId001&nonce=V6BC6WHMU1D2NGT17D959C4W6RQP3I0D&timestamp=20250421111104userCode=54"
    3.
    用商户的RSA私钥进行签名;
    RSAUtils.sign(string, cusRsaPrivateKey)
    如测试私钥
    cusRsaPrivateKey = "xxx";
    则上面字符串加密后的
    sign="DYtffEz/KTeHDOtG47pBbNntgJJEM2akmPRS2w9V43HDTSW5S52d8/k7596cO6/MF/IeEus3o9NLtqKMFwrZc0ju/qYHaSJbYL/YnPgs3peoI8xMTobge2bvIeEdbQXNxb0yxCQe5Im8Ndi5fITTfdFcz1ZM8+4T9M6MSDBnjfdpHpGV/KSgRFAeh71kP2PFYzt/g0NNqWVbeJCJVvStdYYumMis9fnY+quyD1XF8V+gymaQQWIxDKBXbMSpJuOgxjVuHE0jIe+x2dP8PNC/BlxspAFbdv1SRK7qOz7Ph83NPETm2RRigIA2SxLEw+ITC7PfI5kI6+PpZj29wk0KsQ==";
    4.
    请求接口成功后Nexus系统返回JSON字符串数据;
    💡
    签名注意事项
    所有参数(sign、x-nexus-api-key、versions字段除外)只要传了值(不包括空字符串)都要参与签名,否则验签无法通过!
    代码示例(JAVA版)
    含MAVEN-JAR包版本
    https://oss.tevaupay.com/tevau-nexus-test/Public_catalogue/demo.zip
    代码示例(Node.js版)
    代码示例(GO版)
    代码示例(python版)
    """
    getSortedString 的排序规则与 UserTestController 里的完全一致(顶层按 key 字母排序,
    嵌套对象要按 fastjson 默认 HashMap<String,Object> 的哈希桶顺序重排,而不是原始 JSON 顺序),
    这里直接复用同一套实现。
    """
    
    import base64
    import hashlib
    import json
    import random
    import string
    import urllib.error
    import urllib.request
    from datetime import datetime
    
    
    # ---------- DER 解析 + 纯标准库 RSA sign/verify(PKCS#1 v1.5) ----------
    
    def _read_length(data: bytes, pos: int):
      length = data[pos]
      pos += 1
      if length & 0x80:
          num_bytes = length & 0x7F
          length = int.from_bytes(data[pos:pos + num_bytes], "big")
          pos += num_bytes
      return length, pos
    
    
    def _read_tlv(data: bytes, pos: int):
      tag = data[pos]
      pos += 1
      length, pos = _read_length(data, pos)
      value = data[pos:pos + length]
      pos += length
      return tag, value, pos
    
    
    def _parse_pkcs8_rsa_private_key(der: bytes):
      """解析 PKCS8 PrivateKeyInfo(DER),取出 RSA 模数 n 和私钥指数 d"""
      _, pkcs8_seq, _ = _read_tlv(der, 0)  # 外层 SEQUENCE
      pos = 0
      _, _version, pos = _read_tlv(pkcs8_seq, pos)            # INTEGER version
      _, _alg_id, pos = _read_tlv(pkcs8_seq, pos)              # AlgorithmIdentifier
      tag, priv_key_octets, pos = _read_tlv(pkcs8_seq, pos)    # OCTET STRING
      if tag != 0x04:
          raise ValueError("expected OCTET STRING for privateKey")
    
      _, rsa_priv_seq, _ = _read_tlv(priv_key_octets, 0)  # RSAPrivateKey ::= SEQUENCE
      ppos = 0
      _, _rsa_version, ppos = _read_tlv(rsa_priv_seq, ppos)
      _, modulus_bytes, ppos = _read_tlv(rsa_priv_seq, ppos)     # n
      _, _pub_exp_bytes, ppos = _read_tlv(rsa_priv_seq, ppos)    # e(签名不需要)
      _, priv_exp_bytes, ppos = _read_tlv(rsa_priv_seq, ppos)    # d
    
      n = int.from_bytes(modulus_bytes, "big")
      d = int.from_bytes(priv_exp_bytes, "big")
      return n, d
    
    
    # SHA-1 的 DigestInfo DER 前缀(PKCS#1 v1.5 签名固定结构的一部分)
    _SHA1_DIGEST_INFO_PREFIX = bytes.fromhex("3021300906052b0e03021a05000414")
    
    
    def rsa_sign(data: str, private_key_b64: str) -> str:
      """对应 Java: RSAUtils.sign(data, privateKey) —— PKCS8 私钥"""
      n, d = _parse_pkcs8_rsa_private_key(base64.b64decode(private_key_b64))
      k = (n.bit_length() + 7) // 8  # RSA 模数字节长度
    
      digest_info = _SHA1_DIGEST_INFO_PREFIX + hashlib.sha1(data.encode()).digest()
      ps_len = k - 3 - len(digest_info)
      if ps_len < 8:
          raise ValueError("RSA modulus too small for SHA1 PKCS#1 v1.5 padding")
      em = b"\x00\x01" + b"\xff" * ps_len + b"\x00" + digest_info
    
      m = int.from_bytes(em, "big")
      s = pow(m, d, n)
      return base64.b64encode(s.to_bytes(k, "big")).decode()
    
    
    # ---------- getSortedString 的等价实现(与 UserTestController 版本一致,直接复用) ----------
    
    class OrderedObj(list):
      """标记这是 JSON 对象(保留原始字段出现顺序),用于和 JSON 数组(普通 list)区分开"""
      pass
    
    
    def _parse_ordered(json_str: str):
      return json.loads(json_str, object_pairs_hook=OrderedObj, parse_float=str, parse_int=str)
    
    
    def _java_string_hash(s: str) -> int:
      """复现 java.lang.String.hashCode():h = 31*h + c,按 32 位位模式环绕"""
      h = 0
      for c in s:
          h = (31 * h + ord(c)) & 0xFFFFFFFF
      return h
    
    
    def _java_spread_hash(s: str) -> int:
      """复现 java.util.HashMap.hash(key):h ^ (h >>> 16)"""
      h = _java_string_hash(s)
      return (h ^ (h >> 16)) & 0xFFFFFFFF
    
    
    def _simulate_java_hashmap_order(pairs):
      """
      复现 fastjson 嵌套 JSONObject 默认使用的普通 HashMap<String,Object>(非 LinkedHashMap)
      在 entrySet() 遍历时的顺序:按 key 的 spread hash 分桶,并模拟 JDK8+ HashMap 扩容时
      “低位/高位链表拆分”算法,而不是原始 JSON 字段顺序。
      """
      capacity = 16
      threshold = 12  # capacity * 0.75
      buckets = [[] for _ in range(capacity)]
      size = 0
    
      for key, value in pairs:
          h = _java_spread_hash(key)
          idx = h & (capacity - 1)
          buckets[idx].append((key, value, h))
          size += 1
          if size > threshold:
              new_cap = capacity * 2
              new_buckets = [[] for _ in range(new_cap)]
              for j in range(capacity):
                  chain = buckets[j]
                  if not chain:
                      continue
                  lo, hi = [], []
                  for entry in chain:
                      (lo if entry[2] & capacity == 0 else hi).append(entry)
                  new_buckets[j] = lo
                  new_buckets[j + capacity] = hi
              buckets = new_buckets
              capacity = new_cap
              threshold = capacity * 3 // 4
    
      result = []
      for bucket in buckets:
          for key, value, _ in bucket:
              result.append((key, value))
      return result
    
    
    def _serialize_value(value) -> str:
      if value is None:
          return "null"
      if value is True:
          return "true"
      if value is False:
          return "false"
      if isinstance(value, OrderedObj):
          ordered = _simulate_java_hashmap_order(value)
          return "{" + ",".join(f"{k}:{_serialize_value(v)}" for k, v in ordered) + "}"
      if isinstance(value, list):
          return "[" + ",".join(_serialize_value(e) for e in value) + "]"
      return str(value)
    
    
    def get_sorted_string(request_body_str: str) -> str:
      """对应 Java: getSortedString —— 按 key 排序拼接 k=v&,去掉 sign 字段和所有双引号"""
      if not request_body_str or not request_body_str.strip():
          return ""
    
      parsed = _parse_ordered(request_body_str)
      if not isinstance(parsed, OrderedObj):
          raise ValueError("expected top-level JSON object")
    
      filtered = [(k, v) for k, v in parsed if k != "sign"]
      filtered.sort(key=lambda kv: kv[0])  # 等价于 Java TreeMap<String,Object> 的自然升序
    
      sb = "".join(f"{k}={_serialize_value(v)}&" for k, v in filtered)
      result = sb.replace('"', "")
      sorted_str = result[:-1] if result else result  # 去掉末尾的 &
      print("排序后的字符串:" + sorted_str)
      return sorted_str
    
    
    # ---------- 对应 main() 里用到的两个小工具方法 ----------
    
    def get_timestamp() -> str:
      return datetime.now().strftime("%Y%m%d%H%M%S")
    
    
    def random_number(length: int = 32) -> str:
      characters = string.ascii_uppercase + string.digits
      return "".join(random.choice(characters) for _ in range(length))
    
    
    def main():
      # 接口文档地址
      post_url = "https://api-test.tevaupay.com/openapi/kyc/submitKycData"
      # 企业KYB时提交的appId
      app_id = "appId"
      # 平台提供的密钥
      private_key = (
          "xxxxx"
      )
      # 平台提供的apiKey
      x_nexus_api_key = "xxxx"
      # 接口定义入参
      body = (
          "{\n"
          '  "userCode": "CA3720688",\n'
          '  "countryArea": "MY",\n'
          '  "firstNameEn": "wilee",\n'
          '  "lastNameEn": "Matthews",\n'
          '  "birthday": "1980-01-01",\n'
          '  "identityCardType": "1",\n'
          '  "identityFrontPicUrl": "https:/xxx.jpg",\n'
          '  "identityBackPicUrl": "https://xxx.jpg",\n'
          '  "identityCard": "888888888",\n'
          '  "identityCardValidityTime": "2099-12-31"\n'
          "}"
      )
      # 接口定义版本号
      versions = "v1"
      # 时间戳
      timestamp = get_timestamp()
      # 随机数
      nonce = random_number()
      data = f"appId={app_id}&nonce={nonce}&timestamp={timestamp}" + get_sorted_string(body)
      # 签名
      sign = rsa_sign(data, private_key)
    
      print("sign加密前的数据 ==", data)
      print("sign加密后 ==", sign)
    
      headers = {
          "Content-type": "application/json; charset=utf-8",
          "Accept": "application/json",
          "appId": app_id,
          "timestamp": timestamp,
          "nonce": nonce,
          "x-nexus-api-key": x_nexus_api_key,
          "sign": sign,
          "versions": versions,
      }
    
      req = urllib.request.Request(post_url, data=body.encode("utf-8"), headers=headers, method="POST")
      try:
          with urllib.request.urlopen(req, timeout=10) as resp:
              resp_body = resp.read().decode("utf-8", errors="replace")
              print(f"Tevau: {post_url} request headers: {headers} \r\n response: {resp_body}")
      except urllib.error.URLError as e:
          print(f"Tevau: {post_url} 异常: {e}")
    
    
    if __name__ == "__main__":
      main()
    
    代码示例(PHP版)
    PHP包版本
    https://oss.tevaupay.com/tevau-nexus-test/Public_catalogue/TevauTestHttpPhpUtils.php
    5.
    验证传入的 Webhook
    Request Body
    
    {
      "orderId": "12345",
      "eventType": "UsdtDeposit",
      "tradeStatus": "Success"
    }
    要确保传入 Webhook 请求的真实性,您应该验证 "x-timestamp" 和 "x-signature" 标头中包含的数字签名。以下是如何在 JAVA 中执行此作的示例:
    WebHook验签方法(JAVA版)
        private static final String RSA = "RSA";
      private static final String SIGN_ALGORITHMS = "SHA1WithRSA";
    
    
      public static void main(String[] args) throws Exception {
          //Post所有请求参数
          String body = "{\"orderId\":\"12345\",\"eventType\":\"UsdtDeposit\",\"tradeStatus\":\"Success\"}";
          //Headers中x-timestamp
          String timestamp = "20250903140909";
          //Headers中x-signature
          String signature = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzURhq9eaOYsGCnYK0JPgXduRKCOJgbJmiASW+1L+xx3pvsxLH/OScxMwXsEacygmYFHV7MijNEOGSuzbd2u0O7/SzHgqCnn1ID6ehZQktnnF9gZQb1muBmvM4In9+2lSXmuF/t547rpcBuXZaEf5KXR+gQK82u9i8KRl8IszoGoPa4mxf8Krr+C1uXgFhgHyyLxi0GuCOoAJ/lvD5ArIhyOwYS4sGJJXDHt6ZLfNcnyaNHQmx9b7YnytIdswqd6mQ1tN7eHjzcvcE8baArCdZ9E0yVyTQ9tHZ12fhg+qq8HLRd/qJDwTxYYILZ3fZDNm+OPo88iXRjr2qI44CF6y4wIDAQAB";
          //WebHook验签公钥
          String publicKey = "";
          String data = "timestamp=" + timestamp + getSortedString(body);
          boolean verifyResult = verify(data, signature, publicKey);
          System.out.printf("打印验签结果:" + verifyResult);
      }
    
      /**
       * 公钥验签
       *
       * @param data
       * @param sign
       * @param publicKey
       * @return
       * @throws Exception
       */
      public static boolean verify(String data, String sign, String publicKey) throws Exception {
          byte[] keyBytes = java.util.Base64.getDecoder().decode(publicKey);
          X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
          KeyFactory keyFactory = KeyFactory.getInstance(RSA);
          PublicKey publicK = keyFactory.generatePublic(x509EncodedKeySpec);
          Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
          signature.initVerify(publicK);
          signature.update(data.getBytes());
          return signature.verify(Base64.getDecoder().decode(sign));
      }
          /**
       * getSortedString 对参数按照Key进行ASCII排序
       *
       * @param requestBodyStr 请求参数
       * @return 排序拼装后的字符串
       */
      public static String getSortedString(String requestBodyStr) {
          if (jodd.util.StringUtil.isEmpty(requestBodyStr)) {
              return "";
          }
          com.aliyun.openservices.shade.com.alibaba.fastjson.JSONObject jsonObject = com.aliyun.openservices.shade.com.alibaba.fastjson.JSON.parseObject(requestBodyStr);
          SortedMap<String, Object> sortMap = new TreeMap<>();
          StringBuffer sbf = new StringBuffer();
          for (Map.Entry<String, Object> objectEntry : jsonObject.entrySet()) {
              String key = objectEntry.getKey();
              Object value = objectEntry.getValue();
              if ("sign".equals(key)) {
                  continue;
              }
              sortMap.put(key, value);
          }
          Set mapEntrySet = sortMap.entrySet();
          Iterator it = mapEntrySet.iterator();
          while (it.hasNext()) {
              Map.Entry entry = (Map.Entry) it.next();
              String k = (String) entry.getKey();
              Object v = entry.getValue();
              sbf.append(k + "=" + v + "&");
          }
          String sbfString = sbf.toString().replaceAll("\"", "");
          return sbfString.substring(0, sbfString.length() - 1);
      }
    
    WebHook验签方法(GO版)
    WebHook验签方法(Python版)
    You can put other components inside Accordions.
    Modified at 2026-07-30 02:24:50
    Previous
    Get started
    Next
    交易说明
    Built with