Back to skills
SkillHub ClubShip Full StackFull StackIntegration

kryptogo-pay-webhook

Implements KryptoGO Payment webhook/callback handling for receiving payment status notifications. Use when building webhook endpoints, handling payment callbacks, or implementing 支付狀態通知 for KryptoGO Payment.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
221
Hot score
97
Updated
March 20, 2026
Overall rating
C3.1
Composite score
3.1
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install paid-tw-skills-kryptogo-pay-webhook

Repository

paid-tw/skills

Skill path: plugins/kryptogo-pay/skills/kryptogo-pay-webhook

Implements KryptoGO Payment webhook/callback handling for receiving payment status notifications. Use when building webhook endpoints, handling payment callbacks, or implementing 支付狀態通知 for KryptoGO Payment.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: paid-tw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install kryptogo-pay-webhook into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/paid-tw/skills before adding kryptogo-pay-webhook to shared team environments
  • Use kryptogo-pay-webhook for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: kryptogo-pay-webhook
description: >
  Implements KryptoGO Payment webhook/callback handling for receiving payment status
  notifications. Use when building webhook endpoints, handling payment callbacks,
  or implementing 支付狀態通知 for KryptoGO Payment.
argument-hint: "[框架: Express/Flask/FastAPI/NestJS]"
context: fork
agent: general-purpose
disable-model-invocation: true
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
user-invocable: true
---

# KryptoGO Payment Webhook 回調處理任務

你的任務是在用戶的專案中實作 KryptoGO Payment Webhook 回調處理。

## 串接 Checklist

- [ ] **端點建立** - 建立 POST endpoint 接收回調
- [ ] **狀態處理** - 處理所有 5 種支付狀態
- [ ] **冪等處理** - 實作冪等性避免重複處理
- [ ] **回應確認** - 回應 HTTP 200 確認收到
- [ ] **測試驗證** - 驗證端點可正常運作

---

## Step 1: 確認環境

詢問用戶:

1. **框架類型**:你使用什麼後端框架?
   - Node.js (Express / Fastify / NestJS)
   - Python (Django / Flask / FastAPI)
   - 其他

2. **資料庫**:使用什麼資料庫儲存訂單?
   - 需要知道如何更新訂單狀態

用戶輸入: `$ARGUMENTS`

## Step 2: 建立 Webhook 端點

建立 POST 端點接收 KryptoGO 的回調通知。

**端點路徑建議**: `/api/payment/callback` 或 `/webhook/kryptogo`

**必要行為**:
1. 接收 POST JSON body
2. 驗證 `payment_intent_id` 存在於資料庫
3. 根據 `status` 更新訂單狀態
4. 回應 HTTP 200

## Step 3: 處理所有支付狀態

必須處理以下 5 種狀態:

| 狀態 | 處理邏輯 |
|------|---------|
| `pending` | 通常不會收到此狀態的回調 |
| `success` | 更新訂單為已付款,記錄 `payment_tx_hash` |
| `expired` | 標記訂單為過期 |
| `insufficient_not_refunded` | 記錄異常,等待退款 |
| `insufficient_refunded` | 記錄退款資訊 `refund_tx_hash`、`refund_amount` |

## Step 4: 實作冪等性

確保同一個 `payment_intent_id` 的回調不會被重複處理:
- 檢查訂單是否已經更新為最終狀態
- 使用 `payment_intent_id` 作為冪等鍵

## Step 5: 測試

1. 建立一個 Payment Intent(帶 `callback_url`)
2. 完成支付後確認端點收到回調
3. 驗證訂單狀態正確更新
4. 驗證回應 HTTP 200

---

## Callback Payload 格式

```json
{
  "payment_intent_id": "0h39QkYfZps7AUD1xQsj3MDFVLIMaGoV",
  "client_id": "9c5a79fc1117310f976b53752659b61d",
  "fiat_amount": "300.0",
  "fiat_currency": "TWD",
  "payment_deadline": 1715462400,
  "status": "success",
  "payment_chain_id": "arb",
  "symbol": "USDT",
  "crypto_amount": "2.53",
  "payment_tx_hash": "0x1234567890abcdef...",
  "received_crypto_amount": "2.53",
  "aggregated_crypto_amount": "2.50",
  "order_data": {
    "order_id": "uid_12345",
    "item_id": "100"
  },
  "callback_url": "https://example.com/callback",
  "group_key": "buy_stone_with_usdt"
}
```

## 重要欄位

| 欄位 | 說明 |
|------|------|
| `payment_intent_id` | 用來比對你資料庫中的訂單 |
| `status` | 判斷該做什麼處理 |
| `payment_tx_hash` | 成功時的區塊鏈交易 Hash |
| `received_crypto_amount` | 實際收到的加密貨幣金額 |
| `aggregated_crypto_amount` | 扣除手續費後的金額 |
| `refund_tx_hash` | 退款時的區塊鏈交易 Hash |
| `refund_amount` | 退款金額 |
| `order_data` | 你建立 Payment Intent 時傳入的自訂資料 |

---

## 詳細參考文件

- [程式碼範例 (Node.js/Python)](references/code-examples.md)
- [Callback Payload 完整格式](references/callback-payload.md)
- [疑難排解](references/troubleshooting.md)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/code-examples.md

```markdown
# KryptoGO Payment Webhook 程式碼範例

## 目錄

- [Node.js (Express) 範例](#nodejs-express-範例)
- [Node.js (NestJS) 範例](#nodejs-nestjs-範例)
- [Python (Flask) 範例](#python-flask-範例)
- [Python (FastAPI) 範例](#python-fastapi-範例)

---

## Node.js (Express) 範例

```javascript
const express = require('express');
const router = express.Router();

router.post('/api/payment/callback', express.json(), async (req, res) => {
  const payment = req.body;
  const { payment_intent_id, status } = payment;

  // 1. 驗證 payment_intent_id 存在
  const order = await Order.findOne({
    where: { payment_intent_id },
  });

  if (!order) {
    console.warn('Unknown payment_intent_id:', payment_intent_id);
    return res.status(200).send(); // 仍回應 200 避免重試
  }

  // 2. 冪等檢查:若訂單已為最終狀態則跳過
  if (['success', 'expired', 'insufficient_refunded'].includes(order.status)) {
    return res.status(200).send();
  }

  // 3. 根據狀態更新訂單
  switch (status) {
    case 'success':
      await order.update({
        status: 'paid',
        tx_hash: payment.payment_tx_hash,
        received_amount: payment.received_crypto_amount,
        aggregated_amount: payment.aggregated_crypto_amount,
      });
      // 發送確認通知給用戶
      await notifyUser(order.user_id, 'payment_success');
      break;

    case 'expired':
      await order.update({ status: 'expired' });
      break;

    case 'insufficient_not_refunded':
      await order.update({
        status: 'insufficient',
        received_amount: payment.received_crypto_amount,
      });
      break;

    case 'insufficient_refunded':
      await order.update({
        status: 'refunded',
        refund_tx_hash: payment.refund_tx_hash,
        refund_amount: payment.refund_amount,
      });
      break;
  }

  // 4. 回應 200 確認收到
  res.status(200).send();
});

module.exports = router;
```

---

## Node.js (NestJS) 範例

```typescript
import { Controller, Post, Body, HttpCode } from '@nestjs/common';

interface PaymentCallback {
  payment_intent_id: string;
  client_id: string;
  fiat_amount: string;
  fiat_currency: 'TWD' | 'USD';
  status: 'pending' | 'success' | 'expired' | 'insufficient_not_refunded' | 'insufficient_refunded';
  payment_chain_id: string;
  symbol: string;
  crypto_amount: string;
  payment_tx_hash: string | null;
  received_crypto_amount: string | null;
  aggregated_crypto_amount: string | null;
  refund_tx_hash: string | null;
  refund_amount: string | null;
  order_data: Record<string, any> | null;
  callback_url: string | null;
  group_key: string | null;
}

@Controller('api/payment')
export class PaymentController {
  constructor(private readonly orderService: OrderService) {}

  @Post('callback')
  @HttpCode(200)
  async handleCallback(@Body() payment: PaymentCallback) {
    const order = await this.orderService.findByPaymentIntentId(
      payment.payment_intent_id,
    );

    if (!order) return;

    // 冪等檢查
    if (['paid', 'expired', 'refunded'].includes(order.status)) return;

    switch (payment.status) {
      case 'success':
        await this.orderService.markPaid(order.id, {
          txHash: payment.payment_tx_hash,
          receivedAmount: payment.received_crypto_amount,
          aggregatedAmount: payment.aggregated_crypto_amount,
        });
        break;

      case 'expired':
        await this.orderService.markExpired(order.id);
        break;

      case 'insufficient_not_refunded':
        await this.orderService.markInsufficient(order.id, {
          receivedAmount: payment.received_crypto_amount,
        });
        break;

      case 'insufficient_refunded':
        await this.orderService.markRefunded(order.id, {
          refundTxHash: payment.refund_tx_hash,
          refundAmount: payment.refund_amount,
        });
        break;
    }
  }
}
```

---

## Python (Flask) 範例

```python
from flask import Flask, request

app = Flask(__name__)

@app.route('/api/payment/callback', methods=['POST'])
def payment_callback():
    payment = request.json
    payment_intent_id = payment.get('payment_intent_id')
    status = payment.get('status')

    # 1. 驗證 payment_intent_id 存在
    order = Order.query.filter_by(payment_intent_id=payment_intent_id).first()
    if not order:
        return '', 200  # 仍回應 200

    # 2. 冪等檢查
    if order.status in ('paid', 'expired', 'refunded'):
        return '', 200

    # 3. 根據狀態更新訂單
    if status == 'success':
        order.status = 'paid'
        order.tx_hash = payment.get('payment_tx_hash')
        order.received_amount = payment.get('received_crypto_amount')
        order.aggregated_amount = payment.get('aggregated_crypto_amount')
        db.session.commit()
        notify_user(order.user_id, 'payment_success')

    elif status == 'expired':
        order.status = 'expired'
        db.session.commit()

    elif status == 'insufficient_not_refunded':
        order.status = 'insufficient'
        order.received_amount = payment.get('received_crypto_amount')
        db.session.commit()

    elif status == 'insufficient_refunded':
        order.status = 'refunded'
        order.refund_tx_hash = payment.get('refund_tx_hash')
        order.refund_amount = payment.get('refund_amount')
        db.session.commit()

    return '', 200
```

---

## Python (FastAPI) 範例

```python
from fastapi import FastAPI, Response
from pydantic import BaseModel
from typing import Optional, Dict, Any

app = FastAPI()

class PaymentCallback(BaseModel):
    payment_intent_id: str
    client_id: str
    fiat_amount: str
    fiat_currency: str
    status: str
    payment_chain_id: str
    symbol: str
    crypto_amount: str
    payment_tx_hash: Optional[str] = None
    received_crypto_amount: Optional[str] = None
    aggregated_crypto_amount: Optional[str] = None
    refund_tx_hash: Optional[str] = None
    refund_amount: Optional[str] = None
    order_data: Optional[Dict[str, Any]] = None
    callback_url: Optional[str] = None
    group_key: Optional[str] = None

@app.post('/api/payment/callback')
async def payment_callback(payment: PaymentCallback, response: Response):
    order = await get_order_by_payment_intent_id(payment.payment_intent_id)

    if not order:
        response.status_code = 200
        return

    # 冪等檢查
    if order.status in ('paid', 'expired', 'refunded'):
        response.status_code = 200
        return

    if payment.status == 'success':
        await update_order(order.id, status='paid',
                          tx_hash=payment.payment_tx_hash,
                          received_amount=payment.received_crypto_amount)

    elif payment.status == 'expired':
        await update_order(order.id, status='expired')

    elif payment.status == 'insufficient_not_refunded':
        await update_order(order.id, status='insufficient',
                          received_amount=payment.received_crypto_amount)

    elif payment.status == 'insufficient_refunded':
        await update_order(order.id, status='refunded',
                          refund_tx_hash=payment.refund_tx_hash,
                          refund_amount=payment.refund_amount)

    response.status_code = 200
```

```

### references/callback-payload.md

```markdown
# KryptoGO Payment Callback Payload 完整格式

## Callback 觸發時機

KryptoGO 會在以下時機發送 POST 請求到你的 `callback_url`:

1. 支付成功(`status: success`)
2. 支付過期(`status: expired`)
3. 金額不足等待退款(`status: insufficient_not_refunded`)
4. 金額不足已退款(`status: insufficient_refunded`)

## Payload 格式

```json
{
  "payment_intent_id": "0h39QkYfZps7AUD1xQsj3MDFVLIMaGoV",
  "client_id": "9c5a79fc1117310f976b53752659b61d",
  "fiat_amount": "300.0",
  "fiat_currency": "TWD",
  "payment_deadline": 1715462400,
  "status": "success",
  "payment_chain_id": "arb",
  "symbol": "USDT",
  "crypto_amount": "2.53",
  "payment_tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "received_crypto_amount": "2.53",
  "aggregated_crypto_amount": "2.50",
  "order_data": {
    "order_id": "uid_12345",
    "item_id": "100"
  },
  "callback_url": "https://example.com/callback",
  "group_key": "buy_stone_with_usdt"
}
```

## 欄位說明

| 欄位 | 類型 | 說明 |
|------|------|------|
| `payment_intent_id` | String | 支付交易唯一識別碼 |
| `client_id` | String | 你的 KryptoGO Client ID |
| `fiat_amount` | String | 法幣金額 |
| `fiat_currency` | String | 法幣幣別(TWD / USD)|
| `payment_deadline` | Number | 支付截止時間(Unix timestamp)|
| `status` | String | 支付狀態 |
| `payment_chain_id` | String | 區塊鏈網路(如 `arb`)|
| `symbol` | String | 代幣符號(如 `USDT`)|
| `crypto_amount` | String | 需支付的加密貨幣金額 |
| `payment_tx_hash` | String / null | 支付交易 Hash(成功時有值)|
| `received_crypto_amount` | String / null | 實際收到的金額 |
| `aggregated_crypto_amount` | String / null | 扣除手續費後的金額 |
| `order_data` | Object / null | 你建立 Intent 時傳入的自訂資料 |
| `callback_url` | String / null | 回調 URL |
| `group_key` | String / null | 支付分類標籤 |

## 各狀態 Payload 差異

### success

所有金額欄位皆有值:

| 欄位 | 值 |
|------|------|
| `payment_tx_hash` | 有值 |
| `received_crypto_amount` | 有值 |
| `aggregated_crypto_amount` | 有值(= received - 手續費)|
| `refund_tx_hash` | null |
| `refund_amount` | null |

### expired

金額相關欄位為 null:

| 欄位 | 值 |
|------|------|
| `payment_tx_hash` | null |
| `received_crypto_amount` | null |
| `aggregated_crypto_amount` | null |

### insufficient_not_refunded

有收到金額但不足:

| 欄位 | 值 |
|------|------|
| `payment_tx_hash` | 有值 |
| `received_crypto_amount` | 有值(< crypto_amount)|
| `refund_tx_hash` | null(尚未退款)|
| `refund_amount` | null |

### insufficient_refunded

金額不足且已退款:

| 欄位 | 值 |
|------|------|
| `payment_tx_hash` | 有值 |
| `received_crypto_amount` | 有值 |
| `refund_tx_hash` | 有值 |
| `refund_amount` | 有值 |

## 回應要求

你的端點**必須**回應 HTTP 200 以確認收到回調。若未收到 200 回應,KryptoGO 可能會重試發送。

```
HTTP/1.1 200 OK
```

## 安全建議

1. 驗證 `payment_intent_id` 存在於你的資料庫
2. 驗證 `client_id` 與你的 Client ID 一致
3. 實作冪等處理避免重複更新
4. 記錄所有回調資料供稽核使用
5. 端點應使用 HTTPS

```

### references/troubleshooting.md

```markdown
# KryptoGO Payment Webhook 疑難排解

## 常見問題

### 1. 未收到 Webhook 回調

**可能原因**:
- `callback_url` 未在建立 Payment Intent 時設定
- URL 非公開可存取(如 `localhost`)
- 防火牆阻擋了 KryptoGO 的請求
- URL 使用 HTTP 而非 HTTPS

**解決方式**:
1. 確認建立 Payment Intent 時有傳入 `callback_url`
2. 確認 URL 為公開可存取的 HTTPS URL
3. 開發時可使用 ngrok 等工具暴露 localhost:
   ```bash
   ngrok http 3001
   # 使用 ngrok 提供的 HTTPS URL 作為 callback_url
   ```
4. 檢查伺服器防火牆設定

### 2. 收到回調但處理失敗

**可能原因**:
- 未正確解析 JSON body
- `payment_intent_id` 在資料庫中找不到
- 資料庫更新錯誤

**解決方式**:
1. 確認使用 JSON body parser:
   ```javascript
   // Express
   app.use(express.json());
   ```
2. 記錄完整 payload 供除錯:
   ```javascript
   router.post('/callback', (req, res) => {
     console.log('Callback received:', JSON.stringify(req.body));
     // ...處理邏輯
   });
   ```

### 3. 重複收到相同回調

**原因**: 若你的端點未回應 200,KryptoGO 會重試

**解決方式**:
1. 確保端點回應 HTTP 200
2. 實作冪等性處理:
   ```javascript
   // 檢查是否已處理
   if (['paid', 'expired', 'refunded'].includes(order.status)) {
     return res.status(200).send(); // 跳過但仍回應 200
   }
   ```

### 4. 端點回應 timeout

**原因**: 回調處理時間過長

**解決方式**:
1. 先回應 200,再非同步處理:
   ```javascript
   router.post('/callback', (req, res) => {
     // 立即回應
     res.status(200).send();
     // 非同步處理
     processPayment(req.body).catch(console.error);
   });
   ```

### 5. 開發環境無法測試

**解決方式**:

使用 ngrok 暴露本地端點:
```bash
# 安裝 ngrok
npm install -g ngrok

# 暴露本地 3001 port
ngrok http 3001
```

將 ngrok URL 作為 `callback_url` 使用:
```javascript
openPaymentModal({
  fiat_amount: '10',
  fiat_currency: 'TWD',
  callback_url: 'https://abc123.ngrok.io/api/payment/callback',
});
```

## 除錯清單

- [ ] `callback_url` 已在建立 Payment Intent 時設定
- [ ] URL 為 HTTPS 且公開可存取
- [ ] 伺服器有 JSON body parser
- [ ] 端點回應 HTTP 200
- [ ] 有冪等性處理
- [ ] 有記錄完整 callback payload
- [ ] 防火牆未阻擋外部 POST 請求

```

kryptogo-pay-webhook | SkillHub