openapi: 3.0.3
info:
  title: Hjtpay API
  version: 1.0.0
  description: |
    ## Introduction
    Hjtpay 虚拟信用卡平台 API。
    提供虚拟卡管理、交易查询、Webhook 事件通知等功能。

    ## Time Format Description

    **重要提示**：本 API 所有时间相关字段均采用 **UTC 时间**（协调世界时）。

    ### UTC 时间与北京时间转换
    - UTC 时间比北京时间（中国标准时间 CST）慢 8 小时
    - 如需转换为北京时间，请在 UTC 时间基础上 **加 8 小时**
    - 示例：
      - UTC 时间：2025-09-30T12:00:00Z
      - 北京时间：2025-09-30T20:00:00+08:00

    ### 时间字段格式
    - 所有时间字段均使用 ISO 8601 格式
    - 格式示例：2025-09-30T12:00:00Z
    - 时区标识：Z 表示 UTC 时区

    ## Environment Domains

    Hjtpay 提供两个完全独立的环境，每个环境都有对应的登录域名和 API 服务域名：

    ### 开发环境 (Sandbox)
    - **登录域名**：[https://dev.hjtpay.com/](https://dev.hjtpay.com/)
    - **API 服务域名**：[https://sandbox.hjtpay.com](https://sandbox.hjtpay.com)
    - **用途**：用于开发和测试，所有交易均为模拟数据

    ### 生产环境 (Production)
    - **登录域名**：[https://www.hjtpay.com/](https://www.hjtpay.com/)
    - **API 服务域名**：[https://api.hjtpay.com](https://api.hjtpay.com)
    - **用途**：用于真实交易，所有操作都会产生实际费用

    ### 重要说明
    - **域名与 API 服务域名是不同的概念**：登录域名用于访问商户后台，API 服务域名用于后端接口调用
    - **密钥获取**：需要在对应的登录域名环境中获取 API Key 和 API Secret
    - **环境隔离**：开发和生产环境的密钥是独立的，不能混用

    ## Authentication

    Hjtpay API 使用 HMAC-SHA256 签名认证机制来确保请求的安全性。所有 API 请求都必须包含有效的 HMAC 签名，否则将被拒绝。

    ### 认证流程

    1. **获取 API 密钥**：在对应环境的 Hjtpay 商户后台获取您的 `API Key` 和 `API Secret`
       - 开发环境：登录 [https://dev.hjtpay.com/](https://dev.hjtpay.com/)，进入"开发者" -> "API密钥"页面
       - 生产环境：登录 [https://www.hjtpay.com/](https://www.hjtpay.com/)，进入"开发者" -> "API密钥"页面
    2. **构建签名**：使用 HMAC-SHA256 算法和您的 API Secret 对请求内容进行签名
    3. **添加认证头**：将签名结果添加到 HTTP 请求头中
    4. **发送请求**：发送包含认证信息的 API 请求

    ### 签名算法

    HMAC 签名基于以下元素生成：
    - **HTTP Method**：请求方法（GET、POST、PUT、DELETE 等，大写）
    - **Request Path**：请求路径（不包含域名）
    - **Query String**：查询参数，按字母顺序排序后拼接成 `k=v&k2=v2` 格式（可选，GET请求时使用）
    - **Nonce**：随机数（防重放攻击）
    - **Timestamp**：Unix 时间戳（秒）
    - **Request Body**：JSON 对象的 key 按字母顺序排序，再使用 `JSON.stringify` 序列化（可选，POST/PUT请求时使用）

    ### 参数格式化规则

    #### QUERY_STRING 格式化
    对查询参数按字母顺序排序，然后拼接成 `k=v&k2=v2` 的格式：
    
    **示例**：
    ```
    原始查询参数: page=2&limit=10&sort=name
    格式化后: limit=10&page=2&sort=name
    ```

    #### REQUEST_BODY 格式化  
    对 JSON 对象的 key 按字母顺序排序，再使用 `JSON.stringify` 进行序列化：
    
    **示例**：
    ```json
    原始请求体: {"amount": 100, "bin_id": "bin_abc123", "currency": "USD"}
    格式化后: {"amount":100,"bin_id":"bin_abc123","currency":"USD"}
    ```
    
    **注意**：序列化时不包含空格，使用紧凑格式。

    #### 签名格式
    ```
    StringToSign = HTTP_METHOD + "\n" +
                   REQUEST_PATH + "\n" +
                   QUERY_STRING + "\n" +
                   NONCE + "\n" +
                   TIMESTAMP + "\n" +
                   REQUEST_BODY

    Signature = HMAC-SHA256(API_SECRET, StringToSign)
    ```

    ### 认证请求头

    所有 API 请求必须包含以下 HTTP 头：

    | 头部名称 | 说明 | 示例 |
    |---------|------|------|
    | `X-API-Key` | API 密钥 | `sk_test_1234567890abcdef` |
    | `X-Timestamp` | Unix 时间戳（秒） | `1646641200` |
    | `X-Nonce` | 随机数字符串 | `random123` |
    | `X-Signature` | HMAC 签名 | `5257a869e7ecb3e9e4c0a8b7d1234567...` |
    | `Content-Type` | 内容类型 | `application/json` |

    ### 示例代码

    #### Python 示例
    ```python
    import hmac
    import hashlib
    import time
    import requests
    import json
    from urllib.parse import urlencode

    class HjtpayAuth:
        def __init__(self, api_key, api_secret):
            self.api_key = api_key
            self.api_secret = api_secret

        def sort_query_params(self, query_string):
            """对查询参数按字母顺序排序"""
            if not query_string:
                return ""
            params = {}
            for param in query_string.split('&'):
                if '=' in param:
                    key, value = param.split('=', 1)
                    params[key] = value
            return urlencode(sorted(params.items()))

        def sort_json_body(self, body):
            """对JSON请求体按字母顺序排序"""
            if not body:
                return ""
            if isinstance(body, str):
                data = json.loads(body)
            else:
                data = body
            return json.dumps(data, sort_keys=True, separators=(',', ':'))

        def generate_signature(self, method, path, query_string='', body='', timestamp=None, nonce='random123'):
            """生成 HMAC 签名"""
            # 签名生成所需的关键要素
            signing_elements = {
                "http_method": method.upper(),           # HTTP方法（大写）
                "request_path": path,                    # API路径
                "query_string": self.sort_query_params(query_string),  # 查询参数按字母顺序排序，拼接成k=v&k2=v2格式
                "nonce": nonce,                          # 随机数（防重放）
                "timestamp": str(timestamp or int(time.time())),        # 当前时间戳（秒）
                "request_body": self.sort_json_body(body)              # JSON对象key按字母顺序排序，JSON.stringify序列化
            }
            
            string_to_sign = f"{signing_elements['http_method']}\n{signing_elements['request_path']}\n{signing_elements['query_string']}\n{signing_elements['nonce']}\n{signing_elements['timestamp']}\n{signing_elements['request_body']}"
            
            signature = hmac.new(
                self.api_secret.encode('utf-8'),
                string_to_sign.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()
            return signature, signing_elements

        def make_request(self, method, url, path, data=None, query_params=None):
            """发送带认证的 API 请求"""
            timestamp = int(time.time())
            nonce = "random123"
            query_string = urlencode(query_params) if query_params else ""
            body = json.dumps(data, sort_keys=True, separators=(',', ':')) if data else ""

            signature, _ = self.generate_signature(method, path, query_string, body, timestamp, nonce)

            # 设置请求头
            headers = {
                "Content-Type": "application/json",
                "X-API-Key": self.api_key,
                "X-Timestamp": str(timestamp),
                "X-Nonce": nonce,
                "X-Signature": signature
            }

            full_url = f"{url}?{query_string}" if query_string else url
            response = requests.request(
                method=method,
                url=full_url,
                headers=headers,
                json=data if data else None
            )
            return response

    # 使用示例
    auth = HjtpayAuth('sk_test_1234567890abcdef', 'your_api_secret_here')
    response = auth.make_request(
        'POST',
        'https://api.hjtpay.com/v1/cards',
        '/v1/cards',
        {'amount': 100, 'bin_id': 'bin_abc123', 'holder_id': 'ch_001', 'currency': 'USD', 'label': 'Travel Card'}
    )
    print(response.json())
    ```

    #### Java 示例
    ```java
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    import java.time.Instant;
    import java.util.*;
    import java.util.stream.Collectors;
    import org.apache.http.client.methods.*;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import com.fasterxml.jackson.databind.ObjectMapper;

    public class HjtpayAuth {
        private final String apiKey;
        private final String apiSecret;
        private final ObjectMapper objectMapper = new ObjectMapper();

        public HjtpayAuth(String apiKey, String apiSecret) {
            this.apiKey = apiKey;
            this.apiSecret = apiSecret;
        }

        private String sortQueryParams(String queryString) {
            if (queryString == null || queryString.isEmpty()) {
                return "";
            }
            Map<String, String> params = new HashMap<>();
            for (String param : queryString.split("&")) {
                String[] keyValue = param.split("=", 2);
                if (keyValue.length == 2) {
                    params.put(keyValue[0], keyValue[1]);
                }
            }
            return params.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(entry -> entry.getKey() + "=" + entry.getValue())
                    .collect(Collectors.joining("&"));
        }

        private String sortJsonBody(Object data) throws Exception {
            if (data == null) {
                return "";
            }
            // 使用TreeMap确保JSON键按字母顺序排序
            objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
            return objectMapper.writeValueAsString(data);
        }

        public String generateSignature(String method, String path, String queryString, Object data, long timestamp, String nonce) throws Exception {
            // 签名生成所需的关键要素
            Map<String, String> signingElements = new HashMap<>();
            signingElements.put("http_method", method.toUpperCase());          // HTTP方法（大写）
            signingElements.put("request_path", path);                         // API路径
            signingElements.put("query_string", sortQueryParams(queryString)); // 查询参数按字母顺序排序，拼接成k=v&k2=v2格式
            signingElements.put("nonce", nonce);                              // 随机数（防重放）
            signingElements.put("timestamp", String.valueOf(timestamp));       // 当前时间戳（秒）
            signingElements.put("request_body", sortJsonBody(data));           // JSON对象key按字母顺序排序，JSON.stringify序列化

            String stringToSign = signingElements.get("http_method") + "\n" +
                                  signingElements.get("request_path") + "\n" +
                                  signingElements.get("query_string") + "\n" +
                                  signingElements.get("nonce") + "\n" +
                                  signingElements.get("timestamp") + "\n" +
                                  signingElements.get("request_body");

            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);

            byte[] hash = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString();
        }

        public String makeRequest(String method, String url, String path, Object data, String queryString) throws Exception {
            long timestamp = Instant.now().getEpochSecond();
            String nonce = "random123";

            String signature = generateSignature(method, path, queryString, data, timestamp, nonce);

            CloseableHttpClient client = HttpClients.createDefault();
            HttpUriRequest request;

            String fullUrl = queryString != null && !queryString.isEmpty() ? url + "?" + queryString : url;

            switch (method.toUpperCase()) {
                case "GET":
                    request = new HttpGet(fullUrl);
                    break;
                case "POST":
                    HttpPost post = new HttpPost(fullUrl);
                    if (data != null) {
                        post.setEntity(new StringEntity(sortJsonBody(data)));
                    }
                    request = post;
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported method: " + method);
            }

            // 设置请求头
            request.setHeader("Content-Type", "application/json");
            request.setHeader("X-API-Key", apiKey);
            request.setHeader("X-Timestamp", String.valueOf(timestamp));
            request.setHeader("X-Nonce", nonce);
            request.setHeader("X-Signature", signature);

            return client.execute(request, response ->
                new String(response.getEntity().getContent().readAllBytes()));
        }

        // 使用示例
        public static void main(String[] args) throws Exception {
            HjtpayAuth auth = new HjtpayAuth("sk_test_1234567890abcdef", "your_api_secret_here");

            Map<String, Object> data = new TreeMap<>(); // 使用TreeMap确保键排序
            data.put("amount", 100);
            data.put("bin_id", "bin_abc123");
            data.put("holder_id", "ch_001");
            data.put("currency", "USD");
            data.put("label", "Travel Card");

            String response = auth.makeRequest(
                "POST",
                "https://api.hjtpay.com/v1/cards",
                "/v1/cards",
                data,
                null
            );
            System.out.println(response);
        }
    }
    ```

    #### Node.js 示例
    ```javascript
    const crypto = require('crypto');
    const axios = require('axios');

    class HjtpayAuth {
        constructor(apiKey, apiSecret) {
            this.apiKey = apiKey;
            this.apiSecret = apiSecret;
        }

        sortQueryParams(queryString) {
            if (!queryString) return '';
            const params = new URLSearchParams(queryString);
            const sorted = Array.from(params.entries()).sort();
            return new URLSearchParams(sorted).toString();
        }

        sortJsonBody(data) {
            if (!data) return '';
            if (typeof data === 'string') {
                data = JSON.parse(data);
            }
            // 递归排序对象键
            const sortObject = (obj) => {
                if (Array.isArray(obj)) {
                    return obj.map(sortObject);
                } else if (obj !== null && typeof obj === 'object') {
                    return Object.keys(obj)
                        .sort()
                        .reduce((sorted, key) => {
                            sorted[key] = sortObject(obj[key]);
                            return sorted;
                        }, {});
                }
                return obj;
            };
            return JSON.stringify(sortObject(data));
        }

        generateSignature(method, path, queryString = '', body = '', timestamp = null, nonce = 'random123') {
            // 签名生成所需的关键要素
            const signingElements = {
                http_method: method.toUpperCase(),                    // HTTP方法（大写）
                request_path: path,                                   // API路径
                query_string: this.sortQueryParams(queryString),      // 查询参数按字母顺序排序，拼接成k=v&k2=v2格式
                nonce: nonce,                                         // 随机数（防重放）
                timestamp: String(timestamp || Math.floor(Date.now() / 1000)), // 当前时间戳（秒）
                request_body: this.sortJsonBody(body)                // JSON对象key按字母顺序排序，JSON.stringify序列化
            };

            const stringToSign = `${signingElements.http_method}\n${signingElements.request_path}\n${signingElements.query_string}\n${signingElements.nonce}\n${signingElements.timestamp}\n${signingElements.request_body}`;
            
            return {
                signature: crypto
                    .createHmac('sha256', this.apiSecret)
                    .update(stringToSign, 'utf8')
                    .digest('hex'),
                signingElements
            };
        }

        async makeRequest(method, url, path, data = null, queryParams = null) {
            const timestamp = Math.floor(Date.now() / 1000);
            const nonce = 'random123';
            const queryString = queryParams ? new URLSearchParams(queryParams).toString() : '';
            const body = data ? JSON.stringify(data, Object.keys(data).sort()) : '';

            const { signature } = this.generateSignature(method, path, queryString, body, timestamp, nonce);

            // 设置请求头
            const headers = {
                'Content-Type': 'application/json',
                'X-API-Key': this.apiKey,
                'X-Timestamp': String(timestamp),
                'X-Nonce': nonce,
                'X-Signature': signature
            };

            const fullUrl = queryString ? `${url}?${queryString}` : url;

            const config = {
                method: method.toLowerCase(),
                url: fullUrl,
                headers: headers,
                data: data || undefined
            };

            try {
                const response = await axios(config);
                return response.data;
            } catch (error) {
                throw new Error(`API request failed: ${error.message}`);
            }
        }
    }

    // 使用示例
    async function example() {
        const auth = new HjtpayAuth('sk_test_1234567890abcdef', 'your_api_secret_here');

        try {
            const response = await auth.makeRequest(
                'POST',
                'https://api.hjtpay.com/v1/cards',
                '/v1/cards',
                {
                    amount: 100,
                    bin_id: 'bin_abc123',
                    holder_id: 'ch_001',
                    currency: 'USD',
                    label: 'Travel Card'
                }
            );
            console.log(response);
        } catch (error) {
            console.error(error.message);
        }
    }

    example();
    ```

    ### 错误处理

    当认证失败时，API 将返回以下错误代码：

    | HTTP 状态码 | 错误代码 | 说明 |
    |------------|---------|------|
    | 401 | 10001 | 缺少认证信息（X-API-Key, X-Timestamp, X-Nonce, X-Signature） |
    | 401 | 10002 | API Key 无效 |
    | 401 | 10003 | 签名验证失败 |
    | 401 | 10004 | 请求已过期（时间戳超过 5 分钟） |
    | 401 | 10005 | Nonce 重复使用 |
    | 403 | 10006 | API Key 权限不足 |

    ### 安全建议

    1. **保护 API Secret**：切勿在客户端代码或公开仓库中暴露 API Secret
    2. **使用 HTTPS**：所有 API 请求必须使用 HTTPS 协议
    3. **验证时间戳**：请求时间戳与服务器时间差不能超过 5 分钟
    4. **Nonce 唯一性**：每个请求使用唯一的 Nonce，防止重放攻击
    5. **定期轮换密钥**：建议定期更换 API Key 和 Secret

    ### Webhook 认证

    对于 Webhook 事件通知，我们使用专门的 Webhook Secret 进行 HMAC 签名认证：

    #### 配置步骤

    1. **配置回调URL**：在对应环境的 Hjtpay 商户后台配置您的交易事件回调 URL
       - 开发环境：登录 [https://dev.hjtpay.com/](https://dev.hjtpay.com/)，进入"开发者" -> "Webhooks"页面
       - 生产环境：登录 [https://www.hjtpay.com/](https://www.hjtpay.com/)，进入"开发者" -> "Webhooks"页面
    2. **获取 Webhook Secret**：在对应环境的 Hjtpay 商户后台获取您的专用 `Webhook Secret`（与 API Secret 不同）
    3. **验证签名**：获取请求头中的 `X-SIGNATURE` 和 `X-TIMESTAMP`
    4. **签名计算**：使用 Webhook Secret 对时间戳和请求体进行 HMAC-SHA256 签名
    5. **签名比较**：验证计算出的签名是否与请求头中的签名匹配

    **重要说明**：
    - Webhook 认证使用的是专门的 Webhook Secret，而不是 API Secret。这两个密钥是独立的，都可以在对应环境的 Hjtpay 商户后台获取。
    - 交易事件 Webhook 回调 URL 必须在对应环境的 Hjtpay 商户后台进行配置后才能接收到事件通知。
    - 开发环境和生产环境的 Webhook Secret 也是独立的，不能混用。

    #### Webhook 验证示例

    ##### Python 版本
    ```python
    import hmac
    import hashlib
    import time

    class WebhookVerifier:
        @staticmethod
        def verify_webhook_signature(payload, signature, timestamp, webhook_secret):
            """验证 Webhook 签名"""
            # 检查时间戳是否过期（5分钟内）
            current_time = int(time.time())
            if abs(current_time - int(timestamp)) > 300:
                return False

            # 构建待签名字符串
            string_to_sign = f"{timestamp}.{payload}"

            # 计算签名
            expected_signature = hmac.new(
                webhook_secret.encode('utf-8'),
                string_to_sign.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()

            # 比较签名
            return hmac.compare_digest(expected_signature, signature)

    # 使用示例
    def handle_webhook(request_headers, request_body, webhook_secret):
        signature = request_headers.get('X-Signature')
        timestamp = request_headers.get('X-Timestamp')

        verifier = WebhookVerifier()
        if verifier.verify_webhook_signature(request_body, signature, timestamp, webhook_secret):
            # 签名验证通过，处理 Webhook 数据
            print("Webhook 验证成功")
            return True
        else:
            # 签名验证失败
            print("Webhook 验证失败")
            return False
    ```

    ##### Java 版本
    ```java
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;
    import java.time.Instant;
    import java.util.Arrays;

    public class WebhookVerifier {
        
        /**
         * 验证 Webhook 签名
         * @param payload 请求体原始字符串
         * @param signature 签名值
         * @param timestamp 时间戳
         * @param webhookSecret Webhook密钥
         * @return 验证结果
         */
        public static boolean verifyWebhookSignature(String payload, String signature, String timestamp, String webhookSecret) {
            try {
                // 检查时间戳是否过期（5分钟内）
                long currentTime = Instant.now().getEpochSecond();
                if (Math.abs(currentTime - Long.parseLong(timestamp)) > 300) {
                    return false;
                }

                // 构建待签名字符串
                String stringToSign = timestamp + "." + payload;

                // 计算签名
                Mac mac = Mac.getInstance("HmacSHA256");
                SecretKeySpec secretKey = new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                mac.init(secretKey);
                
                byte[] hash = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
                StringBuilder hexString = new StringBuilder();
                for (byte b : hash) {
                    String hex = Integer.toHexString(0xff & b);
                    if (hex.length() == 1) hexString.append('0');
                    hexString.append(hex);
                }
                String expectedSignature = hexString.toString();

                // 安全比较签名
                return timingSafeEqual(signature.getBytes(StandardCharsets.UTF_8), 
                                     expectedSignature.getBytes(StandardCharsets.UTF_8));
                
            } catch (Exception e) {
                return false;
            }
        }

        /**
         * 时间安全的字符串比较，防止时序攻击
         */
        private static boolean timingSafeEqual(byte[] a, byte[] b) {
            if (a.length != b.length) {
                return false;
            }
            
            int result = 0;
            for (int i = 0; i < a.length; i++) {
                result |= a[i] ^ b[i];
            }
            return result == 0;
        }

        // 使用示例
        public static boolean handleWebhook(Map<String, String> requestHeaders, String requestBody, String webhookSecret) {
            String signature = requestHeaders.get("X-Signature");
            String timestamp = requestHeaders.get("X-Timestamp");

            if (verifyWebhookSignature(requestBody, signature, timestamp, webhookSecret)) {
                System.out.println("Webhook 验证成功");
                return true;
            } else {
                System.out.println("Webhook 验证失败");
                return false;
            }
        }
    }
    ```

    ##### Node.js 版本
    ```javascript
    const crypto = require('crypto');

    class WebhookVerifier {
        /**
         * 验证 Webhook 签名
         * @param {string} payload 请求体原始字符串
         * @param {string} signature 签名值
         * @param {string} timestamp 时间戳
         * @param {string} webhookSecret Webhook密钥
         * @returns {boolean} 验证结果
         */
        static verifyWebhookSignature(payload, signature, timestamp, webhookSecret) {
            try {
                // 检查时间戳是否过期（5分钟内）
                const currentTime = Math.floor(Date.now() / 1000);
                if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
                    return false;
                }

                // 构建待签名字符串
                const stringToSign = `${timestamp}.${payload}`;

                // 计算签名
                const expectedSignature = crypto
                    .createHmac('sha256', webhookSecret)
                    .update(stringToSign, 'utf8')
                    .digest('hex');

                // 安全比较签名
                return crypto.timingSafeEqual(
                    Buffer.from(signature, 'hex'),
                    Buffer.from(expectedSignature, 'hex')
                );
            } catch (error) {
                return false;
            }
        }
    }

    // 使用示例
    function handleWebhook(requestHeaders, requestBody, webhookSecret) {
        const signature = requestHeaders['x-signature'];
        const timestamp = requestHeaders['x-timestamp'];

        if (WebhookVerifier.verifyWebhookSignature(requestBody, signature, timestamp, webhookSecret)) {
            console.log('Webhook 验证成功');
            return true;
        } else {
            console.log('Webhook 验证失败');
            return false;
        }
    }

    module.exports = { WebhookVerifier, handleWebhook };
    ```

servers:
  - url: https://api.hjtpay.com
    description: |
      生产环境 API 服务
      - 对应登录域名：[https://www.hjtpay.com/](https://www.hjtpay.com/)
      - 用于真实交易环境
      - 所有操作都会产生实际费用
  - url: https://sandbox.hjtpay.com
    description: |
      开发环境 API 服务
      - 对应登录域名：[https://dev.hjtpay.com/](https://dev.hjtpay.com/)
      - 用于开发和测试
      - 所有交易均为模拟数据

tags:
  - name: Files
    description: 文件上传管理（证件照片上传）
  - name: Cardholders
    description: 持卡人信息管理
  - name: Cards
    description: 虚拟卡管理（获取 BIN / 开卡 / 转入 / 注销）
  - name: Webhooks
    description: Webhook 回调通知（消费 / 撤销 / 退款）

components:
  securitySchemes:
    HmacAuth:
      type: apiKey
      in: header
      name: X-Signature
      description: |
        HMAC-SHA256 签名认证

        签名算法：
        ```
        StringToSign = HTTP_METHOD + "\n" +
                       REQUEST_PATH + "\n" +
                       QUERY_STRING + "\n" +
                       NONCE + "\n" +
                       TIMESTAMP + "\n" +
                       REQUEST_BODY

        Signature = HMAC-SHA256(API_SECRET, StringToSign)
        ```

        必需头部：
        - X-API-Key: 您的 API 密钥
        - X-Timestamp: Unix 时间戳（秒）
        - X-Nonce: 随机数（防重放）
        - X-Signature: HMAC 签名
        - Content-Type: application/json
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
    WebhookSignatureAuth:
      type: apiKey
      in: header
      name: X-SIGNATURE
      description: |
        Webhook HMAC-SHA256 签名认证
        
        使用专门的 Webhook Secret（从 Hjtpay 商户后台获取）对时间戳和请求体进行签名：
        ```
        StringToSign = TIMESTAMP + "." + REQUEST_BODY
        Signature = HMAC-SHA256(WEBHOOK_SECRET, StringToSign)
        ```
        
        必需头部：
        - X-Signature: HMAC 签名
        - X-Timestamp: Unix 时间戳（秒）
    WebhookTimestamp:
      type: apiKey
      in: header
      name: X-TIMESTAMP
      description: Unix 时间戳（秒），用于防重放攻击和签名验证

  schemas:
    StandardResponse:
      type: object
      properties:
        code:
          type: integer
          description: |
            响应状态码：
            - 0: 成功
            - 错误码格式：5位数字（前两位代表错误大类，后三位代表具体错误）
            
            错误大类：
            - 10xxx: 认证相关错误
            - 20xxx: 参数验证错误
            - 30xxx: 业务逻辑错误
            - 40xxx: 资源不存在错误
            - 50xxx: 系统内部错误
          example: 0
        message:
          type: string
          description: 响应消息
          example: "success"
        data:
          type: object
          description: 响应数据（仅成功时返回）
          nullable: true
      example:
        code: 0
        message: "success"
        data: {}

    BaseResponse:
      type: object
      properties:
        code:
          type: integer
          example: 0
        message:
          type: string
          example: success

    Bin:
      type: object
      properties:
        bin:
          type: string
          description: BIN码（银行标识码，通常为前6位）
          example: "450123"
        bin_id:
          type: string
          description: BIN标识符
          example: "bin_abc123"
        type:
          type: string
          description: 卡片类型（预付卡、借记卡等）
          example: "prepaid"
        currency:
          type: string
          description: 支持的货币类型
          example: "USD"
        network:
          type: string
          description: 卡组织（如Visa、MasterCard等）
          example: "mastercard"
        issue_fee:
          type: number
          format: float
          description: 开卡手续费
          example: 2.50
        min_charge_amount:
          type: number
          format: float
          description: 最小充值金额
          example: 10.00
        allow_withdraw:
          type: boolean
          description: 是否允许提现
          example: true
        allow_freeze:
          type: boolean
          description: 是否允许冻结
          example: false

    CardholderResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Cardholder"

    CardholderListResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: "#/components/schemas/Cardholder"

    CardResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Card"

    CardTopupResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: object
              properties:
                transaction_number:
                  type: string
                  description: 交易流水号（用于跟踪和记录）
                  example: "txn_xxx"
                requested_amount:
                  type: number
                  format: float
                  description: 请求充值金额
                  example: 50.00
                topup_amount:
                  type: number
                  format: float
                  description: 实际充值到卡的金额
                  example: 49.00
                fee_amount:
                  type: number
                  format: float
                  description: 充值手续费
                  example: 1.00

    CardCancellationResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: object
              properties:
                transaction_number:
                  type: string
                  description: 交易流水号（用于跟踪和记录注销操作）
                  example: "txn_20250930ghi789"
                refund_amount:
                  type: number
                  format: float
                  description: 退回金额（卡片注销时退回的余额）
                  example: 25.50

    CardListResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: "#/components/schemas/Card"

    BinListResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: "#/components/schemas/Bin"

    FileUploadResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            data:
              type: object
              properties:
                file_key:
                  type: string
                  description: 文件唯一标识key，用于在创建持卡人时引用该文件
                  example: fD9fyhb0juQg3k

    Card:
      type: object
      properties:
        card_id:
          type: string
          example: card_1234567890
          description: 卡片唯一标识
        card_number:
          type: string
          example: "4111111111111111"
          description: 卡号（可能需要屏蔽部分数字，仅显示后四位，安全考虑）
        cvv:
          type: string
          example: "123"
          description: 卡片安全码
        expiry_month:
          type: string
          example: "12"
          description: 有效期（月）
        expiry_year:
          type: string
          example: "2028"
          description: 有效期（年）
        last_name:
          type: string
          example: Zhang
          description: 持卡人姓
        first_name:
          type: string
          example: Wei
          description: 持卡人名
        balance:
          type: number
          example: 100.00
          description: 当前余额
        currency:
          type: string
          example: USD
          description: 余额币种
        status:
          type: string
          example: active
          description: 卡片状态：active（已激活）/frozen（已冻结）/closed（已注销）
        network:
          type: string
          example: MasterCard
          description: 卡组织（Visa / MasterCard / UnionPay 等）
        type:
          type: string
          example: prepaid
          description: 卡片类型
        holder_id:
          type: string
          example: holder_987654321
          description: 持卡人唯一标识
        allow_withdraw:
          type: boolean
          example: true
          description: 是否允许提现
        allow_freeze:
          type: boolean
          example: true
          description: 是否允许冻结
        min_charge_amount:
          type: number
          example: 10.00
          description: 最小充值金额
      example:
        card_id: "card_1234567890"
        card_number: "4111111111111111"
        cvv: "123"
        expiry_month: "12"
        expiry_year: "2028"
        last_name: "Zhang"
        first_name: "Wei"
        balance: 100.00
        currency: "USD"
        status: "active"
        network: "MasterCard"
        type: "prepaid"
        holder_id: "holder_987654321"
        allow_withdraw: true
        allow_freeze: true
        min_charge_amount: 10.00

    Cardholder:
      type: object
      properties:
        holder_id:
          type: string
          description: 持卡人唯一标识
          example: ch_123456
        first_name:
          type: string
          description: 持卡人名
          example: Wei
        last_name:
          type: string
          description: 持卡人姓
          example: Zhang
        nationality:
          type: string
          description: 国籍（ISO 3166-1 alpha-2 代码）
          example: CN
        date_of_birth:
          type: string
          format: date
          description: 出生日期（YYYY-MM-DD格式）
          example: "1990-05-20"
        email:
          type: string
          description: 电子邮箱地址
          example: zhang.wei@example.com
        phone:
          type: object
          description: 电话号码信息
          properties:
            country_code:
              type: string
              description: 国家区号
              example: "+86"
            number:
              type: string
              description: 电话号码
              example: "13800138000"
        billing_address:
          type: object
          description: 账单地址信息
          properties:
            country:
              type: string
              description: 国家（ISO 3166-1 alpha-2 代码）
              example: CN
            state:
              type: string
              description: 省份/州
              example: Guangdong
            city:
              type: string
              description: 城市
              example: Shenzhen
            address_line:
              type: string
              description: 详细地址
              example: Nanshan District, High-tech Park 1st Road
            postal_code:
              type: string
              description: 邮政编码
              example: "518000"
        id_type:
          type: string
          enum:
            - id_card
            - passport
          description: "证件类型（id_card: 身份证, passport: 护照）"
          example: id_card
        id_card_front_key:
          type: string
          description: 身份证人像面文件key（当id_type为id_card时必填）
          example: fD9fyhb0juQg3k
          nullable: true
        id_card_back_key:
          type: string
          description: 身份证国徽面文件key（当id_type为id_card时必填）
          example: gE8gzic1kvRh4l
          nullable: true
        passport_key:
          type: string
          description: 护照文件key（当id_type为passport时必填）
          example: hF7hAjd2lwSi5m
          nullable: true
        status:
          type: string
          enum:
            - pending
            - approved
            - rejected
          description: "持卡人状态（pending: 待审核, approved: 审核通过, rejected: 审核驳回）"
          example: pending
        created_at:
          type: string
          format: date-time
          description: 创建时间（ISO 8601格式）
          example: 2025-09-26T10:00:00Z

    ErrorResponse:
      allOf:
        - $ref: "#/components/schemas/StandardResponse"
        - type: object
          properties:
            code:
              type: integer
              description: |
                错误码列表：
                
                **认证相关错误 (10xxx)**
                - 10001: 缺少认证信息
                - 10002: API Key 无效
                - 10003: 签名验证失败
                - 10004: 请求已过期
                - 10005: Nonce 重复使用
                - 10006: API Key 权限不足
                
                **参数验证错误 (20xxx)**
                - 20001: 缺少必需参数
                - 20002: 参数格式错误
                - 20003: 参数值超出范围
                - 20004: 日期格式错误
                - 20005: 邮箱格式错误
                - 20006: 手机号格式错误
                
                **业务逻辑错误 (30xxx)**
                - 30001: 持卡人已存在
                - 30002: 卡片余额不足
                - 30003: 卡片状态不允许操作
                - 30004: 超出单笔限额
                - 30005: 超出日限额
                - 30006: BIN 不可用
                
                **资源不存在错误 (40xxx)**
                - 40001: 持卡人不存在
                - 40002: 卡片不存在
                - 40003: BIN 不存在
                - 40004: 交易记录不存在
                
                **系统内部错误 (50xxx)**
                - 50001: 数据库错误
                - 50002: 网络连接超时
                - 50003: 第三方服务错误
                - 50004: 系统维护中
                - 50005: 服务不可用
              example: 20001
            message:
              type: string
              example: "缺少必需参数"
            data:
              type: object
              nullable: true
              example: null

    WebhookEvent:
      type: object
      required:
        - event_id
        - event_type
        - occurred_at
        - data
      properties:
        event_id:
          type: string
          description: 事件唯一标识符
          example: "evt_20250930abcd1234"
        event_type:
          type: string
          enum:
            - "transaction.created"
            - "transaction.reversed"
            - "transaction.refunded"
            - "transaction.verification"
            - "cardholder.approved"
            - "cardholder.rejected"
          description: |
            事件类型：
            - transaction.created: 交易创建（消费）
            - transaction.reversed: 交易撤销
            - transaction.refunded: 交易退款
            - transaction.verification: 交易验证
            - cardholder.approved: 持卡人审核通过
            - cardholder.rejected: 持卡人审核驳回
          example: "transaction.created"
        occurred_at:
          type: string
          format: date-time
          description: 事件被记录或推送的时间（事件系统生成的时间）
          example: "2025-09-30T12:00:00Z"
        data:
          type: object
          description: "事件数据内容，根据event_type不同而有所区别。对于持卡人审核事件，仅包含holder_id、status、rejection_reason字段"
          properties:
            order_id:
              type: string
              description: 订单号，用于标识具体的交易（仅交易事件）
              example: "ord_123456789"
              nullable: true
            card_id:
              type: string
              description: 虚拟卡标识符，对应发生交易的卡片（仅交易事件）
              example: "card_789xyz"
              nullable: true
            holder_id:
              type: string
              description: 持卡人标识符（持卡人审核事件必需）
              example: "ch_123456"
              nullable: true
            status:
              type: string
              description: 持卡人状态（持卡人审核事件必需）
              enum:
                - pending
                - approved
                - rejected
              example: "approved"
              nullable: true
            rejection_reason:
              type: string
              description: 驳回原因（持卡人审核事件，仅在status为rejected时返回）
              nullable: true
              example: "身份证照片不清晰"
            merchant:
              type: object
              description: 商户信息（仅交易事件）
              nullable: true
              required:
                - merchant_id
                - name
                - city
                - region
                - category_code
              properties:
                merchant_id:
                  type: string
                  description: 商户ID
                  example: "01424C9967"
                name:
                  type: string
                  description: 商户名称，发生交易的商家
                  example: "Amazon US"
                city:
                  type: string
                  description: 商户所在城市
                  example: "Seattle"
                region:
                  type: string
                  description: 商户所在地区/州/省
                  example: "US"
                category_code:
                  type: string
                  description: 商户类别代码（MCC）
                  example: "5331"
            transaction:
              type: object
              description: 交易信息（仅交易事件）
              nullable: true
              required:
                - amount
                - currency
                - settlement_amount
                - settlement_currency
                - fee_amount
                - fee_currency
                - transaction_time
              properties:
                amount:
                  type: number
                  format: float
                  description: 交易金额（原始交易金额）
                  example: 100
                currency:
                  type: string
                  description: 交易币种（原始交易的币种）
                  example: "USD"
                settlement_amount:
                  type: number
                  format: float
                  description: 结算金额（扣除手续费后实际金额）
                  example: 99
                settlement_currency:
                  type: string
                  description: 结算币种（实际入账的币种）
                  example: "USD"
                fee_amount:
                  type: number
                  format: float
                  description: 手续费金额
                  example: 1
                fee_currency:
                  type: string
                  description: 手续费币种
                  example: "USD"
                transaction_time:
                  type: string
                  format: date-time
                  description: 交易实际发生时间（用户刷卡或扣款时）
                  example: "2025-09-30T12:00:00Z"
            status_code:
              type: string
              description: 交易状态码（00表示成功，其他代码表示各种错误）（仅交易事件）
              example: "00"
              nullable: true
            failure_reason:
              type: string
              description: 失败原因描述（仅在交易失败或持卡人审核驳回时提供具体失败信息）
              nullable: true
              example: null
      example:
        event_id: "evt_20250930abcd1234"
        event_type: "transaction.created"
        occurred_at: "2025-09-30T12:00:00Z"
        data:
          order_id: "ord_123456789"
          card_id: "card_789xyz"
          merchant:
            merchant_id: "01424C9967"
            name: "Amazon US"
            city: "Seattle"
            region: "US"
            category_code: "5331"
          transaction:
            amount: 100
            currency: "USD"
            settlement_amount: 99
            settlement_currency: "USD"
            fee_amount: 1
            fee_currency: "USD"
            transaction_time: "2025-09-30T12:00:00Z"
          status_code: "00"
          failure_reason: null

paths:
  /v1/files/upload:
    post:
      tags:
        - Files
      summary: 上传文件
      description: |
        上传持卡人证件文件（身份证或护照）。上传成功后返回文件key，该key用于创建或更新持卡人时引用该文件。
        
        **支持的文件格式**：
        - 图片格式：JPEG, PNG, PDF
        - 最大文件大小：10MB
        
        **注意事项**：
        - 文件上传后会进行安全扫描
        - 文件key的有效期为24小时，请在有效期内使用
        - 每个文件只能关联到一个持卡人
      security:
        - HmacAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: 要上传的文件（身份证或护照图片）
      responses:
        "200":
          description: 上传成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileUploadResponse"
              example:
                code: 0
                message: "success"
                data:
                  file_key: "fD9fyhb0juQg3k"
        "400":
          description: 参数错误（文件格式不支持或文件大小超出限制）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cardholders:
    post:
      tags:
        - Cardholders
      summary: 创建持卡人
      security:
        - HmacAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - first_name
                - last_name
                - nationality
                - date_of_birth
                - email
                - phone
                - billing_address
                - id_type
              properties:
                first_name:
                  type: string
                  description: 持卡人名字
                  example: Wei
                last_name:
                  type: string
                  description: 持卡人姓氏
                  example: Zhang
                nationality:
                  type: string
                  description: 国籍代码（ISO 3166-1 alpha-2 格式，如CN代表中国）
                  example: CN
                date_of_birth:
                  type: string
                  format: date
                  description: 出生日期（YYYY-MM-DD格式）
                  example: "1990-05-20"
                email:
                  type: string
                  description: 电子邮箱地址（用于接收重要通知）
                  example: zhang.wei@example.com
                phone:
                  type: object
                  description: 手机号码信息
                  required:
                    - country_code
                    - number
                  properties:
                    country_code:
                      type: string
                      description: 国家或地区电话区号（包含+号）
                      example: "+86"
                    number:
                      type: string
                      description: 手机号码（不包含区号）
                      example: "13800138000"
                billing_address:
                  type: object
                  description: 账单地址信息（用于身份验证和合规要求）
                  required:
                    - country
                    - state
                    - city
                    - address_line
                    - postal_code
                  properties:
                    country:
                      type: string
                      description: 国家代码（ISO 3166-1 alpha-2 格式）
                      example: CN
                    state:
                      type: string
                      description: |
                        省份或州名称
                        - 当 country 为 US 时，必须使用2位大写字母的州代码（如：CA, NY, TX）
                        - 其他国家可以使用完整的省份/州名称
                      example: Guangdong
                    city:
                      type: string
                      description: 城市名称
                      example: Shenzhen
                    address_line:
                      type: string
                      description: 详细地址（街道、门牌号等）
                      example: Nanshan District, High-tech Park 1st Road
                    postal_code:
                      type: string
                      description: 邮政编码
                      example: "518000"
                id_type:
                  type: string
                  enum:
                    - id_card
                    - passport
                  description: "证件类型（id_card: 身份证, passport: 护照）"
                  example: id_card
                id_card_front_key:
                  type: string
                  description: 身份证人像面文件key（当id_type为id_card时必填，通过文件上传接口获取）
                  example: fD9fyhb0juQg3k
                id_card_back_key:
                  type: string
                  description: 身份证国徽面文件key（当id_type为id_card时必填，通过文件上传接口获取）
                  example: gE8gzic1kvRh4l
                passport_key:
                  type: string
                  description: 护照文件key（当id_type为passport时必填，通过文件上传接口获取）
                  example: hF7hAjd2lwSi5m
      responses:
        "200":
          description: 创建成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardholderResponse"
        "400":
          description: 参数错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cardholders/{holder_id}:
    put:
      tags:
        - Cardholders
      summary: 更新持卡人信息
      security:
        - HmacAuth: []
      parameters:
        - name: holder_id
          in: path
          required: true
          description: 持卡人唯一标识符（从创建持卡人接口返回的holder_id）
          schema:
            type: string
            example: ch_123456
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                first_name:
                  type: string
                  description: 持卡人名字（可选更新）
                  example: Wei
                last_name:
                  type: string
                  description: 持卡人姓氏（可选更新）
                  example: Zhang
                nationality:
                  type: string
                  description: 国籍代码（ISO 3166-1 alpha-2 格式，可选更新）
                  example: CN
                date_of_birth:
                  type: string
                  format: date
                  description: 出生日期（YYYY-MM-DD格式，可选更新）
                  example: "1990-05-20"
                email:
                  type: string
                  description: 电子邮箱地址（可选更新）
                  example: zhang.wei@example.com
                phone:
                  type: object
                  description: 手机号码信息（可选更新）
                  properties:
                    country_code:
                      type: string
                      description: 国家或地区电话区号（包含+号）
                      example: "+86"
                    number:
                      type: string
                      description: 手机号码（不包含区号）
                      example: "13800138000"
                billing_address:
                  type: object
                  description: 账单地址信息（可选更新）
                  properties:
                    country:
                      type: string
                      description: 国家代码（ISO 3166-1 alpha-2 格式）
                      example: CN
                    state:
                      type: string
                      description: |
                        省份或州名称
                        - 当 country 为 US 时，必须使用2位大写字母的州代码（如：CA, NY, TX）
                        - 其他国家可以使用完整的省份/州名称
                      example: Guangdong
                    city:
                      type: string
                      description: 城市名称
                      example: Shenzhen
                    address_line:
                      type: string
                      description: 详细地址（街道、门牌号等）
                      example: Nanshan District, High-tech Park 1st Road
                    postal_code:
                      type: string
                      description: 邮政编码
                      example: "518000"
                id_type:
                  type: string
                  enum:
                    - id_card
                    - passport
                  description: "证件类型（id_card: 身份证, passport: 护照，可选更新）"
                  example: id_card
                id_card_front_key:
                  type: string
                  description: 身份证人像面文件key（当id_type为id_card时必填，通过文件上传接口获取）
                  example: fD9fyhb0juQg3k
                id_card_back_key:
                  type: string
                  description: 身份证国徽面文件key（当id_type为id_card时必填，通过文件上传接口获取）
                  example: gE8gzic1kvRh4l
                passport_key:
                  type: string
                  description: 护照文件key（当id_type为passport时必填，通过文件上传接口获取）
                  example: hF7hAjd2lwSi5m
      responses:
        "200":
          description: 更新成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardholderResponse"
        "400":
          description: 参数错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 持卡人不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/bins:
    get:
      tags:
        - Cards
      summary: 获取卡 BIN 列表
      responses:
        "200":
          description: BIN 列表
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BinListResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards:
    post:
      tags:
        - Cards
      summary: 创建预付卡
      security:
        - HmacAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - bin_id
                - holder_id
                - amount
                - currency
              properties:
                bin_id:
                  type: string
                  description: BIN标识符（从/cards/bins接口获取可用的BIN ID）
                  example: bin_abc123
                holder_id:
                  type: string
                  description: 持卡人标识符（必须是已创建的持卡人ID）
                  example: ch_001
                amount:
                  type: number
                  format: float
                  description: 初始充值金额（必须大于等于最小充值金额）
                  example: 100.00
                currency:
                  type: string
                  description: 货币类型（ISO 4217货币代码，如USD、EUR等）
                  example: USD
                label:
                  type: string
                  description: 卡片标签（便于识别和管理，如"差旅卡"、"购物卡"等）
                  example: Travel Card
      responses:
        "200":
          description: 创建成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardResponse"
        "400":
          description: 参数错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 资源不存在（BIN不存在或持卡人不存在）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/{card_id}/topup:
    post:
      tags:
        - Cards
      summary: 卡充值
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（从创建卡片接口返回的card_id）
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
              properties:
                amount:
                  type: number
                  description: 充值金额（必须大于等于该卡片的最小充值金额限制）
                  example: 100
      responses:
        "200":
          description: 转入成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardTopupResponse"
        "400":
          description: 参数错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/{card_id}/withdraw:
    post:
      tags:
        - Cards
      summary: 卡提现
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（从创建卡片接口返回的card_id）
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
              properties:
                amount:
                  type: number
                  description: 提现金额（必须小于等于卡片当前余额，且卡片必须支持提现功能）
                  example: 50.00
      responses:
        "200":
          description: 转出成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/StandardResponse"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          transaction_number:
                            type: string
                            description: 交易流水号（用于跟踪和记录）
                            example: "txn_xxx"
                          requested_amount:
                            type: number
                            format: float
                            description: 请求提现金额
                            example: 50.00
                          withdraw_amount:
                            type: number
                            format: float
                            description: 实际提现金额
                            example: 49.00
                          fee_amount:
                            type: number
                            format: float
                            description: 提现手续费
                            example: 1.00
        "400":
          description: 参数错误（金额超出余额或低于最小限制）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: 该卡不支持提现功能
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/{card_id}/freeze:
    post:
      tags:
        - Cards
      summary: 卡冻结
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（从创建卡片接口返回的card_id）
          schema:
            type: string
      responses:
        "200":
          description: 冻结成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/StandardResponse"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          card_id:
                            type: string
                            description: 虚拟卡标识符
                            example: "card_1234567890"
                          status:
                            type: string
                            description: 卡片状态
                            example: "frozen"
        "400":
          description: 参数错误（卡片状态不允许冻结，如已注销的卡）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: 该卡不支持冻结功能
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/{card_id}/unfreeze:
    post:
      tags:
        - Cards
      summary: 卡解冻
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（从创建卡片接口返回的card_id）
          schema:
            type: string
      responses:
        "200":
          description: 解冻成功
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/StandardResponse"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          card_id:
                            type: string
                            description: 虚拟卡标识符
                            example: "card_1234567890"
                          status:
                            type: string
                            description: 卡片状态
                            example: "active"
        "400":
          description: 参数错误（卡片状态不允许解冻，如卡片未被冻结或已注销）
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: 该卡不支持解冻功能
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /v1/cards/{card_id}:
    get:
      tags:
        - Cards
      summary: 获取卡详情
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（用于查询指定卡片的详细信息）
          schema:
            type: string
            example: card_1234567890
      responses:
        "200":
          description: 获取成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      tags:
        - Cards
      summary: 注销虚拟卡
      security:
        - HmacAuth: []
      parameters:
        - name: card_id
          in: path
          required: true
          description: 虚拟卡唯一标识符（待注销的卡片ID）
          schema:
            type: string
            example: card_1234567890
      responses:
        "200":
          description: 注销成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardCancellationResponse"
        "401":
          description: 认证失败
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: 指定的虚拟卡不存在
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: 服务器内部错误
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  # Webhook Callbacks
  # 以下是 ShimaiPay 向您的系统发送的 Webhook 回调通知
  /v1/webhooks/transaction:
    post:
      tags:
        - Webhooks
      summary: 交易和持卡人事件 Webhook 回调
      description: |
        当发生交易事件或持卡人审核事件时，ShimaiPay 会向您配置的 Webhook URL 发送 POST 请求。

        **配置要求**：
        - Webhook 回调 URL 需要在 Hjtpay 商户后台的"Webhook 设置"页面进行配置
        - 配置完成后，系统才会向您的端点发送事件通知
        
        **安全性**：
        - 使用专门的 Webhook Secret（从 Hjtpay 商户后台获取）进行 HMAC-SHA256 签名验证
        - 签名在 `X-Signature` 头部中传递
        - 时间戳在 `X-Timestamp` 头部中传递
        - 签名算法：`HMAC-SHA256(WEBHOOK_SECRET, TIMESTAMP + "." + REQUEST_BODY)`
        
        **重试机制**：
        - 如果您的端点返回非 2xx 状态码，我们会重试发送
        - 重试间隔配置(秒): 5秒 → 10秒 → 30秒 → 60秒 → 15分 → 30分 → 1小时 → 4小时 → 8小时 → 12小时 → 24小时
        - 最多重试 11 次
        
        **响应要求**：
        - 您的端点应在 10 秒内响应
        - 返回 HTTP 状态码 200-299 表示成功接收
      security:
        - WebhookSignatureAuth: []
        - WebhookTimestamp: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            examples:
              consume_event:
                summary: 消费事件
                description: 当持卡人使用虚拟卡进行消费时触发
                value:
                  event_id: "evt_20250930abcd1234"
                  event_type: "transaction.created"
                  occurred_at: "2025-09-30T12:00:00Z"
                  data:
                    order_id: "ord_123456789"
                    card_id: "card_789xyz"
                    merchant:
                      merchant_id: "01424C9967"
                      name: "Amazon US"
                      city: "Seattle"
                      region: "US"
                      category_code: "5331"
                    transaction:
                      amount: 100
                      currency: "USD"
                      settlement_amount: 99
                      settlement_currency: "USD"
                      fee_amount: 1
                      fee_currency: "USD"
                      transaction_time: "2025-09-30T12:00:00Z"
                    status_code: "00"
                    failure_reason: null
              reversal_event:
                summary: 撤销事件
                description: 当交易被撤销时触发
                value:
                  event_id: "evt_20250930efgh5678"
                  event_type: "transaction.reversed"
                  occurred_at: "2025-09-30T12:30:00Z"
                  data:
                    order_id: "ord_123456789"
                    card_id: "card_789xyz"
                    merchant:
                      merchant_id: "01424C9967"
                      name: "Amazon US"
                      city: "Seattle"
                      region: "US"
                      category_code: "5331"
                    transaction:
                      amount: 100
                      currency: "USD"
                      settlement_amount: 99
                      settlement_currency: "USD"
                      fee_amount: 0
                      fee_currency: "USD"
                      transaction_time: "2025-09-30T12:30:00Z"
                    status_code: "00"
                    failure_reason: null
              refund_event:
                summary: 退款事件
                description: 当发生退款时触发
                value:
                  event_id: "evt_20250930ijkl9012"
                  event_type: "transaction.refunded"
                  occurred_at: "2025-09-30T13:00:00Z"
                  data:
                    order_id: "ord_123456789"
                    card_id: "card_789xyz"
                    merchant:
                      merchant_id: "01424C9967"
                      name: "Amazon US"
                      city: "Seattle"
                      region: "US"
                      category_code: "5331"
                    transaction:
                      amount: 50
                      currency: "USD"
                      settlement_amount: 49.50
                      settlement_currency: "USD"
                      fee_amount: 0.50
                      fee_currency: "USD"
                      transaction_time: "2025-09-30T13:00:00Z"
                    status_code: "00"
                    failure_reason: null
              verification_event:
                summary: 验证事件
                description: 当交易需要验证时触发
                value:
                  event_id: "evt_20250930uvwx3456"
                  event_type: "transaction.verification"
                  occurred_at: "2025-09-30T13:30:00Z"
                  data:
                    order_id: "ord_123456789"
                    card_id: "card_789xyz"
                    merchant:
                      merchant_id: "01424C9967"
                      name: "Amazon US"
                      city: "Seattle"
                      region: "US"
                      category_code: "5331"
                    transaction:
                      amount: 100
                      currency: "USD"
                      settlement_amount: 99
                      settlement_currency: "USD"
                      fee_amount: 1
                      fee_currency: "USD"
                      transaction_time: "2025-09-30T13:30:00Z"
                    status_code: "00"
                    failure_reason: null
              cardholder_approved_event:
                summary: 持卡人审核通过事件
                description: 当持卡人审核通过时触发
                value:
                  event_id: "evt_20250930mnop3456"
                  event_type: "cardholder.approved"
                  occurred_at: "2025-09-30T14:00:00Z"
                  data:
                    holder_id: "ch_123456"
                    status: "approved"
                    rejection_reason: null
              cardholder_rejected_event:
                summary: 持卡人审核驳回事件
                description: 当持卡人审核驳回时触发
                value:
                  event_id: "evt_20250930qrst7890"
                  event_type: "cardholder.rejected"
                  occurred_at: "2025-09-30T14:30:00Z"
                  data:
                    holder_id: "ch_123456"
                    status: "rejected"
                    rejection_reason: "身份证照片不清晰，请重新上传"
      responses:
        "200":
          description: Webhook 接收成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    description: 接收状态确认
                    example: "received"
        "400":
          description: 请求格式错误（JSON格式不正确或缺少必要字段）
        "401":
          description: 签名验证失败（HMAC签名不匹配或时间戳过期）
        "500":
          description: 接收方服务器内部错误