> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Mode C：即时履行 API

> URL 签名跳转、即时履行、履行结果查询与履行结果通知 Webhook。用户支付完成后 Waffo Point Topup 调用供应商接口，直接给用户账户充值。

Mode C 面向能直接给用户账户充值的供应商。用户支付完成后，Waffo Point Topup 调用你的 API 完成充值，用户不需要处理点卡码。

<CardGroup cols={2}>
  <Card title="优势" icon="circle-check">
    * 用户体验顺滑，不需要点卡码
    * 账户即时到账
    * 转化率更高
  </Card>

  <Card title="适用场景" icon="users">
    * 具备账户充值系统的游戏发行商
    * 会员 / 订阅类服务
  </Card>
</CardGroup>

## 集成流程

<Tabs>
  <Tab title="简化流程">
    <Frame>
      <img src="https://mintcdn.com/waffo-docs/hUiobY-hbNbq3QYe/images/developer-docs/point-topup/mode-c-flow-simple.png?fit=max&auto=format&n=hUiobY-hbNbq3QYe&q=85&s=abc2f4eabfba9391ae31235d0407ff4c" alt="Mode C 简化流程：供应商生成签名跳转链接、用户在 Waffo 支付、Waffo 调用供应商履行接口" width="3244" height="2933" data-path="images/developer-docs/point-topup/mode-c-flow-simple.png" />
    </Frame>
  </Tab>

  <Tab title="详细流程">
    <Frame>
      <img src="https://mintcdn.com/waffo-docs/hUiobY-hbNbq3QYe/images/developer-docs/point-topup/mode-c-flow-detailed.png?fit=max&auto=format&n=hUiobY-hbNbq3QYe&q=85&s=72a90991e77f8e9d54d5545498ae843c" alt="Mode C 详细流程：含签名校验、会话建立、支付、履行调用、履行结果查询与 Webhook 通知的完整时序" width="4347" height="7001" data-path="images/developer-docs/point-topup/mode-c-flow-detailed.png" />
    </Frame>
  </Tab>
</Tabs>

## 接口清单

| 序号 | 接口名                                   | 说明                                                        | 提供方      | 集成要求       |
| -- | ------------------------------------- | --------------------------------------------------------- | -------- | ---------- |
| 1  | [URL 签名与跳转](#url-签名与跳转)               | URL 签名认证                                                  | Waffo    | **必需**     |
| 2  | [即时履行](#即时履行)                         | 创建即时履行订单                                                  | 供应商      | 可选集成（示例规范） |
| 3  | [履行结果查询](#履行结果查询)                     | 查询即时履行订单。如果你的充值请求本身支持幂等，就不需要这个接口——Waffo 会用同一个订单号重试，由你保证幂等 | 供应商      | 可选集成（示例规范） |
| 4  | [履行结果通知 Webhook](#履行结果通知-webhook)     | Waffo 把最终履行状态推给供应商                                        | Waffo 发送 | 可选         |
| 5  | [Waffo 履行结果查询 API](#waffo-履行结果查询-api) | 供应商主动向 Waffo 拉取履行结果                                       | Waffo    | 可选         |

<Note>
  最小集成只需要第 1 项加第 4 项：签名跳转 + 履行结果通知 Webhook，不实现任何履行接口。这种情况下 Webhook 只会返回 `PAY_SUCCESS` 与 `PAYMENT_FAILED`，由你在收到 `PAY_SUCCESS` 后走自己的发货流程。
</Note>

## URL 签名与跳转

通过带签名校验的 URL 参数传递用户信息，保证参数完整性。这是 Waffo 提供的端点。

**完整的参数清单、类型、必填性与示例见 [API 参考：签名跳转](/docs/api-reference/point-topup-redirect/signed-redirect-to-checkout)。** 本节只讲怎么把签名算出来。

<Warning>
  **签名必须在供应商后端生成，绝不能把 `SECRET_KEY` 暴露到前端。** 签名有效期由 `timestamp` 控制，超过 2 小时的请求会被拒绝。
</Warning>

URL 格式：

```text theme={null}
https://{supplier}.waffoplay.com/redirect?amount={amount}&currency={currency}&faceValue={faceValue}&notifyUrl={notifyUrl}&productId={productId}&returnUrl={returnUrl}&salesOrderId={salesOrderId}&siteCode={siteCode}&supplierId={supplierId}&supplierUserAccount={supplierUserAccount}&theme={theme}&timestamp={timestamp}&signature={signature}
```

其中 `{supplier}` 是入网时 Waffo 分配给你的专属子域名。

<Info>
  **也可以使用自定义商户域名。** 前提是供应商侧把该域名解析指向 Waffo 的 endpoint。启用后，拼接待签串时的 base URL 必须换成这个自定义域名——签名覆盖 base URL，域名不一致会导致验签失败。
</Info>

### 签名算法

<Steps>
  <Step title="剔除参数">
    排除 `signature` 参数，并**丢掉所有值为空或空白的参数**。
  </Step>

  <Step title="排序">
    剩余参数按参数名 ASCII 升序排列。
  </Step>

  <Step title="拼接待签串">
    按 `{baseUrl}?key1=value1&key2=value2&...` 拼接。**base URL（scheme + host + path，例如 `https://supplier.waffoplay.com/redirect`）必须包含在内。**
  </Step>

  <Step title="计算签名">
    以共享的 `SECRET_KEY` 作为 HMAC 密钥，对待签串计算 HMAC-SHA256。
  </Step>

  <Step title="转大写">
    十六进制编码后转为大写。
  </Step>
</Steps>

### 签名示例

给定参数：

| 参数                    | 值                                                      |
| --------------------- | ------------------------------------------------------ |
| `supplierUserAccount` | `supplier_user_12345`                                  |
| `supplierUserEmail`   | `test@supplier.com`                                    |
| `supplierId`          | `WAFFO_POINT_TOPUP_001`                                |
| `faceValue`           | `3000`                                                 |
| `amount`              | `3000`                                                 |
| `currency`            | `JPY`                                                  |
| `salesOrderId`        | `A123456`                                              |
| `siteCode`            | `US`                                                   |
| `returnUrl`           | `https://example.com/return_url_page?salesOrderId=XXX` |
| `theme`               | `light`                                                |
| `timestamp`           | `1713483091000`                                        |
| `SECRET_KEY`          | `your_secret_key_here`                                 |

第 1 步，排序并拼接：

```text theme={null}
https://supplier.waffoplay.com/redirect?amount=3000&currency=JPY&faceValue=3000&returnUrl=https://example.com/return_url_page?salesOrderId=XXX&salesOrderId=A123456&siteCode=US&supplierId=WAFFO_POINT_TOPUP_001&supplierUserAccount=supplier_user_12345&supplierUserEmail=test@supplier.com&theme=light&timestamp=1713483091000
```

第 2 步，计算 HMAC-SHA256 并转大写：

```text theme={null}
signature = HMAC_SHA256(上面的待签串).toUpperCase()
```

完整 URL：

```text theme={null}
https://supplier.waffoplay.com/redirect?amount=3000&currency=JPY&faceValue=3000&returnUrl=https%3A%2F%2Fexample.com%2Freturn_url_page%3FsalesOrderId%3DXXX&salesOrderId=A123456&siteCode=US&supplierId=WAFFO_POINT_TOPUP_001&supplierUserAccount=supplier_user_12345&supplierUserEmail=test@supplier.com&theme=light&timestamp=1713483091000&signature=ABC123XYZ456DEF789
```

<Tip>
  注意待签串里 `returnUrl` 用的是**未编码**的原值，而最终 URL 里 `returnUrl` 是**编码后**的值。签名和传输是两个不同环节，不要混用。
</Tip>

### Java 实现示例

```java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;

public class SignatureUtil {

    private static final String HMAC_SHA256 = "HmacSHA256";

    /**
     * Generate signature using HMAC-SHA256 algorithm (including base URL)
     *
     * @param baseUrl   base URL including host and path (e.g., https://supplier.waffoplay.com/redirect)
     * @param params    request parameters
     * @param secretKey secret key
     * @return signature string (uppercase hexadecimal)
     */
    public static String generateSignature(String baseUrl, Map<String, String> params, String secretKey) {
        try {
            // Use TreeMap for automatic alphabetical sorting
            TreeMap<String, String> sortedParams = new TreeMap<>(params);

            // Remove existing signature parameter if present
            sortedParams.remove("signature");

            // Build full URL with sorted parameters
            StringBuilder fullUrl = new StringBuilder(baseUrl);
            fullUrl.append("?");
            for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
                if (fullUrl.charAt(fullUrl.length() - 1) != '?') {
                    fullUrl.append("&");
                }
                fullUrl.append(entry.getKey())
                       .append("=")
                       .append(entry.getValue());
            }

            // Calculate signature using HMAC-SHA256 algorithm
            Mac mac = Mac.getInstance(HMAC_SHA256);
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                secretKey.getBytes(StandardCharsets.UTF_8),
                HMAC_SHA256
            );
            mac.init(secretKeySpec);
            byte[] hash = mac.doFinal(fullUrl.toString().getBytes(StandardCharsets.UTF_8));

            // Convert to hexadecimal string and uppercase
            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().toUpperCase();
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate HMAC-SHA256 signature", e);
        }
    }

    /**
     * Verify signature
     */
    public static boolean verifySignature(String baseUrl, Map<String, String> params, String secretKey) {
        String receivedSignature = params.get("signature");
        if (receivedSignature == null || receivedSignature.isEmpty()) {
            return false;
        }
        String calculatedSignature = generateSignature(baseUrl, params, secretKey);
        return calculatedSignature.equals(receivedSignature);
    }

    /**
     * Build the final signed URL
     */
    public static String buildSignedUrl(String baseUrl, Map<String, String> params, String secretKey) {
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        sortedParams.remove("signature");

        String signature = generateSignature(baseUrl, sortedParams, secretKey);
        sortedParams.put("signature", signature);

        StringBuilder url = new StringBuilder(baseUrl);
        url.append("?");
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            if (url.charAt(url.length() - 1) != '?') {
                url.append("&");
            }
            url.append(entry.getKey())
               .append("=")
               .append(entry.getValue());
        }

        return url.toString();
    }
}
```

<Card title="Node.js 签名示例（HMAC-SHA256 & RSA-SHA256）" icon="file-archive" href="/docs/files/developer-docs/point-topup/waffoplay-sign-methods.zip">
  下载 `Waffoplay Sign Methods.zip`，内含 Node.js 的 HMAC-SHA256 与 RSA-SHA256 签名实现。
</Card>

### 校验流程

用户访问签名 URL 后，Waffo Point Topup 后端会：

1. 接收所有 URL 参数
2. 校验必填参数是否齐全：`faceValue`、`salesOrderId`、`supplierId`、`supplierUserAccount`、`siteCode`、`timestamp`、`signature`
3. 校验 `timestamp` 在有效期（2 小时）内
4. 用同样的算法重新计算签名
5. 把计算出的签名与请求中的签名比对
6. 校验通过后生成 JWT Token 并写入用户会话
7. 跳转到目标 `siteCode` 的购买页

错误处理：

| 情况     | 结果              |
| ------ | --------------- |
| 参数缺失   | 跳转到错误页，提示缺少必填参数 |
| 签名校验失败 | 跳转到错误页，提示签名无效   |
| 时间戳过期  | 跳转到过期页，提示链接已失效  |

## 即时履行

用户支付完成后由 Waffo Point Topup 调用。

| 项      | 值                                                                                       |
| ------ | --------------------------------------------------------------------------------------- |
| Method | `POST`                                                                                  |
| Path   | 由供应商定义                                                                                  |
| 认证     | 共通 API 签名（SHA256WithRSA），见 [API 共通规范](/docs/zh/developer-docs/point-topup/api-common#api-安全) |

<Warning>
  本节接口规范只是**示例**。如果你已有即时充值接口，直接把自己的接口文档给 Waffo Point Topup，Waffo Point Topup 会适配你的既有接口。
</Warning>

<Warning>
  **幂等要求：** 供应商必须以 `salesOrderId` + `supplierId` 支持幂等。同一请求重复到达时，返回相同的成功响应。
</Warning>

<Tip>
  **强烈建议：** 校验同一 `salesOrderId` 的 `faceValue`、`amount`、`currency` 与你内部系统的订单记录严格一致。业务校验不通过时，应拒绝履行该笔交易。
</Tip>

### 请求参数

| 字段名            |                       | 说明                                                        | 类型            | 必填 |
| -------------- | --------------------- | --------------------------------------------------------- | ------------- | -- |
| `supplierId`   |                       | Waffo 分配给供应商的唯一标识                                         | String(64)    | 必填 |
| `salesOrderId` |                       | 签名 URL 中传入的供应商订单号。与 `supplierId` 一起作为幂等键                  | String(64)    | 必填 |
| `faceValue`    |                       | 面额                                                        | String(32)    | 必填 |
| `amount`       |                       | 订单实际应付金额。如果同时拿到**面额**和**订单金额**，建议在两者之间做一次**校验**           | DECIMAL(20,8) | 选填 |
| `currency`     |                       | 订单定价币种                                                    | String(3)     | 选填 |
| `requestedAt`  |                       | Waffo 侧请求时间。ISO 8601 扩展格式（UTC），`YYYY-MM-DDThh:mm:ss.sssZ` | String(32)    | 必填 |
| `buyerInfo`    |                       | 购买者信息。涉及即时充值的场景下必须提供                                      | Object        | 选填 |
|                | `supplierUserAccount` | 充值账号 ID                                                   | String(64)    | 选填 |

<Note>
  `requestedAt` 中，`T` 是日期与时间的分隔符，`.000` 是毫秒，`Z` 表示 UTC（协定世界时）。例如 `2025-01-05T10:30:00.000Z` 表示 UTC 时间 2025 年 1 月 5 日 10:30:00。
</Note>

### 响应参数

| 字段名            | 说明                                               | 类型         | 必填 |
| -------------- | ------------------------------------------------ | ---------- | -- |
| `salesOrderId` | 签名 URL 中传入的供应商订单号                                | String(32) | 必填 |
| `status`       | 订单状态：`IN_PROGRESS` 处理中，`SUCCESS` 成功，`FAILURE` 失败 | String(24) | 必填 |

响应示例：

```json theme={null}
{
  "code": "0",
  "msg": "success",
  "data": {
    "salesOrderId": "M202504160311311156635",
    "status": "IN_PROGRESS"
  }
}
```

## 履行结果查询

由 Waffo Point Topup 调用，用于向供应商查询履行结果。

| 项      | 值                                                                                       |
| ------ | --------------------------------------------------------------------------------------- |
| Method | `POST`                                                                                  |
| Path   | 由供应商定义                                                                                  |
| 认证     | 共通 API 签名（SHA256WithRSA），见 [API 共通规范](/docs/zh/developer-docs/point-topup/api-common#api-安全) |

<Note>
  如果你的即时履行接口本身支持幂等，就不需要提供这个查询接口——Waffo 会用同一个 `salesOrderId` 重试，由你保证幂等处理。
</Note>

### 请求参数

| 字段名            | 说明                | 类型         | 必填 |
| -------------- | ----------------- | ---------- | -- |
| `salesOrderId` | 签名 URL 中传入的供应商订单号 | String(64) | 必填 |
| `supplierId`   | Waffo 分配给供应商的唯一标识 | String(64) | 必填 |

### 响应参数

| 字段名             |   | 说明                                     | 类型         | 必填 |
| --------------- | - | -------------------------------------- | ---------- | -- |
| `salesOrderId`  |   | 签名 URL 中传入的供应商订单号                      | String(64) | 必填 |
| `supplierId`    |   | Waffo 分配给供应商的唯一标识                      | String(64) | 必填 |
| `status`        |   | 订单状态：`IN_PROGRESS`、`SUCCESS`、`FAILURE` | String(24) | 必填 |
| `faceValue`     |   | 面额。动态面额的游戏点卡商品需要明确该字段                  | String(32) | 选填 |
| `buyerInfo`     |   | 购买者信息，对象结构与即时履行请求相同                    | Object     | 选填 |
| `requestedAt`   |   | Waffo 侧请求时间，与履行请求一致                    | String(32) | 必填 |
| `completedTime` |   | 订单完成时间                                 | String(32) | 选填 |
| `failureCode`   |   | 订单失败码                                  | String(32) | 选填 |
| `failureReason` |   | 订单失败原因                                 | String(64) | 选填 |

响应示例：

```json theme={null}
{
  "code": "0",
  "msg": "success",
  "data": {
    "salesOrderId": "M202504160311311156635",
    "supplierId": "123",
    "status": "SUCCESS",
    "faceValue": 1000,
    "buyerInfo": {
      "supplierUserAccount": "user@example.com"
    },
    "requestedAt": "2024-04-16T03:11:31.000Z",
    "completedTime": "2024-04-16T03:12:31.000Z"
  }
}
```

## 履行结果通知 Webhook

Waffo Point Topup 把最终履行状态 POST 到你在签名 URL 的 `notifyUrl` 参数里给出的端点，覆盖支付成功但履行失败、支付失败、以及超时未支付这几类情况。

**完整字段清单、`failureCode` 全表与请求示例见 [API 参考：履行结果 Webhook](/docs/api-reference/zh/point-topup-fulfillment-webhook)。**

<Warning>
  **幂等要求：** 供应商必须以 `salesOrderId` + `supplierId` 支持幂等。同一通知重复到达时，返回相同的成功响应。
</Warning>

驱动你集成逻辑的是 `fulfillmentStatus`：

| 取值                          | 含义                                    |
| --------------------------- | ------------------------------------- |
| `SUCCESS`                   | 履行成功                                  |
| `PAY_SUCCESS`               | 支付成功。发送给未接入履行 API 的供应商，作为其自有发货流程的触发信号 |
| `PAY_SUCCESS_SUPPLY_FAILED` | 支付成功但履行失败，需要确认退款或重发                   |
| `PAYMENT_FAILED`            | 支付失败，含用户支付超时                          |

<Note>
  如果你选择最小集成（只做签名跳转 + 本 Webhook，不实现履行接口），Waffo 只会返回 `PAY_SUCCESS` 与 `PAYMENT_FAILED` 两种状态。
</Note>

### 响应与重试策略

收到履行回调后，如果处理成功，请返回 HTTP `200 OK`，并在响应体中包含 `success`。Waffo Point Topup 据此认为履行结果已成功通知供应商；**否则 Waffo Point Topup 会重试。**

```json theme={null}
{
    "message": "success"
}
```

| 项      | 值                                                          |
| ------ | ---------------------------------------------------------- |
| 重试间隔   | 5 秒、30 秒、1 分钟、5 分钟、30 分钟、1 小时、2 小时、4 小时、8 小时、24 小时、24 小时…… |
| 最大重试次数 | 15 次                                                       |
| 超过上限后  | 通知标记为失败，需要人工处理                                             |

## Waffo 履行结果查询 API

供应商主动向 Waffo Point Topup 查询 Mode C 订单的履行结果。这是履行结果通知 Webhook 的**拉取式对应版本**，通常用于对账，或漏收 Webhook 通知时的兜底。响应的 `data` 结构与 Webhook 的 `data` 完全一致。

**完整参数、响应示例与在线调用见 [API 参考：履行结果查询](/docs/api-reference/point-topup-fulfillment-inquiry/fulfillment-inquiry)。**

| 项      | 值                                                                                       |
| ------ | --------------------------------------------------------------------------------------- |
| Method | `POST`                                                                                  |
| Path   | `/api/v1/gamepin/fulfillment-inquiry`                                                   |
| 认证     | 共通 API 签名（SHA256WithRSA），见 [API 共通规范](/docs/zh/developer-docs/point-topup/api-common#api-安全) |

查询键：`supplierId` 必填，`salesOrderId` 与 `payOrderId` 至少要传一个。订单不存在、或该订单不属于当前供应商时，按[错误码](/docs/zh/developer-docs/point-topup/api-common#错误码)规范返回错误响应。

```json theme={null}
{
    "supplierId": "WAFFO_POINT_TOPUP_001",
    "salesOrderId": "A123456"
}
```

## 下一步

<CardGroup cols={2}>
  <Card title="API 共通规范" icon="shield-check" href="/docs/zh/developer-docs/point-topup/api-common">
    消息结构、RSA 签名验签、错误码与密钥生成。
  </Card>

  <Card title="集成总览" icon="map" href="/docs/zh/developer-docs/point-topup/overview">
    三种模式的完整对比与接入准备清单。
  </Card>
</CardGroup>
