{
  "openapi": "3.1.0",
  "info": {
    "title": "VKL Shield API — v1",
    "version": "1.0.0",
    "description": "v1 là API shield xác thực bằng key, ký bằng Ed25519. Mọi request (trừ JWKS) đều mang `?shield_key=` (định dạng `shpk_live_…` / `shpk_test_…`). Response capture/complete là các envelope đã ký có thể xác minh dựa trên JWKS bên dưới — xem `/v1/keys`. Các route chỉ dành cho wallet trong API cũ không còn trong v1; dùng trực tiếp route PayPal/Stripe.\n"
  },
  "servers": [
    {
      "url": "https://live.vkl-shield.com",
      "description": "shield host live mặc định (production). Một đối tác có thể được cấp một shield host riêng — truyền nó làm `selectBase` của SDK (hoặc load script SDK từ host đó) thay cho host mặc định này."
    },
    {
      "url": "https://sandbox.vkl-shield.com",
      "description": "shield host sandbox mặc định (testing). Áp dụng cùng cơ chế ghi đè host riêng như trên host live mặc định."
    },
    {
      "url": "https://keys.vkl-shield.com",
      "description": "host JWKS chuẩn (live); sandbox là https://keys.vkl-shield.com/sandbox — luôn dùng host này cho JWKS bất kể request được phục vụ bởi shield host nào (mặc định hoặc riêng)"
    }
  ],
  "tags": [
    {
      "name": "paypal",
      "description": "Checkout smart-buttons PayPal (render trang, tạo/capture order, chọn pool)"
    },
    {
      "name": "stripe",
      "description": "Stripe Express Checkout (wallet) + checkout dạng card-form (select, create-intent, complete)"
    },
    {
      "name": "keys",
      "description": "JWKS Ed25519 để xác minh các envelope đã ký"
    }
  ],
  "paths": {
    "/v1/": {
      "get": {
        "operationId": "v1PaypalButtonsPage",
        "summary": "Trang iframe smart-buttons PayPal",
        "description": "Render trang HTML mà iframe do SDK quản lý sẽ load (`src` của iframe trong `PaypalShield.init`). Kiểm tra origin qua `Referer` khi key có cấu hình `allowedOrigins` (chống hotlink, không phải ranh giới xác thực). Bị chặn bởi pool guard (POOL_MISMATCH nếu pool của key không bao gồm host này).\n",
        "tags": [
          "paypal"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "slug định tuyến pool tùy chọn (hỗ trợ nhiều slug phân tách bằng dấu |)"
          }
        ],
        "responses": {
          "200": {
            "description": "trang HTML",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ShieldKeyError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "500": {
            "description": "Lỗi server nội bộ — `{error:string}` trần, không có `data`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyStringError"
                }
              }
            }
          },
          "503": {
            "description": "SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED | PAYMENT_ENV_MISMATCH | SIGNING_UNAVAILABLE (hình dạng `V1Error`) HOẶC shield chưa được cấu hình secret (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/card-fields": {
      "get": {
        "operationId": "v1PaypalCardFieldsPage",
        "summary": "Trang iframe card-fields nâng cao của PayPal",
        "description": "Chặn giống `/v1/`; render trang biến thể card-fields.",
        "tags": [
          "paypal"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "responses": {
          "200": {
            "description": "trang HTML",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ShieldKeyError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "500": {
            "description": "Lỗi server nội bộ — `{error:string}` trần, không có `data`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyStringError"
                }
              }
            }
          },
          "503": {
            "description": "SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED | PAYMENT_ENV_MISMATCH | SIGNING_UNAVAILABLE (hình dạng `V1Error`) HOẶC shield chưa được cấu hình (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/create_order": {
      "post": {
        "operationId": "v1PaypalCreateOrder",
        "summary": "Tạo một đơn hàng PayPal",
        "description": "Kiểm tra origin qua `sec-fetch-or-origin` khi key có cấu hình `allowedOrigins`. Được gọi từ bên trong iframe của SDK (cùng origin với shield domain đã chọn).\n",
        "tags": [
          "paypal"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "data"
                ],
                "properties": {
                  "data": {
                    "type": "object",
                    "description": "body đơn hàng dạng PayPal (purchase_units[0].amount/invoice_id/reference_id, …)"
                  },
                  "store_domain": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "fraudnet_session": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "đơn hàng đã được tạo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SHIPPING_ADDRESS | INVALID_EMAIL | INVALID_ORDER",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyControllerError"
                }
              }
            }
          },
          "401": {
            "description": "lỗi xác thực key (V1Error) HOẶC tổng đơn hàng vượt MAX_ORDER_AMOUNT (body rỗng, `res.status(401).send()` — cùng mức giới hạn mà PayPal/Stripe chia sẻ; phân biệt bằng body RỖNG so với body JSON V1Error).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "429": {
            "description": "RATE_LIMITED | TEMPORARILY_LOCKED (giới hạn tốc độ thanh toán theo IP dùng chung)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyControllerError"
                }
              }
            }
          },
          "500": {
            "description": "Chuyển tiếp lỗi API PayPal nguyên bản — `data` là JSON lỗi gốc đã parse của PayPal, không phải hình dạng `{issue,...}` — HOẶC \"Internal server error\" (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "data": {
                          "type": "object"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED | PAYMENT_ENV_MISMATCH | SIGNING_UNAVAILABLE (hình dạng `V1Error`) HOẶC shield chưa được cấu hình (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/capture_order": {
      "post": {
        "operationId": "v1PaypalCaptureOrder",
        "summary": "Capture một đơn hàng PayPal và nhận envelope đã ký",
        "description": "Không kiểm tra origin (ràng buộc thực hiện lúc tạo order thông qua ràng buộc capture-key hậu-charge). Khi thành công trả về một `SignedEnvelope` mà `data` mang kết quả capture gốc của PayPal và `signed_payload` parse ra `PaypalCaptureV1Payload`. Bằng chứng đã ký là đường hoàn tất CHÍNH (store hoàn tất ngay, không chờ webhook). Một capture THÀNH CÔNG (tiền đã chuyển) nhưng không thể ký trả về 502 `CAPTURED_PROOF_UNAVAILABLE`: người mua đã bị charge; không retry hay hiển thị lỗi — đơn hàng được hoàn tất bởi webhook AIO `transaction.completed` (khớp theo `settle_ref`) như đường fallback.\n",
        "tags": [
          "paypal"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "capture_id"
                ],
                "properties": {
                  "capture_id": {
                    "type": "string",
                    "description": "id đơn hàng PayPal trả về từ create_order"
                  },
                  "order_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "id đơn hàng của store"
                  },
                  "store_domain": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "fraudnet_session": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "client_static": {
                    "type": [
                      "object",
                      "null"
                    ]
                  },
                  "payment_source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "paypal | card | googlepay | applepay | venmo | paylater | credit"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "đã capture — envelope đã ký",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SignedEnvelope"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "capture_id/order_id thiếu hoặc không an toàn — `data` ở đây là một STRING thuần (ví dụ \"Capture ID is required\"), không phải object `{issue,...}` (validate trước khi gọi PayPal trên route này).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "data"
                  ],
                  "properties": {
                    "error": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "lỗi xác thực key (V1Error) HOẶC gate fraud blocklist đã từ chối capture này (body rỗng, `res.status(401).send()` — không lộ lý do cho client; phân biệt bằng body RỖNG so với JSON V1Error).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "403": {
            "description": "SCOPE_DENIED | POOL_MISMATCH (cố gắng capture bằng key khác). LƯU Ý: `/v1/capture_order` không kiểm tra origin — ràng buộc origin được thực hiện lúc tạo order — nên ORIGIN_NOT_ALLOWED không bao giờ xảy ra ở route này dù nó có trong danh sách issue chung của `ForbiddenError`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED | TEMPORARILY_LOCKED (giới hạn tốc độ thanh toán theo IP dùng chung)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyControllerError"
                }
              }
            }
          },
          "500": {
            "description": "Chuyển tiếp lỗi API PayPal nguyên bản — `data` là JSON lỗi gốc đã parse của PayPal, không phải hình dạng `{issue,...}` — HOẶC \"Internal server error\" (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "data": {
                          "type": "object"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          },
          "502": {
            "description": "CAPTURED_PROOF_UNAVAILABLE — người mua đã bị charge; không retry hay hiển thị lỗi — đơn hàng được hoàn tất bởi webhook AIO `transaction.completed` (khớp theo `settle_ref`) như đường fallback.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "503": {
            "description": "SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED | PAYMENT_ENV_MISMATCH | SIGNING_UNAVAILABLE (hình dạng `V1Error`) HOẶC shield chưa được cấu hình (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/shield/select": {
      "get": {
        "operationId": "v1PaypalShieldSelect",
        "summary": "Chọn một shield PayPal từ pool của key",
        "description": "Được gọi bởi `PaypalShield.init` trước khi mount iframe (bỏ qua khi script SDK mang pin `?domain=`). Kiểm tra origin qua `Origin` khi key có cấu hình `allowedOrigins`.\n",
        "tags": [
          "paypal"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "kết quả chọn pool khi thành công. Với NO_ACTIVE/NO_POOL/SELECT_FAILED (nhánh JSON pool-select chung không set status, nên Express mặc định về 200) đây thay vào đó là một `LegacyControllerError` — `error:true` với `data.issue` là một trong ba mã đó. SELECT_FAILED là một quy ước phía SDK (selector chung không bao giờ phát ra chuỗi đó; nó được dành riêng cho việc phân loại network/timeout của SDK trên lời gọi này), được giữ trong enum `V1Issue` chung cho các caller muốn một tập hợp đóng duy nhất trên cả hai lớp.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "boolean",
                          "enum": [
                            false
                          ]
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "shield_domain": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LegacyControllerError"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ShieldKeyError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "503": {
            "$ref": "#/components/responses/RegistryUnavailableError"
          }
        }
      }
    },
    "/v1/stripe/select": {
      "get": {
        "operationId": "v1StripeSelect",
        "summary": "Chọn một shield Stripe + publishable key từ pool của key",
        "description": "Được gọi bởi `StripeShield.init` trước khi mount wallet/card form. Kiểm tra origin qua `Origin` khi key có cấu hình `allowedOrigins`.\n",
        "tags": [
          "stripe"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "kết quả chọn pool khi thành công. Với NO_ACTIVE/NO_POOL/SELECT_FAILED (nhánh JSON pool-select chung không set status, nên Express mặc định về 200) đây thay vào đó là một `LegacyControllerError` — `error:true` với `data.issue` là một trong ba mã đó. SELECT_FAILED là một quy ước phía SDK, không do selector chung phát ra (xem ghi chú tương tự của `/v1/shield/select`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "boolean",
                          "enum": [
                            false
                          ]
                        },
                        "data": {
                          "type": "object",
                          "properties": {
                            "shield_domain": {
                              "type": "string"
                            },
                            "publishable_key": {
                              "type": "string"
                            },
                            "card_enabled": {
                              "type": "boolean"
                            },
                            "form_methods": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LegacyControllerError"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ShieldKeyError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundError"
          },
          "503": {
            "$ref": "#/components/responses/RegistryUnavailableError"
          }
        }
      }
    },
    "/v1/stripe/create-intent": {
      "post": {
        "operationId": "v1StripeCreateIntent",
        "summary": "Tạo một PaymentIntent Stripe (deferred-intent cho wallet hoặc card-form)",
        "description": "Kiểm tra origin qua `Origin` khi key có cấu hình `allowedOrigins`. `publishable_key` (từ `/v1/stripe/select`) định tuyến request tới đúng tài khoản Stripe và phải thuộc cùng pool với `shield_key` được trình (nếu không sẽ trả 403 `PK_POOL_MISMATCH`).\n",
        "tags": [
          "stripe"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "order_data",
                  "publishable_key"
                ],
                "properties": {
                  "order_data": {
                    "type": "object",
                    "description": "body đơn hàng dạng PayPal"
                  },
                  "contact": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "billing_details": {
                        "type": [
                          "object",
                          "null"
                        ]
                      },
                      "shipping_address": {
                        "type": [
                          "object",
                          "null"
                        ]
                      }
                    }
                  },
                  "wallet_type": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "channel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "card_form",
                      null
                    ]
                  },
                  "client_form_methods": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    }
                  },
                  "publishable_key": {
                    "type": "string"
                  },
                  "store_domain": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "store_order_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "intent đã được tạo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "client_secret": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ORDER | INVALID_EMAIL | INVALID_PHONE | INVALID_SHIPPING_ADDRESS | NO_METHOD_FOR_AMOUNT (hình dạng `LegacyControllerError`) HOẶC MISSING_PK | UNKNOWN_PK khi `publishable_key` không xác định được (hình dạng `LegacyGateError` — `{error:\"Why you do that?\"}` trần, không có `data`/`issue` nào cả).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/LegacyControllerError"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyGateError"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "lỗi xác thực key (V1Error) HOẶC tổng đơn hàng vượt MAX_ORDER_AMOUNT (body rỗng).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED (hình dạng `V1Error`) | PK_POOL_MISMATCH khi `publishable_key` không cùng pool với `shield_key` (hình dạng `LegacyGateError` — `{error:\"Why you do that?\"}` trần).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyGateError"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "CHỈ LEGACY — shield_key thiếu/không hợp lệ, body rỗng. KHÔNG thể xảy ra trên v1 (xem ghi chú của NotFoundError); được ghi lại để tương thích với API cũ.\n"
          },
          "429": {
            "description": "RATE_LIMITED | TEMPORARILY_LOCKED (giới hạn tốc độ thanh toán theo IP dùng chung, hoặc giới hạn riêng cho card-form theo IP mỗi giờ) — cùng hình dạng LegacyControllerError",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyControllerError"
                }
              }
            }
          },
          "500": {
            "description": "Chuyển tiếp lỗi API Stripe nguyên bản — `issue` = `error.code` của Stripe khi Stripe cung cấp, nếu không là chuỗi literal `STRIPE_ERROR`; KHÔNG phải enum đóng cho response này (xem `StripeErrorPassthrough`) — HOẶC \"Shield misconfigured\" (lệch chế độ key pk/sk của Stripe) | \"Internal server error\" (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/StripeErrorPassthrough"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "PAYMENT_ENV_MISMATCH | SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/stripe/complete": {
      "post": {
        "operationId": "v1StripeComplete",
        "summary": "Xác nhận một PaymentIntent đã thành công và nhận envelope đã ký",
        "description": "Không kiểm tra origin (ràng buộc qua metadata lúc create-time của chính PaymentIntent thay vào đó). Bằng chứng đã ký là đường hoàn tất CHÍNH (store hoàn tất ngay, không chờ webhook). Chính sách retry (SDK): lỗi network và 502/503/504 với issue ∈ {SIGNING_UNAVAILABLE, SHIELD_REGISTRY_UNAVAILABLE, SHIELD_REGISTRY_STALE} (hoặc không có issue) được retry tối đa 3 lần với backoff 1s/3s/9s — mọi mã khác là cuối cùng (terminal). Khi hết lượt retry hoặc một lỗi cuối cùng xảy ra sau một lần charge đã xác nhận, SDK hiển thị `charged_but_unconfirmed`: người mua đã bị charge; không retry hay hiển thị lỗi — đơn hàng được hoàn tất bởi webhook AIO `transaction.completed` (khớp theo `settle_ref`) như đường fallback. Một key đã hết hạn/bị thu hồi giữa create-intent và complete vẫn được ủy quyền nếu nó đang hoạt động lúc create-time (grace hậu-charge). Khi thành công trả về một `SignedEnvelope` mà `signed_payload` parse ra `StripeCompleteV1Payload`, cùng với một field `contact` không được ký.\n",
        "tags": [
          "stripe"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "payment_intent_id",
                  "publishable_key"
                ],
                "properties": {
                  "payment_intent_id": {
                    "type": "string"
                  },
                  "wallet_type": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "publishable_key": {
                    "type": "string"
                  },
                  "store_domain": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "store_order_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "client_static": {
                    "type": [
                      "object",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "đã hoàn tất — envelope đã ký",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SignedEnvelope"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`payment_intent_id` thiếu hoặc không phải id `pi_…` an toàn — `data` ở đây là một STRING thuần (ví dụ \"Invalid payment_intent_id\"), không phải object `{issue,...}` — HOẶC MISSING_PK | UNKNOWN_PK khi `publishable_key` không xác định được (hình dạng `LegacyGateError` — `{error:\"Why you do that?\"}` trần, không có `data`/`issue` nào cả).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "error",
                        "data"
                      ],
                      "properties": {
                        "error": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "data": {
                          "type": "string"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/LegacyGateError"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "lỗi xác thực key, bao gồm cả việc một key đã hết hạn/thu hồi ngoài khoảng ân hạn hậu-charge bị từ chối bằng SHIELD_KEY_REVOKED/SHIELD_KEY_EXPIRED.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          },
          "403": {
            "description": "SCOPE_DENIED | POOL_MISMATCH (PaymentIntent thuộc key khác, hình dạng `V1Error`) | WALLET_REQUIRED (hình dạng `LegacyControllerError`) | PK_POOL_MISMATCH khi `publishable_key` không cùng pool với `shield_key` (hình dạng `LegacyGateError` — `{error:\"Why you do that?\"}` trần).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/V1Error"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyControllerError"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyGateError"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "CHỈ LEGACY — shield_key thiếu/không hợp lệ (ngoài khoảng ân hạn hậu-charge), body rỗng. KHÔNG thể xảy ra trên v1 — xem ghi chú của NotFoundError.\n"
          },
          "409": {
            "description": "NOT_SUCCEEDED (mang `data.status` = trạng thái Stripe của PI) | ALREADY_COMPLETED (chỉ API cũ — v1 phát lại một envelope mới khi replay thay vì trả 409) | AMOUNT_MISMATCH\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyControllerError"
                }
              }
            }
          },
          "500": {
            "description": "Chuyển tiếp lỗi API Stripe nguyên bản (route này không bao giờ trả 502 — CAPTURED_PROOF_UNAVAILABLE chỉ dành cho PayPal); `issue` = `error.code` của Stripe khi có, nếu không là `STRIPE_ERROR` — KHÔNG phải enum đóng cho response này (xem `StripeErrorPassthrough`) — HOẶC \"Shield misconfigured\" (lệch chế độ key pk/sk của Stripe) | \"Internal server error\" (hình dạng `LegacyStringError`, không có `data`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/StripeErrorPassthrough"
                    },
                    {
                      "$ref": "#/components/schemas/LegacyStringError"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "SIGNING_UNAVAILABLE | SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE (SDK tự retry, xem ghi chú retry của x-sdk.stripe) | PAYMENT_ENV_MISMATCH (KHÔNG được SDK retry — lệch cuối cùng giữa env của key và livemode của PaymentIntent).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/wallet_beacon": {
      "post": {
        "operationId": "v1WalletBeacon",
        "summary": "Telemetry phía client theo kiểu best-effort (không bao giờ chặn checkout)",
        "description": "Kiểm tra origin qua `Origin` khi key có cấu hình `allowedOrigins`. Fire-and-forget từ SDK (`navigator.sendBeacon` hoặc `fetch(..., {keepalive:true})`); lỗi ở đây không bao giờ hiển thị cho người mua.\n",
        "tags": [
          "stripe"
        ],
        "security": [
          {
            "ShieldKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShieldKeyParam"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "string",
                    "example": "stripe_sdk"
                  },
                  "store": {
                    "type": "string"
                  },
                  "stage": {
                    "type": "string"
                  },
                  "detail": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "đã nhận — body rỗng; SDK bỏ qua body"
          },
          "401": {
            "$ref": "#/components/responses/ShieldKeyError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "503": {
            "$ref": "#/components/responses/RegistryUnavailableError"
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "operationId": "v1Jwks",
        "summary": "Lấy JWKS Ed25519 hiện tại",
        "description": "Không cần `shield_key` — công khai theo thiết kế (docs/đối tác fetch endpoint này không cần xác thực). Một `shield_key` hợp lệ tùy chọn sẽ gán lần fetch đó cho key đó chỉ để theo dõi go-live; một key thiếu/không hợp lệ vẫn được phục vụ JWKS, không tính. URL chuẩn: `https://keys.vkl-shield.com/v1/keys` (live) / `https://keys.vkl-shield.com/sandbox/v1/keys` (sandbox) — không bao giờ là một shield/park host. `Cache-Control: public, max-age=600`; tuân theo nó và loại bỏ các kid vắng mặt từ một lần fetch mới. Fail closed (503 `SIGNING_UNAVAILABLE`, `no-store`) thay vì bao giờ phục vụ một tập key rỗng/không đáng tin.\n",
        "tags": [
          "keys"
        ],
        "security": [],
        "parameters": [
          {
            "name": "shield_key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "tùy chọn — gán lần fetch cho mục đích theo dõi go-live, không bao giờ dùng để chặn truy cập"
          }
        ],
        "responses": {
          "200": {
            "description": "JWKS",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=600"
                }
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Jwks"
                }
              }
            }
          },
          "304": {
            "description": "không thay đổi (If-None-Match khớp ETag hiện tại)"
          },
          "503": {
            "description": "SIGNING_UNAVAILABLE | SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED — fail-closed, không bao giờ phục vụ một tập key rỗng/sai (hình dạng `V1Error`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/V1Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ShieldKeyParam": {
        "name": "shield_key",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^shpk_(live|test)_[0-9a-zA-Z]{24}_[0-9a-zA-Z]{6}$",
          "example": "shpk_test_xxxxxxxxxxxxxxxxxxxxxxxx_xxxxxx"
        },
        "description": "shield key v1 của đối tác; bắt buộc trên mọi route trừ `/v1/keys`. Phân đoạn key là `live` hoặc `test`; `test` tương ứng với môi trường `sandbox` ở mọi nơi khác trong spec này (các field `env`, `SHIELD_KEY_ENV_MISMATCH`) — `live`/`test` chỉ là quy ước đặt tên key, không phải một bộ giá trị môi trường khác. 6 ký tự cuối là checksum trên toàn bộ phần trước nó — một key sai checksum hoặc sai định dạng đều bị từ chối ngay, không cần tra cứu thêm.\n"
      }
    },
    "responses": {
      "ShieldKeyError": {
        "description": "SHIELD_KEY_INVALID | SHIELD_KEY_ENV_MISMATCH | SHIELD_KEY_REVOKED | SHIELD_KEY_EXPIRED (hình dạng `V1Error`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "ForbiddenError": {
        "description": "SCOPE_DENIED | ORIGIN_NOT_ALLOWED | POOL_MISMATCH (hình dạng `V1Error`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      },
      "NotFoundError": {
        "description": "NOT_FOUND (đường bị chặn wallet) | INVALID_SHIELD_KEY (/v1/shield/select, /v1/stripe/select) — body inline thuần, không bao giờ có message/docs (hình dạng `LegacyControllerError`). KHÔNG THỂ XẢY RA trên v1 — lỗi 404 này chỉ fire trên API cũ (không-v1). Được ghi lại ở đây chỉ để đầy đủ/tương thích với API cũ. Một path sai chữ hoa/thường hoặc có dấu gạch chéo cuối trên route v1 thay vào đó sẽ rơi vào 404 mặc định (HTML, ngoài spec này) vì router v1 xử lý đường dẫn nghiêm ngặt — không theo hình dạng JSON này.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/LegacyControllerError"
            }
          }
        }
      },
      "RegistryUnavailableError": {
        "description": "SHIELD_REGISTRY_UNAVAILABLE | SHIELD_REGISTRY_STALE | SHIELD_ENV_MISCONFIGURED | PAYMENT_ENV_MISMATCH | SIGNING_UNAVAILABLE (hình dạng `V1Error`).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/V1Error"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "ShieldKey": {
        "type": "apiKey",
        "in": "query",
        "name": "shield_key",
        "description": "Key `shpk_live_…` hoặc `shpk_test_…`. Đây là ranh giới xác thực bằng key, không phải một secret chung — các kiểm tra origin (nơi có) là biện pháp chống hotlink của trình duyệt, không phải cơ chế xác thực.\n"
      }
    },
    "schemas": {
      "V1Error": {
        "type": "object",
        "description": "Body lỗi dùng chung cho mọi lỗi auth/registry/signing/origin/pool (SHIELD_KEY_*, SCOPE_DENIED, ORIGIN_NOT_ALLOWED, POOL_MISMATCH, SHIELD_REGISTRY_*, PAYMENT_ENV_MISMATCH, CAPTURED_PROOF_UNAVAILABLE). `message`/`docs` LUÔN có mặt. Khác với `LegacyControllerError` dưới đây — hình dạng lỗi theo từng endpoint của PayPal/Stripe — không bao giờ mang `message`/`docs`.\n",
        "required": [
          "error",
          "data"
        ],
        "properties": {
          "error": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "issue",
              "message",
              "docs"
            ],
            "properties": {
              "issue": {
                "$ref": "#/components/schemas/V1Issue"
              },
              "message": {
                "type": "string"
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "LegacyControllerError": {
        "type": "object",
        "description": "Body lỗi theo từng endpoint của PayPal/Stripe — CÙNG hình dạng mà API cũ (không-v1) luôn trả về cho các route này. Chỉ `issue` được đảm bảo; `message`/`docs` KHÔNG BAO GIỜ có mặt (khác với `V1Error`). Một vài response không mang `data` nào cả — xem mô tả riêng của từng response để biết body chính xác.\n",
        "required": [
          "error",
          "data"
        ],
        "properties": {
          "error": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "issue"
            ],
            "properties": {
              "issue": {
                "$ref": "#/components/schemas/V1Issue"
              },
              "details": {
                "type": "array",
                "items": {},
                "description": "chi tiết validate theo field tùy chọn (ví dụ INVALID_SHIPPING_ADDRESS)"
              },
              "status": {
                "type": "string",
                "description": "echo trạng thái phía nhà cung cấp tùy chọn (ví dụ Stripe PaymentIntent.status khi NOT_SUCCEEDED)"
              },
              "message": {
                "type": "string",
                "description": "chỉ có mặt khi STRIPE_ERROR (chuyển tiếp error.message của Stripe)"
              }
            }
          }
        }
      },
      "LegacyStringError": {
        "type": "object",
        "description": "Body `{error:string}` trần không có `data` nào cả — khác với `LegacyGateError` (cùng hình dạng) và với `V1Error`/`LegacyControllerError` (có mang `data`). Dùng cho các lỗi cấu hình/nội bộ chung: \"Shield misconfigured\" (lệch chế độ key pk/sk của Stripe), \"Internal server error\" (lỗi không xác định), \"Shield unavailable\" (shield chưa được cấu hình secret trên route PayPal).\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "StripeErrorPassthrough": {
        "type": "object",
        "description": "Body 500 trên `/v1/stripe/create-intent` và `/v1/stripe/complete` khi lời gọi API Stripe gốc throw lỗi. `issue` là MỞ — nó chuyển tiếp nguyên văn `error.code` của Stripe (ví dụ `card_declined`, `parameter_invalid_integer`, `amount_too_small`) khi Stripe đặt một giá trị, chỉ fallback về chuỗi literal `STRIPE_ERROR` khi không có. KHÔNG phải thành viên của enum đóng `V1Issue` — không validate `issue` dựa trên enum đó cho response này.\n",
        "required": [
          "error",
          "data"
        ],
        "properties": {
          "error": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "data": {
            "type": "object",
            "required": [
              "issue",
              "message"
            ],
            "properties": {
              "issue": {
                "type": "string",
                "description": "error.code của Stripe, hoặc 'STRIPE_ERROR' khi Stripe không cung cấp"
              },
              "message": {
                "type": "string",
                "description": "error.message của Stripe, hoặc 'unknown'"
              }
            }
          }
        }
      },
      "LegacyGateError": {
        "type": "object",
        "description": "Body trần từ gate phân giải pk của Stripe và gate hostname của PayPal — `error` ở đây là một STRING, không phải `true`, và không có `data`/`issue` nào cả. Bao gồm Stripe MISSING_PK/UNKNOWN_PK/PK_POOL_MISMATCH và gate unknown-host/no-secret của PayPal; tên issue trong mô tả của từng response chỉ mang tính tài liệu, không bao giờ có mặt trong body thực tế.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "Why you do that?"
          }
        }
      },
      "V1Issue": {
        "type": "string",
        "description": "Mọi mã issue một response v1 có thể mang. Nhóm đầu tiên (SHIELD_KEY_*, SCOPE_DENIED, ORIGIN_NOT_ALLOWED, POOL_MISMATCH, SHIELD_REGISTRY_*, SHIELD_ENV_MISCONFIGURED, PAYMENT_ENV_MISMATCH, SIGNING_UNAVAILABLE, LEGACY_API_DISABLED, CAPTURED_PROOF_UNAVAILABLE) là bảng auth/registry/signing v1 dùng chung (bảng V1_ISSUE_STATUS dùng chung — được contract-test khớp 1:1). Các mã còn lại là các issue validate/business theo từng endpoint do các controller PayPal/Stripe bên dưới trả về (cùng mã mà API legacy trả về cho cùng endpoint). NGOẠI LỆ: `STRIPE_ERROR` được liệt kê ở đây như giá trị fallback literal, nhưng response 500 của Stripe create-intent/complete KHÔNG đóng với enum này — xem `StripeErrorPassthrough`, nơi ghi lại `issue` là `error.code` mở của chính Stripe.\n",
        "enum": [
          "SHIELD_KEY_INVALID",
          "SHIELD_KEY_ENV_MISMATCH",
          "SHIELD_KEY_REVOKED",
          "SHIELD_KEY_EXPIRED",
          "SCOPE_DENIED",
          "ORIGIN_NOT_ALLOWED",
          "POOL_MISMATCH",
          "SHIELD_REGISTRY_UNAVAILABLE",
          "SHIELD_REGISTRY_STALE",
          "SHIELD_ENV_MISCONFIGURED",
          "PAYMENT_ENV_MISMATCH",
          "SIGNING_UNAVAILABLE",
          "LEGACY_API_DISABLED",
          "CAPTURED_PROOF_UNAVAILABLE",
          "NOT_FOUND",
          "INVALID_SHIELD_KEY",
          "INVALID_SHIPPING_ADDRESS",
          "INVALID_EMAIL",
          "INVALID_ORDER",
          "MISSING_PK",
          "UNKNOWN_PK",
          "PK_POOL_MISMATCH",
          "RATE_LIMITED",
          "TEMPORARILY_LOCKED",
          "INVALID_PHONE",
          "NO_METHOD_FOR_AMOUNT",
          "NOT_SUCCEEDED",
          "ALREADY_COMPLETED",
          "AMOUNT_MISMATCH",
          "WALLET_REQUIRED",
          "STRIPE_ERROR",
          "SELECT_FAILED",
          "NO_ACTIVE",
          "NO_POOL"
        ]
      },
      "SignedEnvelope": {
        "type": "object",
        "description": "Body thành công v1 trên `/v1/capture_order` và `/v1/stripe/complete` (envelope builder v1 dùng chung). Chỉ mang bằng chứng Ed25519 — không bao giờ có các field legacy `signature`/`signed_fields`/ `payload_signature`. Xác minh `sig` dựa trên các byte chính xác của `signed_payload` (một chuỗi JSON, không phải object — không re-serialize nó) dùng public key cho `kid` lấy từ `/v1/keys`.\n",
        "required": [
          "error",
          "data",
          "signed_payload",
          "sig",
          "kid",
          "alg",
          "v"
        ],
        "properties": {
          "error": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "data": {
            "type": "object",
            "description": "Lặp lại hình dạng legacy cho endpoint này (kết quả PayPal OrdersCaptureRequest, hoặc signed_fields dạng flat của Stripe) — chỉ mang tính thông tin; bằng chứng là `signed_payload`+`sig`, không phải field này.\n"
          },
          "signed_payload": {
            "type": "string",
            "description": "chuỗi JSON chính xác đã được ký (xác minh dựa trên các byte chính xác này)"
          },
          "sig": {
            "type": "string",
            "description": "chữ ký Ed25519 base64url trên `signed_payload`"
          },
          "kid": {
            "type": "string",
            "description": "key id — dùng nó để tra JWK khớp trên `/v1/keys`"
          },
          "alg": {
            "type": "string",
            "enum": [
              "Ed25519"
            ]
          },
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "contact": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Contact"
              }
            ],
            "description": "Chỉ Stripe `/v1/stripe/complete` — thông tin liên hệ billing/shipping KHÔNG ĐƯỢC KÝ thu thập tại wallet sheet. Chỉ mang tính thông tin, không bao giờ là một phần của bằng chứng đã xác minh; không tin tưởng cho các quyết định fraud/compliance.\n"
          }
        }
      },
      "PaypalCaptureV1Payload": {
        "type": "object",
        "description": "Hình dạng đã parse của `signed_payload` cho `/v1/capture_order` (các field `extra` của PayPal capture signed-payload builder cộng với envelope builder v1 dùng chung). Mọi field dưới đây đều được ký — không field nào có thể bị thay đổi mà không làm `sig` không hợp lệ.\n",
        "required": [
          "order_id",
          "capture_id",
          "status",
          "amount",
          "currency",
          "invoice_id",
          "reference_id",
          "payee_merchant_id",
          "shield",
          "iat",
          "typ",
          "v",
          "partner_id",
          "key_id",
          "env",
          "store_domain"
        ],
        "properties": {
          "order_id": {
            "type": "string"
          },
          "capture_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "trạng thái capture của PayPal, ví dụ COMPLETED"
          },
          "amount": {
            "type": "string",
            "description": "chuỗi decimal, ví dụ \"19.99\""
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "invoice_id": {
            "type": "string",
            "description": "id đơn hàng của store"
          },
          "reference_id": {
            "type": "string",
            "description": "`''` khi purchase_unit của PayPal không mang giá trị nào (luôn là string, không bao giờ null). DO CLIENT KHAI BÁO lúc tạo order, nằm trong tập được ký chỉ để chống giả mạo; không được xác minh độc lập ở phía server, nên coi là CHỈ MANG TÍNH THÔNG TIN trừ khi bạn tự đặt nó và assert lại.\n"
          },
          "payee_merchant_id": {
            "type": "string",
            "description": "`''` khi không có (payee của PayPal được ưu tiên hơn merchant id fallback đã cấu hình của shield)"
          },
          "shield": {
            "type": "string",
            "description": "shield domain đã xử lý capture"
          },
          "iat": {
            "type": "integer",
            "description": "unix seconds",
            "được ký lúc capture": null
          },
          "typ": {
            "type": "string",
            "enum": [
              "paypal_capture"
            ]
          },
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "partner_id": {
            "type": "string",
            "description": "ranh giới bảo mật — phải khớp partner id của chính caller"
          },
          "key_id": {
            "type": "string",
            "description": "ranh giới bảo mật — phải là một trong các key id được caller cho phép"
          },
          "env": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ],
            "description": "ranh giới bảo mật — một key `shpk_test_…` tương ứng với 'sandbox' ở đây"
          },
          "store_domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "DO CLIENT KHAI BÁO, KHÔNG chuẩn hóa ở phía server — chỉ mang tính thông tin/audit, không bao giờ là một phần của ranh giới tin cậy (không ràng buộc việc xác minh vào giá trị này).\n"
          }
        }
      },
      "StripeAddress": {
        "type": "object",
        "description": "Hình dạng con địa chỉ bưu chính bên trong `Contact`. KHÔNG ĐƯỢC KÝ (chỉ để hiển thị). Phản chiếu object address gốc của Stripe; bất kỳ field nào có thể là `null` khi Stripe không thu thập nó.\n",
        "properties": {
          "line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Contact": {
        "type": "object",
        "x-signed": false,
        "description": "Thông tin liên hệ/giao hàng của người mua trả về cùng một envelope v1 Stripe. KHÔNG ĐƯỢC KÝ — KHÔNG phải một phần của bằng chứng đã xác minh, không dùng cho các quyết định fraud/compliance. `name`/`address` ưu tiên chi tiết SHIPPING, fallback về billing; `email`/`phone` ưu tiên BILLING (email fallback về `receipt_email`). Trên một lần replay v1, `wallet_type` có thể fallback về giá trị do client cung cấp trong request — coi đó là gợi ý từ client, không phải giá trị đáng tin, trên các lần gọi replay.\n",
        "required": [
          "name",
          "address",
          "email",
          "phone",
          "wallet_type"
        ],
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StripeAddress"
              },
              {
                "type": "null"
              }
            ],
            "description": "null khi Stripe không có cả shipping và billing address trên PaymentIntent"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "wallet_type": {
            "type": "string",
            "description": "nhãn wallet đáng tin suy ra từ lần charge (ví dụ apple_pay/google_pay); '' khi chưa biết; gợi ý từ client khi replay — xem mô tả ở trên"
          }
        }
      },
      "StripeCompleteV1Payload": {
        "type": "object",
        "description": "Hình dạng đã parse của `signed_payload` cho `/v1/stripe/complete` (các field dạng flat, cộng với mọi key `meta_*`). Mọi `properties` dưới đây cộng với mọi key `meta_*` đều ĐƯỢC KÝ (chống giả mạo); xác minh `sig` trên `signed_payload` chứng minh không field nào bị thay đổi sau khi shield tạo ra envelope này.\nRANH GIỚI BẢO MẬT (những gì một verifier PHẢI assert): `partner_id` + `key_id` (∈ các key id được bạn cho phép) + `env` + `invoice_id` + `amount_received` + `id` (chống replay — từ chối nếu đã settle) + `status === 'succeeded'` + `iat` trong khoảng [-600s, +300s] so với hiện tại. `store`, `store_domain`, `reference_id` và mọi key `meta_*` đều DO CLIENT KHAI BÁO lúc create-time (SDK/caller cung cấp chúng; server chỉ flatten và ký để chống giả mạo, không bao giờ xác minh độc lập nội dung) — CHỈ MANG TÍNH THÔNG TIN/audit/hiển thị, không bao giờ dùng chúng cho quyết định ràng buộc ở trên.\n",
        "required": [
          "id",
          "amount_received",
          "status",
          "store",
          "invoice_id",
          "reference_id",
          "ship_name",
          "ship_phone",
          "ship_line1",
          "ship_line2",
          "ship_city",
          "ship_state",
          "ship_postal",
          "ship_country",
          "bill_name",
          "bill_email",
          "bill_phone",
          "bill_line1",
          "bill_line2",
          "bill_city",
          "bill_state",
          "bill_postal",
          "bill_country",
          "typ",
          "v",
          "partner_id",
          "key_id",
          "env",
          "store_domain",
          "iat"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stripe PaymentIntent id (pi_…) — RANH GIỚI BẢO MẬT: key chống replay của bạn, từ chối nếu đã settle"
          },
          "amount_received": {
            "type": "integer",
            "description": "RANH GIỚI BẢO MẬT — số nguyên cent, chỉ USD (Stripe v1 chỉ hỗ trợ USD; không có field currency). Phải bằng tổng đơn hàng của bạn tính bằng cent."
          },
          "status": {
            "type": "string",
            "enum": [
              "succeeded"
            ],
            "description": "RANH GIỚI BẢO MẬT — phải chính xác là 'succeeded' (complete() chỉ ký một PI đã succeeded)"
          },
          "store": {
            "type": "string",
            "description": "DO CLIENT KHAI BÁO — `location.hostname` của trang store do SDK gửi lúc create-intent (chữ thường, không có port, giữ `www.` nếu có; KHÔNG chuẩn hóa ở phía server). CHỈ MANG TÍNH THÔNG TIN, không bao giờ là một phần của ranh giới tin cậy.\n"
          },
          "invoice_id": {
            "type": "string",
            "description": "RANH GIỚI BẢO MẬT — id đơn hàng store của bạn, ràng buộc-kiểm tra giá trị này với đơn hàng của bạn"
          },
          "reference_id": {
            "type": "string",
            "description": "'' khi không có. DO CLIENT KHAI BÁO lúc create-time — CHỈ MANG TÍNH THÔNG TIN trừ khi bạn tự đặt nó và assert lại"
          },
          "ship_name": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_phone": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_line1": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_line2": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_city": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_state": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_postal": {
            "type": "string",
            "description": "'' khi không có"
          },
          "ship_country": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_name": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_email": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_phone": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_line1": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_line2": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_city": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_state": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_postal": {
            "type": "string",
            "description": "'' khi không có"
          },
          "bill_country": {
            "type": "string",
            "description": "'' khi không có"
          },
          "typ": {
            "type": "string",
            "enum": [
              "stripe_complete"
            ]
          },
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "partner_id": {
            "type": "string",
            "description": "RANH GIỚI BẢO MẬT — phải khớp partner id của chính caller"
          },
          "key_id": {
            "type": "string",
            "description": "RANH GIỚI BẢO MẬT — phải là một trong các key id được caller cho phép"
          },
          "env": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ],
            "description": "RANH GIỚI BẢO MẬT — một key `shpk_test_…` tương ứng với 'sandbox' ở đây"
          },
          "store_domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "DO CLIENT KHAI BÁO — `location.hostname` của trang đã gọi `/v1/stripe/complete` (`completeBody.store_domain || null`); `''` trở thành `null`. Có thể khác `store` trên một luồng redirect-return (trang return, không phải trang checkout gốc). CHỈ MANG TÍNH THÔNG TIN, KHÔNG phải một phần của ranh giới tin cậy.\n"
          },
          "iat": {
            "type": "integer",
            "description": "RANH GIỚI BẢO MẬT — unix seconds; verifier phải ép buộc khoảng [-600s, +300s]"
          }
        },
        "patternProperties": {
          "^meta_": {
            "type": "string",
            "description": "Một key cho mỗi mục metadata PaymentIntent, trừ `settle_ref` (không bao giờ được ký). Một PI create-intent v1 mang `meta_channel, meta_store, meta_invoice_id, meta_reference_id, meta_shield, meta_amount_cents, meta_items_json, meta_items_total, meta_shipping_fee` cộng với `meta_key_id, meta_partner_id` khi request create-intent được xác thực bằng key. Một bằng chứng v1 hợp lệ từ `/v1/stripe/complete` LUÔN mang `meta_key_id`/`meta_partner_id`: route trả 403 POOL_MISMATCH khi metadata thuộc một key khác, nên một PI từ API cũ/không xác thực bằng key không thể tạo ra bằng chứng v1. Chúng chỉ vắng mặt trên metadata gốc của PaymentIntent cho các lời gọi create-intent kiểu cũ (không xác thực bằng key) — không bao giờ vắng mặt trên một envelope v1 đã ký. DO SERVER ĐẶT, không phải do client khai báo: `meta_shield`, `meta_channel` (chuẩn hóa ở phía server), `meta_key_id`/ `meta_partner_id` (từ key đã xác minh, không bao giờ từ request body). `meta_store, meta_invoice_id, meta_reference_id, meta_items_json, meta_items_total, meta_shipping_fee` LÀ do client khai báo lúc create-intent — CHỈ MANG TÍNH THÔNG TIN/audit/hiển thị, không bao giờ là một phần của ranh giới tin cậy. `meta_amount_cents` là số tiền charge lúc create-time (server tính). Các item trong `meta_items_json` là `{n,q,p,s}` với `p` là một số đã làm tròn, không phải string.\n"
          }
        },
        "additionalProperties": true,
        "x-additional-properties-note": "tương thích ngược về trước: verifier của đối tác nên BỎ QUA các key cấp cao nhất chưa biết (metadata/field mới có thể được thêm mà không cần tăng version) thay vì từ chối envelope.\n"
      },
      "Jwks": {
        "type": "object",
        "description": "Tập JWK OKP Ed25519 theo RFC 8037 phục vụ bởi `/v1/keys`.",
        "required": [
          "keys"
        ],
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "kty",
                "crv",
                "x",
                "kid",
                "alg",
                "use"
              ],
              "properties": {
                "kty": {
                  "type": "string",
                  "enum": [
                    "OKP"
                  ]
                },
                "crv": {
                  "type": "string",
                  "enum": [
                    "Ed25519"
                  ]
                },
                "x": {
                  "type": "string",
                  "description": "public key 32 byte dạng base64url"
                },
                "kid": {
                  "type": "string"
                },
                "alg": {
                  "type": "string",
                  "enum": [
                    "EdDSA"
                  ]
                },
                "use": {
                  "type": "string",
                  "enum": [
                    "sig"
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "ShieldKey": []
    }
  ],
  "x-sdk": {
    "paypal": {
      "script": "/assets/v1/paypal-shield-sdk.js",
      "init": [
        {
          "name": "container",
          "type": "string|Element",
          "required": true
        },
        {
          "name": "shieldKey",
          "type": "string",
          "required": true
        },
        {
          "name": "getOrderData",
          "type": "async (fundingSource) => {id, body}",
          "required": true
        },
        {
          "name": "slug",
          "type": "string",
          "required": false,
          "default": null
        },
        {
          "name": "iframeParams",
          "type": "object",
          "required": false,
          "default": {}
        },
        {
          "name": "selectBase",
          "type": "string",
          "required": false,
          "default": "origin of this script tag"
        }
      ],
      "initReturns": {
        "type": "{destroy(): void}",
        "description": "destroy() gỡ iframe + listener; gọi trước khi re-init trên cùng container (SPA đổi route)."
      },
      "callbacks": [
        {
          "name": "onClicked",
          "args": [
            "fundingSource"
          ],
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "onCapturing",
          "args": []
        },
        {
          "name": "onCompleted",
          "args": [
            "value",
            "orderId"
          ],
          "schema": {
            "$ref": "#/components/schemas/SignedEnvelope"
          }
        },
        {
          "name": "onCancel",
          "args": []
        },
        {
          "name": "onFailed",
          "args": [
            "error"
          ],
          "schema": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      ]
    },
    "stripe": {
      "script": "/assets/v1/stripe-shield-sdk.js",
      "cardForm": "/assets/v1/stripe-shield-sdk-card-form.js",
      "init": [
        {
          "name": "container",
          "type": "{applePay: string|Element, googlePay: string|Element}",
          "required": true,
          "description": "mỗi giá trị là một chuỗi CSS selector HOẶC một Element"
        },
        {
          "name": "shieldKey",
          "type": "string",
          "required": true
        },
        {
          "name": "getOrderData",
          "type": "async () => <PayPal-shaped order body>",
          "required": true
        },
        {
          "name": "selectBase",
          "type": "string",
          "required": false,
          "default": "origin of this script tag"
        },
        {
          "name": "slug",
          "type": "string",
          "required": false,
          "default": null
        },
        {
          "name": "clientStatic",
          "type": "object",
          "required": false,
          "default": "auto-collected fingerprint"
        },
        {
          "name": "cardForm",
          "type": "object",
          "required": false,
          "default": null,
          "description": "{container: string|Element, submitButton:{class,text}, appearance, fonts}"
        }
      ],
      "initReturns": {
        "type": "{destroy(): void}",
        "description": "destroy() unmount bất kỳ thứ gì instance này đã mount (ECE / DOM card form), xóa listener/timer của nó, và biến các callback của nó thành no-op; an toàn khi gọi nhiều lần. NGOẠI LỆ: khi một thanh toán đang diễn ra trên instance đó (giữa lúc pay()/redirect-return bắt đầu và onCompleted/onFailed cuối cùng), destroy() bị trì hoãn — callback và backoff retry /complete vẫn sống, và teardown thực sự chạy ngay sau khi callback cuối cùng fire (trừ khi bị hủy, xem dưới). Gọi init() lại khi một instance trước đó từ CÙNG trang vẫn đang sống sẽ destroy instance trước đó trước, TRỪ KHI nó có một thanh toán đang diễn ra — trong trường hợp đó init() trả về CHÍNH instance sống đó không đổi (config/callback mới truyền vào lần gọi này bị bỏ qua). Nếu instance in-flight đó cũng có một destroy() đang chờ (trì hoãn theo trên), lần gọi init() này SẼ HỦY nó thay vì để nó chạy, nên instance vẫn được mount sau callback cuối cùng của thanh toán — giữ nút Pay/card form có thể dùng để retry sau một lỗi có thể retry (bị từ chối, rate limit, …) thay vì phá checkout mà không còn gì để re-init.\n"
      },
      "callbacks": [
        {
          "name": "onReady",
          "args": [
            "status"
          ],
          "schema": {
            "type": "object",
            "properties": {
              "wallets": {
                "type": "object",
                "properties": {
                  "applePay": {
                    "type": "boolean"
                  },
                  "googlePay": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        {
          "name": "onClicked",
          "args": [
            "status"
          ],
          "schema": {
            "type": "object",
            "properties": {
              "walletType": {
                "type": "string"
              }
            }
          }
        },
        {
          "name": "onProcessing",
          "args": [
            "status"
          ],
          "schema": {
            "type": "object",
            "properties": {
              "walletType": {
                "type": "string"
              }
            }
          }
        },
        {
          "name": "onCompleted",
          "args": [
            "value",
            "orderId"
          ],
          "schema": {
            "$ref": "#/components/schemas/SignedEnvelope"
          }
        },
        {
          "name": "onCancel",
          "args": []
        },
        {
          "name": "onFailed",
          "args": [
            "error"
          ],
          "schema": {
            "type": "object",
            "description": "`code` thường là một bộ phân loại cấp SDK (`error`, `redirect_failed`, `redirect_not_succeeded`), nhưng trên các lỗi create-intent, SDK chuyển tiếp nguyên văn `issue` của server thành `code` (ví dụ RATE_LIMITED, INVALID_ORDER, SHIELD_KEY_*) để store phân biệt các loại lỗi; trên một lần Stripe từ chối, đó là `error.code`/`decline_code` gốc của Stripe. `code: 'charged_but_unconfirmed'` nghĩa là người mua ĐÃ bị charge (mọi lỗi cuối cùng của /v1/stripe/complete trừ NOT_SUCCEEDED, và trường hợp `redirect_state_missing` trên đường redirect-return dưới đây) — không hiển thị lỗi hoặc mời thử lại; đối soát qua webhook AIO `transaction.completed` thay vào đó.\n",
            "properties": {
              "message": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "walletType": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "channel": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "'card_form' khi lỗi đến từ đường submit của card-form"
              },
              "paymentIntentId": {
                "type": "string",
                "description": "có mặt khi code:'charged_but_unconfirmed'"
              },
              "reason": {
                "type": "string",
                "enum": [
                  "redirect_state_missing"
                ],
                "description": "chỉ có mặt trên đường redirect-return khi trạng thái đã lưu bị mất/hết hạn"
              }
            }
          }
        }
      ]
    }
  },
  "x-postmessage": {
    "paypal": [
      {
        "type": "CHECKOUT_ORDER_DATA",
        "dir": "parent→iframe",
        "schema": {
          "type": "object",
          "properties": {
            "value": {
              "type": "object",
              "properties": {
                "id": {},
                "body": {
                  "type": "object"
                }
              }
            },
            "hostname": {
              "type": "string"
            }
          }
        }
      },
      {
        "type": "PAYPAL_PAYMENT_INIT",
        "dir": "iframe→parent",
        "schema": {
          "type": "object",
          "properties": {
            "source": {
              "type": "string"
            }
          }
        },
        "maps_to": "onClicked"
      },
      {
        "type": "PAYPAL_CAPTURING",
        "dir": "iframe→parent",
        "schema": {
          "type": "object"
        },
        "maps_to": "onCapturing"
      },
      {
        "type": "PAYPAL_PAYMENT_COMPLETED",
        "dir": "iframe→parent",
        "schema": {
          "type": "object",
          "properties": {
            "value": {
              "$ref": "#/components/schemas/SignedEnvelope"
            },
            "order_id": {
              "type": "string"
            }
          }
        },
        "maps_to": "onCompleted"
      },
      {
        "type": "PAYPAL_PAYMENT_CANCEL",
        "dir": "iframe→parent",
        "schema": {
          "type": "object"
        },
        "maps_to": "onCancel"
      },
      {
        "type": "PAYPAL_PAYMENT_ERROR",
        "dir": "iframe→parent",
        "schema": {
          "type": "object",
          "properties": {
            "value": {
              "type": "object"
            }
          }
        },
        "maps_to": "onFailed"
      },
      {
        "type": "PAYPAL_RESIZE_FORM",
        "dir": "iframe→parent",
        "schema": {
          "type": "object",
          "properties": {
            "value": {
              "type": "number"
            }
          }
        },
        "internal": true
      }
    ]
  }
}