{
  "openapi": "3.1.0",
  "info": {
    "title": "Hensa API",
    "version": "2026-11-01",
    "description": "API của Hensa Connect: tạo mã VietQR động, nhận tiền về theo thời gian thực qua webhook và đối soát tự động. Mọi số tiền là số nguyên VND, thời gian theo ISO 8601."
  },
  "servers": [
    {
      "url": "https://api.hensa.example/v1"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "payment_requests",
      "x-displayName": "Yêu cầu thanh toán",
      "description": "Tạo mã VietQR động cho từng đơn. Khi khách chuyển đúng, Hensa tự khớp và gửi `payment_request.paid`."
    },
    {
      "name": "bank_transactions",
      "x-displayName": "Giao dịch ngân hàng",
      "description": "Mọi khoản tiền vào tài khoản đã liên kết, kèm trạng thái đối soát."
    },
    {
      "name": "webhook_endpoints",
      "x-displayName": "Quản lý webhook",
      "description": "Đăng ký URL nhận sự kiện, xoay secret, xem lịch sử gửi và gửi lại."
    },
    {
      "name": "test",
      "x-displayName": "Test mode",
      "description": "Chỉ dùng với khóa `hk_test_`. Mô phỏng tiền về để thử toàn bộ luồng mà không cần chuyển tiền thật."
    }
  ],
  "paths": {
    "/payment_requests": {
      "post": {
        "operationId": "createPaymentRequest",
        "tags": [
          "payment_requests"
        ],
        "summary": "Tạo yêu cầu thanh toán",
        "description": "Tạo mã VietQR động với số tiền và mã tham chiếu riêng (`referenceCode`). Hiển thị `qrPayload` thành mã QR cho khách quét – hoặc chuyển khách sang trang thanh toán Hensa ở `payUrl`. Mã hết hạn sau 15 phút.\n\n**Lỗi có thể gặp:** `validation_error` (400) · `idempotency_conflict` (409) · `no_active_bank_account` (422) · `merchant_name_required` (422) · `plan_limit` (422)",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Khóa chống trùng (UUID). Gửi lại cùng khóa + cùng nội dung trong 24 giờ sẽ nhận lại đúng kết quả cũ, không tạo bản ghi mới.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Yêu cầu thanh toán vừa tạo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequest"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` – Cùng `Idempotency-Key` nhưng nội dung khác.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`no_active_bank_account` – Cửa hàng chưa có tài khoản ngân hàng đang hoạt động.\n`merchant_name_required` – Cửa hàng chưa đặt tên – đặt tên trong Dashboard trước khi tạo QR (chỉ ở chế độ thật).\n`plan_limit` – Vượt giới hạn của gói hiện tại.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "minimum": 1000,
                    "maximum": 9007199254740991,
                    "description": "Số tiền cần thu, số nguyên VND, tối thiểu 1.000.",
                    "examples": [
                      150000
                    ]
                  },
                  "externalOrderRef": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Mã đơn trong hệ thống của bạn (tối đa 64 ký tự). Trả lại nguyên vẹn trong webhook.",
                    "examples": [
                      "DH-1024"
                    ]
                  }
                },
                "required": [
                  "amount"
                ]
              },
              "example": {
                "amount": 150000,
                "externalOrderRef": "DH-1024"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPaymentRequests",
        "tags": [
          "payment_requests"
        ],
        "summary": "Liệt kê yêu cầu thanh toán",
        "description": "Mới nhất trước. Phân trang bằng con trỏ.\n\n**Lỗi có thể gặp:** `validation_error` (400)",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Lọc theo trạng thái.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "paid",
                "partially_paid",
                "expired",
                "cancelled"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Số bản ghi mỗi trang (1–100, mặc định 25).",
            "schema": {
              "default": 25,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Giá trị `nextCursor` của trang trước.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Một trang kết quả.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PaymentRequest"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/payment_requests/{id}": {
      "get": {
        "operationId": "getPaymentRequest",
        "tags": [
          "payment_requests"
        ],
        "summary": "Lấy một yêu cầu thanh toán",
        "description": "Dùng để kiểm tra lại trạng thái khi chưa nhận được webhook.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID yêu cầu thanh toán.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^prq_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Yêu cầu thanh toán.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequest"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bank_transactions": {
      "get": {
        "operationId": "listBankTransactions",
        "tags": [
          "bank_transactions"
        ],
        "summary": "Liệt kê giao dịch",
        "description": "Mới nhất trước. Dùng định kỳ (vd. mỗi 15 phút) làm lưới an toàn bên cạnh webhook.\n\n**Lỗi có thể gặp:** `validation_error` (400)",
        "parameters": [
          {
            "name": "reconciliation",
            "in": "query",
            "required": false,
            "description": "Lọc theo trạng thái đối soát.",
            "schema": {
              "type": "string",
              "enum": [
                "matched",
                "pending",
                "unmatched"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Số bản ghi mỗi trang (1–100, mặc định 25).",
            "schema": {
              "default": 25,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Giá trị `nextCursor` của trang trước.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Một trang kết quả.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BankTransaction"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bank_transactions/{id}": {
      "get": {
        "operationId": "getBankTransaction",
        "tags": [
          "bank_transactions"
        ],
        "summary": "Lấy một giao dịch",
        "description": "Chi tiết một khoản tiền vào.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID giao dịch.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^btx_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Giao dịch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankTransaction"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bank_transactions/{id}/reconcile": {
      "post": {
        "operationId": "reconcileBankTransaction",
        "tags": [
          "bank_transactions"
        ],
        "summary": "Khớp giao dịch thủ công",
        "description": "Khi khách ghi sai nội dung chuyển khoản: gắn giao dịch với một yêu cầu thanh toán, một mã đơn của bạn, hoặc đánh dấu \"không phải đơn\".\n\n**Lỗi có thể gặp:** `not_found` (404) · `already_reconciled` (409) · `validation_error` (400)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID giao dịch.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^btx_.*"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Khóa chống trùng (UUID). Gửi lại cùng khóa + cùng nội dung trong 24 giờ sẽ nhận lại đúng kết quả cũ, không tạo bản ghi mới.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Giao dịch sau khi khớp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankTransaction"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`already_reconciled` – Giao dịch đã được khớp trước đó.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "paymentRequestId": {
                        "type": "string",
                        "format": "starts_with",
                        "pattern": "^prq_.*"
                      }
                    },
                    "required": [
                      "paymentRequestId"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "orderRef": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 64
                      }
                    },
                    "required": [
                      "orderRef"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "notAnOrder": {
                        "type": "boolean",
                        "const": true
                      }
                    },
                    "required": [
                      "notAnOrder"
                    ]
                  }
                ]
              },
              "example": {
                "paymentRequestId": "prq_01JD7Q8M4K2X9V3T6B5N0R1W8Y"
              }
            }
          }
        }
      }
    },
    "/webhook_endpoints": {
      "get": {
        "operationId": "listWebhookEndpoints",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Liệt kê endpoint",
        "description": "Mọi URL đang nhận sự kiện.",
        "parameters": [
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Danh sách endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEndpoint"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhookEndpoint",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Tạo endpoint",
        "description": "URL phải là HTTPS công khai (không chấp nhận địa chỉ nội bộ). Secret ký (`whsec_…`) **chỉ trả về một lần** – lưu ngay.\n\n**Lỗi có thể gặp:** `validation_error` (400) · `url_not_https` (422) · `url_private_address` (422)",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Khóa chống trùng (UUID). Gửi lại cùng khóa + cùng nội dung trong 24 giờ sẽ nhận lại đúng kết quả cũ, không tạo bản ghi mới.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Endpoint và secret ký.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoint": {
                      "$ref": "#/components/schemas/WebhookEndpoint"
                    },
                    "secret": {
                      "type": "string",
                      "description": "whsec_… – shown once"
                    }
                  },
                  "required": [
                    "endpoint",
                    "secret"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`url_not_https` – URL webhook phải dùng HTTPS.\n`url_private_address` – URL trỏ tới địa chỉ nội bộ – không được phép.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^https:\\/\\/.*",
                    "description": "URL HTTPS công khai sẽ nhận sự kiện.",
                    "examples": [
                      "https://cuahang.vn/webhooks/hensa"
                    ]
                  },
                  "events": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "bank_transaction.created",
                        "payment_request.paid",
                        "payment_request.partially_paid",
                        "payment_request.expired",
                        "bank_connection.status_changed"
                      ]
                    },
                    "description": "Danh sách sự kiện muốn nhận (ít nhất một).",
                    "examples": [
                      [
                        "payment_request.paid"
                      ]
                    ]
                  }
                },
                "required": [
                  "url",
                  "events"
                ]
              },
              "example": {
                "url": "https://cuahang.vn/webhooks/hensa",
                "events": [
                  "payment_request.paid",
                  "bank_transaction.created"
                ]
              }
            }
          }
        }
      }
    },
    "/webhook_endpoints/{id}": {
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Xóa endpoint",
        "description": "Ngừng gửi sự kiện tới URL này ngay lập tức.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID endpoint.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^whe_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Đã xóa."
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhook_endpoints/{id}/rotate_secret": {
      "post": {
        "operationId": "rotateWebhookSecret",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Đổi secret ký",
        "description": "Tạo secret mới. Secret cũ còn hiệu lực thêm 24 giờ – trong thời gian đó mỗi sự kiện mang hai chữ ký `v1` để bạn chuyển đổi không gián đoạn.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID endpoint.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^whe_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoint và secret mới.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endpoint": {
                      "$ref": "#/components/schemas/WebhookEndpoint"
                    },
                    "secret": {
                      "type": "string",
                      "description": "whsec_… – shown once"
                    }
                  },
                  "required": [
                    "endpoint",
                    "secret"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhook_endpoints/{id}/test": {
      "post": {
        "operationId": "sendTestWebhook",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Gửi sự kiện thử",
        "description": "Gửi ngay một sự kiện mẫu (đã ký) tới endpoint và trả về kết quả.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID endpoint.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^whe_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Kết quả lần gửi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhook_endpoints/{id}/deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Lịch sử gửi",
        "description": "Mọi lần gửi trong 30 ngày, kể cả lần thất bại.\n\n**Lỗi có thể gặp:** `not_found` (404)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID endpoint.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^whe_.*"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Số bản ghi mỗi trang (1–100, mặc định 25).",
            "schema": {
              "default": 25,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Giá trị `nextCursor` của trang trước.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Một trang kết quả.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhook_deliveries/{id}/redeliver": {
      "post": {
        "operationId": "redeliverWebhook",
        "tags": [
          "webhook_endpoints"
        ],
        "summary": "Gửi lại",
        "description": "Gửi lại một sự kiện (trong 30 ngày). Cùng `id` sự kiện – hệ thống của bạn phải xử lý trùng.\n\n**Lỗi có thể gặp:** `not_found` (404) · `delivery_too_old` (422)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "ID lần gửi.",
            "schema": {
              "type": "string",
              "format": "starts_with",
              "pattern": "^whd_.*"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Kết quả lần gửi lại.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` – Không tìm thấy (hoặc không thuộc cửa hàng của khóa này).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`delivery_too_old` – Chỉ gửi lại được sự kiện trong 30 ngày.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/test/bank_transfers": {
      "post": {
        "operationId": "simulateBankTransfer",
        "tags": [
          "test"
        ],
        "summary": "Mô phỏng tiền về",
        "description": "**Chỉ với khóa `hk_test_`.** Tạo một giao dịch tiền về giả và phát sự kiện thật như production. Ghi `referenceCode` của một yêu cầu thanh toán vào `description` để thử luồng tự khớp.\n\n**Lỗi có thể gặp:** `test_mode_only` (403) · `validation_error` (400)",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Khóa chống trùng (UUID). Gửi lại cùng khóa + cùng nội dung trong 24 giờ sẽ nhận lại đúng kết quả cũ, không tạo bản ghi mới.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Hensa-Version",
            "in": "header",
            "required": false,
            "description": "Phiên bản API (định dạng ngày). Bỏ trống = phiên bản đã ghim cho khóa.",
            "schema": {
              "type": "string",
              "example": "2026-11-01"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Giao dịch giả vừa tạo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BankTransaction"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error` – Dữ liệu gửi lên không hợp lệ – xem `message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`test_mode_only` – Chỉ dùng được với khóa `hk_test_`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "minimum": 1000,
                    "maximum": 9007199254740991,
                    "description": "Số tiền giả lập, số nguyên VND, tối thiểu 1.000.",
                    "examples": [
                      150000
                    ]
                  },
                  "description": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 140,
                    "description": "Nội dung chuyển khoản như khách gõ. Ghi `referenceCode` để thử tự khớp.",
                    "examples": [
                      "LK7F3K9 thanh toan don hang"
                    ]
                  },
                  "counterpartyName": {
                    "type": "string",
                    "maxLength": 70,
                    "description": "Tên người chuyển (tùy chọn).",
                    "examples": [
                      "NGUYEN VAN AN"
                    ]
                  }
                },
                "required": [
                  "amount",
                  "description"
                ]
              },
              "example": {
                "amount": 150000,
                "description": "LK7F3K9 thanh toan don hang",
                "counterpartyName": "NGUYEN VAN AN"
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "bank_transaction.created": {
      "post": {
        "summary": "bank_transaction.created",
        "description": "Có tiền vào tài khoản đã liên kết. Ký bằng header `Hensa-Signature`. Trả mã 2xx trong 10 giây.",
        "parameters": [
          {
            "name": "Hensa-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1762135200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^evt_.*"
                  },
                  "type": {
                    "type": "string",
                    "const": "bank_transaction.created"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  },
                  "data": {
                    "$ref": "#/components/schemas/BankTransaction"
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2xx"
          }
        }
      }
    },
    "payment_request.paid": {
      "post": {
        "summary": "payment_request.paid",
        "description": "Yêu cầu thanh toán đã được trả đủ. Ký bằng header `Hensa-Signature`. Trả mã 2xx trong 10 giây.",
        "parameters": [
          {
            "name": "Hensa-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1762135200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^evt_.*"
                  },
                  "type": {
                    "type": "string",
                    "const": "payment_request.paid"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  },
                  "data": {
                    "$ref": "#/components/schemas/PaymentRequest"
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2xx"
          }
        }
      }
    },
    "payment_request.partially_paid": {
      "post": {
        "summary": "payment_request.partially_paid",
        "description": "undefined Ký bằng header `Hensa-Signature`. Trả mã 2xx trong 10 giây.",
        "parameters": [
          {
            "name": "Hensa-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1762135200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^evt_.*"
                  },
                  "type": {
                    "type": "string",
                    "const": "payment_request.partially_paid"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  },
                  "data": {
                    "$ref": "#/components/schemas/PaymentRequest"
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2xx"
          }
        }
      }
    },
    "payment_request.expired": {
      "post": {
        "summary": "payment_request.expired",
        "description": "Yêu cầu thanh toán hết hạn mà chưa được trả. Ký bằng header `Hensa-Signature`. Trả mã 2xx trong 10 giây.",
        "parameters": [
          {
            "name": "Hensa-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1762135200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^evt_.*"
                  },
                  "type": {
                    "type": "string",
                    "const": "payment_request.expired"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  },
                  "data": {
                    "$ref": "#/components/schemas/PaymentRequest"
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2xx"
          }
        }
      }
    },
    "bank_connection.status_changed": {
      "post": {
        "summary": "bank_connection.status_changed",
        "description": "Kết nối tài khoản ngân hàng thay đổi trạng thái (lỗi, khôi phục…). Ký bằng header `Hensa-Signature`. Trả mã 2xx trong 10 giây.",
        "parameters": [
          {
            "name": "Hensa-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1762135200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "starts_with",
                    "pattern": "^evt_.*"
                  },
                  "type": {
                    "type": "string",
                    "const": "bank_connection.status_changed"
                  },
                  "createdAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "bankAccountId": {
                        "type": "string",
                        "format": "starts_with",
                        "pattern": "^bac_.*"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "active",
                          "error",
                          "disabled"
                        ]
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    },
                    "required": [
                      "bankAccountId",
                      "status",
                      "reason"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "id",
                  "type",
                  "createdAt",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2xx"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Khóa API dạng `hk_test_…` (thử nghiệm) hoặc `hk_live_…` (thật), gửi trong header `Authorization: Bearer <khóa>`."
      }
    },
    "schemas": {
      "PaymentRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^prq_.*",
            "description": "ID yêu cầu thanh toán.",
            "examples": [
              "prq_01JD7Q8M4K2X9V3T6B5N0R1W8Y"
            ]
          },
          "amount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Số tiền cần thu, số nguyên VND.",
            "examples": [
              150000
            ]
          },
          "amountReceived": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Tổng tiền đã nhận được khớp với yêu cầu này. Nhỏ hơn `amount` khi `partially_paid`; có thể lớn hơn nếu khách chuyển thừa.",
            "examples": [
              0
            ]
          },
          "currency": {
            "type": "string",
            "const": "VND",
            "description": "Luôn là `VND`.",
            "examples": [
              "VND"
            ]
          },
          "referenceCode": {
            "type": "string",
            "description": "Mã tham chiếu khách ghi vào nội dung chuyển khoản – dùng để tự khớp.",
            "examples": [
              "LK7F3K9"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "partially_paid",
              "expired",
              "cancelled"
            ],
            "description": "`pending` chờ trả · `paid` đã trả đủ · `partially_paid` trả thiếu · `expired` hết hạn · `cancelled` đã hủy.",
            "examples": [
              "pending"
            ]
          },
          "qrPayload": {
            "type": "string",
            "description": "Chuỗi VietQR (EMVCo) – vẽ thành mã QR cho khách quét.",
            "examples": [
              "00020101021238…"
            ]
          },
          "receiver": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "bankName": {
                "type": "string"
              },
              "accountMasked": {
                "type": "string"
              }
            },
            "required": [
              "name",
              "bankName",
              "accountMasked"
            ],
            "additionalProperties": false,
            "description": "Người nhận hiển thị cho khách kiểm tra trước khi chuyển.",
            "examples": [
              {
                "name": "CA PHE MOC",
                "bankName": "MB Bank",
                "accountMasked": "•••• 6789"
              }
            ]
          },
          "externalOrderRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mã đơn trong hệ thống của bạn (nếu gửi khi tạo).",
            "examples": [
              "DH-1024"
            ]
          },
          "payUrl": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "null"
              }
            ],
            "description": "Trang thanh toán Hensa cho yêu cầu này (QR, nút mở app ngân hàng, biên nhận). Gửi cho người mua hoặc chuyển hướng họ tới đây thay vì tự hiển thị QR. URL chứa mã bí mật – chỉ gửi cho người trả tiền. `null` nếu trang đã bị thu hồi.",
            "examples": [
              "https://pay.hensa.vn/r/pt_7Hq2mX9vKd4RtY1nB8cZ0w"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Hết hạn sau 15 phút kể từ khi tạo.",
            "examples": [
              "2026-11-03T08:15:00Z"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Thời điểm tạo.",
            "examples": [
              "2026-11-03T08:00:00Z"
            ]
          }
        },
        "required": [
          "id",
          "amount",
          "amountReceived",
          "currency",
          "referenceCode",
          "status",
          "qrPayload",
          "receiver",
          "externalOrderRef",
          "payUrl",
          "expiresAt",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "BankTransaction": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^btx_.*",
            "description": "ID giao dịch.",
            "examples": [
              "btx_01JD7QA9F3W5Y7Z1B3D5G7J9KM"
            ]
          },
          "bankAccountId": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^bac_.*",
            "description": "Tài khoản ngân hàng nhận tiền.",
            "examples": [
              "bac_01JD5A2B4C6D8E0F2G4H6J8K0M"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ],
            "description": "`in` tiền vào · `out` tiền ra.",
            "examples": [
              "in"
            ]
          },
          "amount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Số tiền, số nguyên VND.",
            "examples": [
              150000
            ]
          },
          "currency": {
            "type": "string",
            "const": "VND",
            "description": "Luôn là `VND`.",
            "examples": [
              "VND"
            ]
          },
          "description": {
            "type": "string",
            "description": "Nội dung chuyển khoản đúng như ngân hàng gửi.",
            "examples": [
              "LK7F3K9 thanh toan don hang"
            ]
          },
          "referenceCode": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mã tham chiếu Hensa tìm thấy trong nội dung (nếu có).",
            "examples": [
              "LK7F3K9"
            ]
          },
          "counterpartyName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tên người chuyển (nếu ngân hàng cung cấp).",
            "examples": [
              "NGUYEN VAN AN"
            ]
          },
          "counterpartyBank": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ngân hàng người chuyển (nếu có).",
            "examples": [
              "VCB"
            ]
          },
          "channel": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "counter",
                  "website",
                  "facebook",
                  "zalo",
                  "delivery"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Kênh bán gắn với giao dịch (nếu biết).",
            "examples": [
              "website"
            ]
          },
          "reconciliation": {
            "type": "string",
            "enum": [
              "matched",
              "pending",
              "unmatched"
            ],
            "description": "`matched` đã khớp · `pending` cần xử lý · `unmatched` không phải đơn.",
            "examples": [
              "matched"
            ]
          },
          "matchedOrderRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Mã đơn đã khớp (nếu có).",
            "examples": [
              "DH-1024"
            ]
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Thời điểm tiền vào tài khoản theo ngân hàng.",
            "examples": [
              "2026-11-03T08:03:10Z"
            ]
          }
        },
        "required": [
          "id",
          "bankAccountId",
          "direction",
          "amount",
          "currency",
          "description",
          "referenceCode",
          "counterpartyName",
          "counterpartyBank",
          "channel",
          "reconciliation",
          "matchedOrderRef",
          "occurredAt"
        ],
        "additionalProperties": false
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^whe_.*",
            "description": "ID endpoint.",
            "examples": [
              "whe_01JD7R1S3T5V7X9Z1B3D5F7H9K"
            ]
          },
          "url": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^https:\\/\\/.*",
            "description": "URL HTTPS công khai nhận sự kiện.",
            "examples": [
              "https://cuahang.vn/webhooks/hensa"
            ]
          },
          "events": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "bank_transaction.created",
                "payment_request.paid",
                "payment_request.partially_paid",
                "payment_request.expired",
                "bank_connection.status_changed"
              ]
            },
            "description": "Các sự kiện đăng ký.",
            "examples": [
              [
                "payment_request.paid",
                "bank_transaction.created"
              ]
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "failing",
              "disabled"
            ],
            "description": "`active` · `failing` (đang lỗi, đã gửi email cảnh báo) · `disabled`.",
            "examples": [
              "active"
            ]
          },
          "secretLast4": {
            "type": "string",
            "description": "4 ký tự cuối của secret ký – để đối chiếu.",
            "examples": [
              "9f2a"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Thời điểm tạo.",
            "examples": [
              "2026-11-03T07:00:00Z"
            ]
          }
        },
        "required": [
          "id",
          "url",
          "events",
          "status",
          "secretLast4",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^whd_.*",
            "description": "ID lần gửi.",
            "examples": [
              "whd_01JD7QC4V6X8Z0B2D4F6H8K0MP"
            ]
          },
          "endpointId": {
            "type": "string",
            "format": "starts_with",
            "pattern": "^whe_.*",
            "description": "Endpoint nhận.",
            "examples": [
              "whe_01JD7R1S3T5V7X9Z1B3D5F7H9K"
            ]
          },
          "eventType": {
            "type": "string",
            "enum": [
              "bank_transaction.created",
              "payment_request.paid",
              "payment_request.partially_paid",
              "payment_request.expired",
              "bank_connection.status_changed"
            ],
            "description": "Loại sự kiện.",
            "examples": [
              "payment_request.paid"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "succeeded",
              "failed",
              "retrying"
            ],
            "description": "`succeeded` · `failed` · `retrying`.",
            "examples": [
              "succeeded"
            ]
          },
          "httpStatus": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "Mã HTTP máy chủ của bạn trả về.",
            "examples": [
              200
            ]
          },
          "durationMs": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "Thời gian phản hồi (ms).",
            "examples": [
              142
            ]
          },
          "attempt": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Lần gửi thứ mấy (1–8).",
            "examples": [
              1
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
            "description": "Thời điểm gửi.",
            "examples": [
              "2026-11-03T08:03:12Z"
            ]
          }
        },
        "required": [
          "id",
          "endpointId",
          "eventType",
          "status",
          "httpStatus",
          "durationMs",
          "attempt",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message",
              "request_id"
            ],
            "additionalProperties": false,
            "description": "`code` ổn định để lập trình · `message` cho người đọc · `request_id` gửi kèm khi cần hỗ trợ.",
            "examples": [
              {
                "code": "no_active_bank_account",
                "message": "Link a bank account before creating payment requests.",
                "request_id": "req_01JD7QF0X2C4M8R6T1Y3W5Z7NB"
              }
            ]
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false
      }
    }
  }
}
