{
  "info": {
    "_postman_id": "6d2f1b7a-3c48-4e9a-9f1d-2b7c8a5e4d31",
    "name": "Tổng Kho eSIM — Open API đối tác · v2.4.0",
    "description": "Collection sinh TỰ ĐỘNG từ OpenAPI spec của Tổng Kho eSIM (spec sinh từ code — không viết tay).\n\nCách dùng:\n1. Mở tab Variables của collection, điền `key_id` và `secret`.\n2. CHỈ dùng key thử `tke_test_` hoặc `tke_member_test_`; mọi key live bị chặn.\n3. `base_url` chỉ nhận api-testing hoặc localhost. Các operation chỉ-live bị loại khỏi collection này.\n\nPre-request script của collection tự ký HMAC-SHA256 cho mọi request (4 header X-TKE-*).\nSecret nằm trong biến collection trên máy bạn — đừng export collection kèm secret rồi chia sẻ.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "catalog",
      "description": "Danh mục gói: MỘT bề mặt cho cả hai hệ nguồn. Hệ nguồn là TRƯỜNG `market` (`domestic` | `international`) và bộ lọc `?market=`, không còn là hai đường dẫn riêng.\n\nCatalog KHÔNG có khái niệm tồn kho (không số, không mức `available`/`low`/`out_of_stock`): gói cứ hiện trên kệ, đặt hàng mà nguồn không cấp được thì `POST /v1/orders` trả lỗi rõ ràng (`esim.out_of_stock`…) thay vì catalog nói trước.\n\nPhân trang CON TRỎ thống nhất. `total` chỉ có khi đếm đủ rẻ (lọc theo hệ nội địa), còn lại là `null`.",
      "item": [
        {
          "name": "GET /v1/catalog/packages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/catalog/packages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "catalog",
                "packages"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "50",
                  "description": "Số bản ghi mỗi trang. Mặc định 50, trần 100 — gửi lớn hơn thì bị kẹp về 100 (KHÔNG lỗi).",
                  "disabled": true
                },
                {
                  "key": "market",
                  "value": "",
                  "description": "Hệ nguồn: `domestic` | `international`. Giá trị ngoài danh sách trả `internal.invalid_filter` (400), KHÔNG trả cả danh mục: một lần gõ nhầm không được đọc thành \"bộ lọc này không loại gói nào\".",
                  "disabled": true
                },
                {
                  "key": "country",
                  "value": "",
                  "description": "Mã quốc gia (ISO-3166 alpha-2, vd `VN`). Chỉ gói phủ quốc gia này.",
                  "disabled": true
                },
                {
                  "key": "region",
                  "value": "",
                  "description": "Slug khu vực (vd `chau-a`). Gói phủ ít nhất một nước của khu, cộng mọi gói phủ toàn cầu.",
                  "disabled": true
                },
                {
                  "key": "q",
                  "value": "",
                  "description": "Tìm tự do trên mã công khai + tên gói (khớp một phần, không phân biệt hoa thường).",
                  "disabled": true
                },
                {
                  "key": "tag",
                  "value": "",
                  "description": "Lọc theo MỘT nhãn thương mại. Nhãn được chuẩn hoá như lúc lưu (bỏ dấu cách thừa, hạ chữ thường, nối bằng gạch), nên `Ban Le` và `ban-le` là cùng một nhãn.",
                  "disabled": true
                },
                {
                  "key": "data_model",
                  "value": "",
                  "description": "Mô hình dung lượng: `daily` | `fixed` | `unlimited`. THAY `supports_topup` cũ. Giá trị lạ trả `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "data_min_bytes",
                  "value": "5368709120",
                  "description": "Dung lượng tối thiểu, tính bằng **BYTES**. Gói theo ngày so bằng mức MỖI NGÀY; gói không giới hạn LUÔN lọt qua ngưỡng dưới.",
                  "disabled": true
                },
                {
                  "key": "data_max_bytes",
                  "value": "5368709120",
                  "description": "Dung lượng tối đa, tính bằng **BYTES**. Gói không giới hạn bị LOẠI: một trần dung lượng không áp được lên gói vô hạn, cho lọt là trả gói vô hạn cho người đang tìm gói nhỏ nhất.",
                  "disabled": true
                },
                {
                  "key": "days_min",
                  "value": "1",
                  "description": "Số ngày hiệu lực tối thiểu.",
                  "disabled": true
                },
                {
                  "key": "days_max",
                  "value": "1",
                  "description": "Số ngày hiệu lực tối đa.",
                  "disabled": true
                },
                {
                  "key": "price_min",
                  "value": "1",
                  "description": "Giá tối thiểu (VND), so trên GIÁ CỦA TỔ CHỨC BẠN. Bộ lọc này chạy SAU khi máy chủ cắt trang, nên nó có thể làm một trang ngắn đi mà không có nghĩa là hết dữ liệu.",
                  "disabled": true
                },
                {
                  "key": "price_max",
                  "value": "1",
                  "description": "Giá tối đa (VND), cùng luật với `price_min`.",
                  "disabled": true
                },
                {
                  "key": "network_gen",
                  "value": "",
                  "description": "Chỉ gói chạy được thế hệ mạng này (`2g` | `3g` | `4g` | `5g`). Giá trị lạ trả `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "hotspot",
                  "value": "true",
                  "description": "`true` = chỉ gói KHAI có phát Wi-Fi; `false` = chỉ gói KHAI không có. Gói CHƯA KHAI (`features.hotspot = null`) không lọt vào vế nào — luật `null` sống ở bộ lọc y như ở phản hồi. Chỉ nhận đúng hai chuỗi `true`/`false`.",
                  "disabled": true
                },
                {
                  "key": "app",
                  "value": "",
                  "description": "Chỉ gói KHAI ứng dụng này chạy được trên ít nhất một hệ điều hành (`app=tiktok`). Gói không khai gì về ứng dụng ấy KHÔNG lọt — im lặng không phải lời hứa. Hôm nay chỉ nhận `tiktok`; mã khác trả `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "voice",
                  "value": "true",
                  "description": "`true` = chỉ gói KHAI có nghe gọi; `false` = chỉ gói KHAI không có. Gói chưa khai không lọt vào vế nào. Chỉ nhận đúng hai chuỗi `true`/`false`.",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Con trỏ trang kế: truyền lại NGUYÊN VĂN giá trị `next_cursor` của trang trước.\n\n**Dừng khi `next_cursor` là `null`, ĐỪNG dừng khi trang ngắn.** Ba cổng chạy SAU khi máy chủ đã cắt trang (giới hạn danh mục của khoá · gói phải có giá cho tổ chức bạn · bộ lọc khoảng giá) có thể lược bớt dòng, nên một trang trả 3 phần tử vẫn có thể còn hàng nghìn gói phía sau.\n\nCon trỏ mang sẵn DẤU VÂN TAY của bộ lọc đã sinh ra nó. Gửi kèm bộ lọc khác (hoặc gửi một chuỗi hỏng) trả `catalog.cursor_mismatch` (400) thay vì đọc tiếp lặng lẽ từ một truy vấn khác: nối hai trang của hai bộ lọc lại là một danh mục chưa từng tồn tại, và không tầng nào phát hiện được.",
                  "disabled": true
                }
              ]
            },
            "description": "Danh sách gói\n\nScope yêu cầu: catalog:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/catalog/packages."
          }
        },
        {
          "name": "GET /v1/catalog/packages/{code}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/catalog/packages/:code",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "catalog",
                "packages",
                ":code"
              ],
              "variable": [
                {
                  "key": "code",
                  "value": "",
                  "description": "Mã gói, chính là `code` trong danh mục (`public_sku`). BẤT BIẾN theo gói: không đổi khi chúng tôi đổi nguồn hàng phía sau, nên lưu được vào hệ thống của bạn làm khoá đối soát. Mã ngoài giới hạn danh mục của khoá, hoặc mã mà tổ chức bạn chưa có giá, trả `404 partner.package_not_visible` giống hệt mã không tồn tại."
                }
              ]
            },
            "description": "Chi tiết một gói\n\nScope yêu cầu: catalog:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/catalog/packages/{code}."
          }
        }
      ]
    },
    {
      "name": "orders",
      "description": "Đặt đơn + đối soát",
      "item": [
        {
          "name": "GET /v1/orders",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/orders",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "50",
                  "description": "Số bản ghi mỗi trang. Mặc định 50, trần 200 — gửi lớn hơn thì bị kẹp về 200 (KHÔNG lỗi).",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "completed",
                  "description": "Lọc theo trạng thái đơn. Giá trị ngoài danh sách → `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "external_ref",
                  "value": "",
                  "description": "Lọc theo mã tham chiếu phía bạn gửi lúc chốt đơn (khớp CHÍNH XÁC).",
                  "disabled": true
                },
                {
                  "key": "updated_since",
                  "value": "2026-07-11T08:12:30Z",
                  "description": "ISO-8601 UTC. Trả mọi bản ghi có `updated_at >= updated_since`, sắp tăng dần — đây là đường ĐỐI SOÁT KÉO chuẩn (§8.5): miss webhook vẫn tái dựng được trạng thái. Sai định dạng → `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Con trỏ trang kế — truyền lại NGUYÊN VĂN giá trị `next_cursor` của trang trước. CẢNH BÁO: cursor sai định dạng KHÔNG báo lỗi, server đọc lại TỪ ĐẦU danh sách (chọn có chủ ý để một lần đổi cách mã hóa cursor không làm chết vòng đối soát đang chạy của đối tác). Hệ quả phải biết: vòng lặp đối soát tự động truyền cursor rác sẽ quay vòng vô tận trên trang đầu thay vì dừng — hãy kiểm `next_cursor` khác cursor vừa gửi trước khi lặp tiếp.",
                  "disabled": true
                }
              ]
            },
            "description": "Danh sách đơn (đối soát kéo)\n\nScope yêu cầu: orders:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/orders."
          }
        },
        {
          "name": "POST /v1/orders",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/orders",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders"
              ]
            },
            "description": "Đặt đơn hàng (mua trực tiếp)\n\nScope yêu cầu: orders:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/orders.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_ref\": \"po-1001\",\n  \"include\": [\n    \"lpa\"\n  ],\n  \"invoice_buyer\": {\n    \"address\": \"<address>\",\n    \"email\": \"<email>\",\n    \"full_name\": \"<full_name>\",\n    \"legal_name\": \"<legal_name>\",\n    \"tax_code\": \"TKE-VN-30D-X3K9F\"\n  },\n  \"items\": [\n    {\n      \"package_code\": \"TKE-VN-30D-X3K9F\",\n      \"quantity\": 2\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/orders/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/orders/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders",
                ":id"
              ],
              "query": [
                {
                  "key": "include",
                  "value": "lpa",
                  "description": "Xin thêm khối dữ liệu, ngăn bằng dấu phẩy. `lpa` = trả khối `install` (mã cài eSIM); `qr` = trả thêm `install.qr_code_base64` (chỉ có tác dụng ở endpoint CHI TIẾT).\n\n**TRẢ MÃ LÀ PHÁT MÃ.** Xin `lpa` sẽ đóng VĨNH VIỄN cửa sổ huỷ của những chiếc eSIM trong phản hồi (`cancellable.allowed` thành `false`, `reason = code_disclosed`) — mã đã rời hệ thống thì không thu hồi được. Giao ngay cho khách thì xin; đặt trước giao sau thì đừng xin, và bạn giữ được quyền huỷ. Giá trị lạ → `internal.invalid_filter` (400, `field: \"include\"`).",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID đơn hàng (UUID)."
                }
              ]
            },
            "description": "Chi tiết đơn + eSIM\n\nScope yêu cầu: orders:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/orders/{id}."
          }
        },
        {
          "name": "POST /v1/orders/{id}/resend",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/orders/:id/resend",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders",
                ":id",
                "resend"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID đơn hàng (UUID)."
                }
              ]
            },
            "description": "Gửi lại email/QR của đơn\n\nScope yêu cầu: orders:write\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/orders/{id}/resend."
          }
        },
        {
          "name": "POST /v1/orders/bulk",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/orders/bulk",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders",
                "bulk"
              ]
            },
            "description": "Đặt lô nhiều dòng (mỗi dòng một đơn)\n\nScope yêu cầu: orders:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/orders/bulk.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_ref\": \"batch-2026-001\",\n  \"items\": [\n    {\n      \"external_ref\": \"po-1001\",\n      \"package_code\": \"TKE-VN-30D-X3K9F\",\n      \"quantity\": 5\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/orders/bulk/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/orders/bulk/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "orders",
                "bulk",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID lô đặt hàng loạt (UUID) trả về từ POST /v1/orders/bulk."
                }
              ]
            },
            "description": "Kết quả một lô đặt hàng\n\nScope yêu cầu: orders:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/orders/bulk/{id}."
          }
        }
      ]
    },
    {
      "name": "esims",
      "description": "Hồ sơ eSIM, usage, topup, hủy",
      "item": [
        {
          "name": "GET /v1/esims",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/esims",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "50",
                  "description": "Số bản ghi mỗi trang. Mặc định 50, trần 200 — gửi lớn hơn thì bị kẹp về 200 (KHÔNG lỗi).",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "pending",
                  "description": "Lọc theo `esim_status`. Giá trị ngoài danh sách → `internal.invalid_filter` (400) — KHÔNG trả danh sách rỗng, để một lần gõ nhầm (`canceled` thiếu chữ `l`) không bị đọc thành \"khách không có eSIM nào ở trạng thái đó\".",
                  "disabled": true
                },
                {
                  "key": "order_id",
                  "value": "018f2b00-0000-7000-8000-00000000009a",
                  "description": "Chỉ eSIM thuộc một đơn.",
                  "disabled": true
                },
                {
                  "key": "iccid",
                  "value": "",
                  "description": "Lọc theo ICCID (chuỗi, khớp chính xác).",
                  "disabled": true
                },
                {
                  "key": "external_ref",
                  "value": "",
                  "description": "Mọi eSIM của đơn mang mã đơn phía bạn này (`external_ref` gửi lúc đặt, khớp chính xác).",
                  "disabled": true
                },
                {
                  "key": "updated_since",
                  "value": "2026-07-11T08:12:30Z",
                  "description": "ISO-8601 UTC. Trả mọi bản ghi có `updated_at >= updated_since`, sắp tăng dần — đây là đường ĐỐI SOÁT KÉO chuẩn (§8.5): miss webhook vẫn tái dựng được trạng thái. Sai định dạng → `internal.invalid_filter` (400).",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Con trỏ trang kế — truyền lại NGUYÊN VĂN giá trị `next_cursor` của trang trước. CẢNH BÁO: cursor sai định dạng KHÔNG báo lỗi, server đọc lại TỪ ĐẦU danh sách (chọn có chủ ý để một lần đổi cách mã hóa cursor không làm chết vòng đối soát đang chạy của đối tác). Hệ quả phải biết: vòng lặp đối soát tự động truyền cursor rác sẽ quay vòng vô tận trên trang đầu thay vì dừng — hãy kiểm `next_cursor` khác cursor vừa gửi trước khi lặp tiếp.",
                  "disabled": true
                },
                {
                  "key": "include",
                  "value": "lpa",
                  "description": "Xin thêm khối dữ liệu, ngăn bằng dấu phẩy. `lpa` = trả khối `install` (mã cài eSIM); `qr` = trả thêm `install.qr_code_base64` (chỉ có tác dụng ở endpoint CHI TIẾT).\n\n**TRẢ MÃ LÀ PHÁT MÃ.** Xin `lpa` sẽ đóng VĨNH VIỄN cửa sổ huỷ của những chiếc eSIM trong phản hồi (`cancellable.allowed` thành `false`, `reason = code_disclosed`) — mã đã rời hệ thống thì không thu hồi được. Giao ngay cho khách thì xin; đặt trước giao sau thì đừng xin, và bạn giữ được quyền huỷ. Giá trị lạ → `internal.invalid_filter` (400, `field: \"include\"`).",
                  "disabled": true
                }
              ]
            },
            "description": "Danh sách eSIM (đối soát kéo)\n\nScope yêu cầu: esims:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/esims."
          }
        },
        {
          "name": "GET /v1/esims/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/esims/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims",
                ":id"
              ],
              "query": [
                {
                  "key": "include",
                  "value": "lpa",
                  "description": "Xin thêm khối dữ liệu, ngăn bằng dấu phẩy. `lpa` = trả khối `install` (mã cài eSIM); `qr` = trả thêm `install.qr_code_base64` (chỉ có tác dụng ở endpoint CHI TIẾT).\n\n**TRẢ MÃ LÀ PHÁT MÃ.** Xin `lpa` sẽ đóng VĨNH VIỄN cửa sổ huỷ của những chiếc eSIM trong phản hồi (`cancellable.allowed` thành `false`, `reason = code_disclosed`) — mã đã rời hệ thống thì không thu hồi được. Giao ngay cho khách thì xin; đặt trước giao sau thì đừng xin, và bạn giữ được quyền huỷ. Giá trị lạ → `internal.invalid_filter` (400, `field: \"include\"`).",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Chi tiết một eSIM\n\nScope yêu cầu: esims:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/esims/{id}."
          }
        },
        {
          "name": "POST /v1/esims/{id}/cancel",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/esims/:id/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims",
                ":id",
                "cancel"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Hủy eSIM chưa dùng\n\nScope yêu cầu: esims:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/esims/{id}/cancel."
          }
        },
        {
          "name": "POST /v1/esims/{id}/change-plan",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/esims/:id/change-plan",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims",
                ":id",
                "change-plan"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Nạp thêm gói vào eSIM\n\nScope yêu cầu: esims:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/esims/{id}/change-plan.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"package_code\": \"TKE-VN-7D-P4M2Q\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/esims/{id}/plan-options",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/esims/:id/plan-options",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims",
                ":id",
                "plan-options"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "50",
                  "description": "Số bản ghi mỗi trang. Mặc định 50, trần 200 — gửi lớn hơn thì bị kẹp về 200 (KHÔNG lỗi).",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Con trỏ trang kế — truyền lại NGUYÊN VĂN giá trị `next_cursor` của trang trước. CẢNH BÁO: cursor sai định dạng KHÔNG báo lỗi, server đọc lại TỪ ĐẦU danh sách (chọn có chủ ý để một lần đổi cách mã hóa cursor không làm chết vòng đối soát đang chạy của đối tác). Hệ quả phải biết: vòng lặp đối soát tự động truyền cursor rác sẽ quay vòng vô tận trên trang đầu thay vì dừng — hãy kiểm `next_cursor` khác cursor vừa gửi trước khi lặp tiếp.",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Gói nạp hợp lệ cho eSIM này\n\nScope yêu cầu: esims:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/esims/{id}/plan-options."
          }
        },
        {
          "name": "GET /v1/esims/{id}/usage",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/esims/:id/usage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "esims",
                ":id",
                "usage"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Dung lượng đã dùng\n\nScope yêu cầu: esims:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/esims/{id}/usage."
          }
        }
      ]
    },
    {
      "name": "wallet",
      "description": "Số dư ví",
      "item": [
        {
          "name": "GET /v1/wallet/balance",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/wallet/balance",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "wallet",
                "balance"
              ]
            },
            "description": "Số dư ví\n\nScope yêu cầu: wallet:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/wallet/balance."
          }
        }
      ]
    },
    {
      "name": "webhook",
      "description": "Cấu hình webhook + delivery log + simulator",
      "item": [
        {
          "name": "POST /v1/testmode/webhooks/fail-next",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/testmode/webhooks/fail-next",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "webhooks",
                "fail-next"
              ]
            },
            "description": "Mô phỏng lỗi webhook kế tiếp (chỉ test key)\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/testmode/webhooks/fail-next.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"count\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/testmode/webhooks/fire",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/testmode/webhooks/fire",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "webhooks",
                "fire"
              ]
            },
            "description": "Bắn thử webhook (chỉ test key)\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/testmode/webhooks/fire.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"data\": {},\n  \"event_type\": \"order.completed\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/webhooks",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "Danh sách webhook endpoint\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/webhooks."
          }
        },
        {
          "name": "POST /v1/webhooks",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "Tạo webhook endpoint\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/webhooks.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\n    \"order.completed\"\n  ],\n  \"url\": \"https://partner.example.com/hooks/tke\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "GET /v1/webhooks/{id}",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhooks/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID webhook endpoint (`id` trả về từ `POST /v1/webhooks` hoặc `GET /v1/webhooks`)."
                }
              ]
            },
            "description": "Chi tiết một webhook endpoint\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/webhooks/{id}."
          }
        },
        {
          "name": "PUT /v1/webhooks/{id}",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhooks/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID webhook endpoint (`id` trả về từ `POST /v1/webhooks` hoặc `GET /v1/webhooks`)."
                }
              ]
            },
            "description": "Sửa webhook endpoint\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho PUT /v1/webhooks/{id}.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\n    \"order.completed\"\n  ],\n  \"url\": \"https://partner.example.com/hooks/tke\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "DELETE /v1/webhooks/{id}",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhooks/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID webhook endpoint (`id` trả về từ `POST /v1/webhooks` hoặc `GET /v1/webhooks`)."
                }
              ]
            },
            "description": "Gỡ webhook endpoint (ngừng nhận sự kiện mới)\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho DELETE /v1/webhooks/{id}."
          }
        },
        {
          "name": "GET /v1/webhooks/deliveries",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhooks/deliveries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "deliveries"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Số trang 1-based. Trang vượt quá dữ liệu trả `items: []` + `total` thật (200, không 404).",
                  "disabled": true
                },
                {
                  "key": "page_size",
                  "value": "50",
                  "description": "Số bản ghi mỗi trang. Mặc định 50, trần 200 — gửi lớn hơn thì bị kẹp về 200 (KHÔNG lỗi).",
                  "disabled": true
                },
                {
                  "key": "event_type",
                  "value": "",
                  "description": "Lọc theo loại sự kiện (xem danh mục webhook events).",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "pending",
                  "description": "Lọc theo trạng thái gửi. `?status=failed` là bộ lọc dùng cho đối soát kéo webhook (§13). Giá trị lạ KHÔNG báo lỗi — bộ lọc bị BỎ QUA và trả về toàn bộ log.",
                  "disabled": true
                },
                {
                  "key": "event_id",
                  "value": "018f3c00-0000-7000-8000-0000000000b0",
                  "description": "Mọi lần gửi (kể cả replay) của MỘT sự kiện.",
                  "disabled": true
                }
              ]
            },
            "description": "Log các lần gửi webhook\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/webhooks/deliveries."
          }
        },
        {
          "name": "POST /v1/webhooks/deliveries/{id}/replay",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhooks/deliveries/:id/replay",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "deliveries",
                ":id",
                "replay"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID một lượt gửi webhook (`items[].id` của `GET /v1/webhooks/deliveries`)."
                }
              ]
            },
            "description": "Gửi lại một webhook\n\nScope yêu cầu: webhook:manage\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/webhooks/deliveries/{id}/replay."
          }
        }
      ]
    },
    {
      "name": "testmode",
      "description": "Chỉ dùng key thử tke_test_ hoặc tke_member_test_ trên host api-testing. Mô phỏng eSIM và dữ liệu thử của đúng chủ, không dùng ví hoặc NCC thật. Member không có webhook, bulk hoặc hàng chờ duyệt hoàn trong chế độ thử.",
      "item": [
        {
          "name": "POST /v1/testmode/esims/{id}/simulate",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/testmode/esims/:id/simulate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "esims",
                ":id",
                "simulate"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "",
                  "description": "ID ổn định của eSIM đã bán (KHÔNG phải ICCID — ICCID có thể tái sử dụng)."
                }
              ]
            },
            "description": "Mô phỏng vòng đời eSIM chế độ thử (chỉ test key)\n\nScope yêu cầu: esims:write\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/testmode/esims/{id}/simulate.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"data_used_mb\": 4096,\n  \"expire_at\": \"2026-07-11T08:12:30Z\",\n  \"expire_now\": true,\n  \"provider_reject\": true,\n  \"status\": \"enabled\",\n  \"throttled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/testmode/reset",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/testmode/reset",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "reset"
              ]
            },
            "description": "Đặt lại dữ liệu chế độ thử (chỉ test key)\n\nScope yêu cầu: orders:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/testmode/reset.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Reset test data\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "POST /v1/testmode/seed",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Có thể bắt buộc theo operation/chủ key. Retry cùng lệnh thì giữ nguyên giá trị."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/testmode/seed",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "seed"
              ]
            },
            "description": "Tạo dữ liệu mẫu chế độ thử (chỉ test key)\n\nScope yêu cầu: orders:write\n\nHeader Idempotency-Key được khai theo yêu cầu operation/chủ key (đã set sẵn {{$guid}} — retry cùng ý định thì GIỮ NGUYÊN giá trị cũ).\n\nChữ ký HMAC do pre-request script của collection tự tính cho POST /v1/testmode/seed."
          }
        },
        {
          "name": "GET /v1/testmode/status",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/testmode/status",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "testmode",
                "status"
              ]
            },
            "description": "Tổng quan chế độ thử (chỉ test key)\n\nScope yêu cầu: esims:read\n\nChữ ký HMAC do pre-request script của collection tự tính cho GET /v1/testmode/status."
          }
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Tổng Kho eSIM — ký HMAC-SHA256 cho mọi request /v1 (tài liệu §4.2).",
          "// signing_string = method \\n path?query \\n timestamp \\n nonce \\n SHA256(raw_body)",
          "// Chạy bằng CryptoJS có sẵn của Postman — không cài thêm gì.",
          "const keyId = pm.variables.get(\"key_id\");",
          "const secret = pm.variables.get(\"secret\");",
          "if (!keyId || !secret) {",
          "  throw new Error(\"Thiếu biến key_id / secret. Đặt trong Variables của collection (chỉ dùng key tke_test_ / tke_member_test_).\");",
          "}",
          "if (!keyId.startsWith(\"tke_test_\") && !keyId.startsWith(\"tke_member_test_\")) throw new Error(\"Test keys only: tke_test_ / tke_member_test_.\");",
          "",
          "const method = pm.request.method.toUpperCase();",
          "// URL sau khi thay biến, bỏ scheme+host → còn đúng path (kèm query) mà server ký lại.",
          "const resolvedUrl = pm.variables.replaceIn(pm.request.url.toString());",
          "if (!/^https:\\/\\/api-testing\\.tongkhoesim\\.com(?::443)?\\//i.test(resolvedUrl) && !/^https?:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?\\//i.test(resolvedUrl)) throw new Error(\"Test host only.\");",
          "const pathWithQuery = resolvedUrl.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\/[^/]+/, \"\");",
          "",
          "let rawBody = \"\";",
          "if (pm.request.body && pm.request.body.mode === \"raw\" && pm.request.body.raw) {",
          "  rawBody = pm.variables.replaceIn(pm.request.body.raw);",
          "}",
          "",
          "const timestamp = String(Date.now());",
          "const nonce = pm.variables.replaceIn(\"{{$guid}}\");",
          "const bodyHash = CryptoJS.SHA256(rawBody).toString(CryptoJS.enc.Hex);",
          "const signingString = [method, pathWithQuery, timestamp, nonce, bodyHash].join(\"\\n\");",
          "const signature = CryptoJS.HmacSHA256(signingString, secret).toString(CryptoJS.enc.Hex);",
          "",
          "// Chỉ gửi chữ ký — secret KHÔNG BAO GIỜ rời khỏi máy bạn.",
          "pm.request.headers.upsert({ key: \"X-TKE-Key\", value: keyId });",
          "pm.request.headers.upsert({ key: \"X-TKE-Timestamp\", value: timestamp });",
          "pm.request.headers.upsert({ key: \"X-TKE-Nonce\", value: nonce });",
          "pm.request.headers.upsert({ key: \"X-TKE-Signature\", value: signature });"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://api-testing.tongkhoesim.com",
      "type": "string"
    },
    {
      "key": "key_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "secret",
      "value": "",
      "type": "secret"
    }
  ]
}
