{
  "openapi": "3.1.0",
  "info": {
    "title": "Rajacek API — Dokumentasi Member",
    "version": "1.0.0",
    "description": "API validasi rekening bank, e-wallet, nomor WhatsApp, dan ID pelanggan PLN Indonesia untuk member Rajacek.\n\n**Endpoint:** satu endpoint `GET /api/v3/validate` (kunci lewat query `api_key`) dan `POST /api/v3/validate` (kunci lewat header `X-API-Key` / `Authorization: Bearer`, atau field body `api_key`). Kunci lama `rv_live_...` tetap berlaku; kunci baru diawali `rc_...`. Jaga kerahasiaan kunci Anda; kunci hanya ditampilkan sekali saat dibuat. Bila lebih dari satu sumber kunci dikirim dengan nilai berbeda, request ditolak 401.\n\n**Idempotency-Key** hanya berlaku untuk POST: nilai unik 8–200 karakter per operasi bisnis. Bila tidak dikirim atau kosong, server membuatkan kunci unik otomatis dan mengembalikannya di header respons `Idempotency-Key`. Mengirim ulang request yang sama dengan kunci yang sama akan memutar respons yang sama tanpa memotong kuota dua kali. GET tidak idempotent: setiap GET adalah cek baru.\n\n**Kode produk (`code`):** kode bank 3 digit (mis. `002` BRI, `014` BCA) — ambil daftar lengkapnya dari `GET /v1/banks`; slug e-wallet (`dana`, `gopay`, `gopay_driver`, `ovo`, `shopeepay`, `linkaja`, `astrapay`, `isaku`, `kaspro`); `pln`; atau `whatsapp`. `code` harus berupa string — angka `2` bukan `\"002\"` dan ditolak 400. Kode internal provider tidak pernah dipakai di sisi member.\n\n**Hasil validasi** ada di field `status` pada respons 200: `valid`, `invalid`, `not_found`, `not_kyc`, atau `pending`. Error teknis memakai format `ApiError` dengan HTTP 4xx/5xx.\n\n**Rate limit** bawaan 60 request/menit per kunci (header `X-RateLimit-Remaining`/`X-RateLimit-Reset` dikembalikan); kuota paket berkurang untuk hasil `valid` dan `not_kyc` (1 hit), serta `invalid`/`not_found` di luar jatah gratis 100 pertama per hari per kunci (WIB)."
  },
  "servers": [
    {
      "url": "/",
      "description": "Server Rajacek ini"
    }
  ],
  "tags": [
    {
      "name": "Validation",
      "description": "Endpoint validasi untuk member"
    },
    {
      "name": "Reference",
      "description": "Data referensi publik"
    },
    {
      "name": "System",
      "description": "Status layanan"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/health/live": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Cek layanan hidup",
        "security": [],
        "responses": {
          "200": {
            "description": "Layanan hidup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "tags": [
          "System"
        ],
        "security": [],
        "operationId": "getPublicStatusPage",
        "summary": "Halaman status layanan publik (HTML)",
        "description": "Halaman status tanpa login. Hanya label produk dan status agregat operational/degraded, plus banner aktif. Tidak menyebut nama, URL, atau kode provider upstream.",
        "responses": {
          "200": {
            "description": "Halaman status layanan.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "tags": [
          "System"
        ],
        "security": [],
        "operationId": "getPublicStatus",
        "summary": "Status layanan publik (JSON)",
        "description": "Data yang sama dengan GET /status, dalam JSON, tanpa login.",
        "responses": {
          "200": {
            "description": "Status agregat layanan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicStatus"
                }
              }
            }
          }
        }
      }
    },
    "/v1/banks": {
      "get": {
        "tags": [
          "Reference"
        ],
        "summary": "Daftar kode bank 3 digit",
        "security": [],
        "description": "Mengembalikan seluruh bank yang didukung beserta kode 3 digit resminya. Gunakan `code_bank` sebagai `bank_code` pada validasi rekening.",
        "responses": {
          "200": {
            "description": "Daftar bank.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BankCode"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v3/validate": {
      "get": {
        "tags": [
          "Validation"
        ],
        "operationId": "validateV3Get",
        "summary": "Validasi lewat query string",
        "description": "Satu endpoint untuk semua produk. `code` memilih produk: kode bank 3 digit (mis. `002` BRI), slug e-wallet (`dana`, `gopay`, `gopay_driver`, `ovo`, `shopeepay`, `linkaja`, `astrapay`, `isaku`, `kaspro`), `pln`, atau `whatsapp`; nomornya di `accountNumber`.\n\nGET tidak idempotent: setiap GET adalah cek baru. Kunci lewat query `api_key` atau header (`X-API-Key` / `Authorization: Bearer`); bila sumber berbeda nilainya, request ditolak 401.\n\nScope kunci yang diperlukan: scope produk yang sesuai atau `validate:*`.",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "required": true,
            "description": "Kode produk: kode bank 3 digit, slug e-wallet, `pln`, atau `whatsapp`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "accountNumber",
            "in": "query",
            "required": true,
            "description": "Nomor rekening / akun e-wallet / ID pelanggan PLN / nomor WhatsApp.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "api_key",
            "in": "query",
            "required": false,
            "description": "Kunci API member (boleh juga lewat header).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Validasi selesai diproses. Periksa field `status` untuk hasil bisnisnya.",
            "headers": {
              "X-Request-ID": {
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "description": "Detik sampai jendela limit berikutnya."
                }
              },
              "X-Idempotent-Replay": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Hadir hanya bila respons diputar dari idempotency cache."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak memenuhi kontrak (field hilang/format salah/kode tidak dikenal).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "API key tidak ada, tidak valid, atau sudah dicabut.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "402": {
            "description": "Kuota paket habis.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Kunci tidak memiliki scope produk ini.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit terlampaui atau antrean provider penuh. Tunggu sesuai `Retry-After`.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "502": {
            "description": "Provider validasi mengembalikan respons yang tidak dapat diproses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Provider sedang tidak tersedia (termasuk circuit breaker terbuka) atau endpoint belum dikonfigurasi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Validation"
        ],
        "operationId": "validateV3Post",
        "summary": "Validasi lewat body JSON (mendukung idempotensi)",
        "description": "Sama seperti GET, tetapi kunci lewat header atau field body `api_key`, dan mendukung `Idempotency-Key` (8–200 karakter ASCII tanpa spasi). `code` harus berupa string — angka `2` bukan `\"002\"` dan ditolak 400.\n\nScope kunci yang diperlukan: scope produk yang sesuai atau `validate:*`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/V3ValidateRequest"
              },
              "examples": {
                "bank": {
                  "value": {
                    "code": "002",
                    "accountNumber": "1234567890"
                  }
                },
                "ewallet": {
                  "value": {
                    "code": "gopay",
                    "accountNumber": "081234567890"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validasi selesai diproses. Periksa field `status` untuk hasil bisnisnya.",
            "headers": {
              "X-Request-ID": {
                "schema": {
                  "type": "string"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "description": "Detik sampai jendela limit berikutnya."
                }
              },
              "X-Idempotent-Replay": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Hadir hanya bila respons diputar dari idempotency cache."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Permintaan tidak memenuhi kontrak (field hilang/format salah/kode tidak dikenal).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "API key tidak ada, tidak valid, atau sudah dicabut.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "402": {
            "description": "Kuota paket habis.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Kunci tidak memiliki scope produk ini.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key yang sama sedang diproses atau bertabrakan dengan payload berbeda.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit terlampaui atau antrean provider penuh. Tunggu sesuai `Retry-After`.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "502": {
            "description": "Provider validasi mengembalikan respons yang tidak dapat diproses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Provider sedang tidak tersedia (termasuk circuit breaker terbuka) atau endpoint belum dikonfigurasi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Kunci member (kunci baru berprefix `rc_`; kunci lama `rv_live_` tetap berlaku). Alternatif: `Authorization: Bearer <kunci>`. Untuk `GET /api/v3/validate`, kunci juga bisa dikirim lewat query `api_key`."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Nilai unik 8-200 karakter ASCII per operasi bisnis. Gunakan nilai yang sama saat retry request yang sama. Opsional — bila kosong, server membuatkan kunci unik otomatis dan mengembalikannya di header respons Idempotency-Key.",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 200,
          "pattern": "^[!-~]+$"
        },
        "example": "pay-ord-20261021-8a3c2f11"
      },
      "RequestId": {
        "name": "X-Request-ID",
        "in": "header",
        "required": false,
        "description": "Korelasi opsional dari klien; server selalu mengembalikan request ID pada respons.",
        "schema": {
          "type": "string",
          "maxLength": 128
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "service",
          "timestamp",
          "provider_mode"
        ],
        "properties": {
          "status": {
            "type": "string",
            "example": "ok"
          },
          "service": {
            "type": "string",
            "example": "rajacek-api"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "provider_mode": {
            "type": "string",
            "enum": [
              "demo",
              "http"
            ]
          }
        }
      },
      "PublicProductStatus": {
        "type": "object",
        "required": [
          "product",
          "label",
          "status"
        ],
        "properties": {
          "product": {
            "type": "string",
            "enum": [
              "bank_account",
              "ewallet",
              "whatsapp",
              "pln"
            ]
          },
          "label": {
            "type": "string",
            "example": "Validasi Rekening"
          },
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded"
            ]
          }
        }
      },
      "PublicStatus": {
        "type": "object",
        "required": [
          "overall",
          "products",
          "maintenance_blocks_validations"
        ],
        "properties": {
          "overall": {
            "type": "string",
            "enum": [
              "operational",
              "degraded"
            ]
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicProductStatus"
            }
          },
          "announcement": {
            "type": [
              "string",
              "null"
            ]
          },
          "maintenance_banner": {
            "type": [
              "string",
              "null"
            ]
          },
          "maintenance_blocks_validations": {
            "type": "boolean"
          }
        }
      },
      "BankCode": {
        "type": "object",
        "required": [
          "code_bank",
          "name_bank"
        ],
        "properties": {
          "code_bank": {
            "type": "string",
            "pattern": "^[0-9]{3}$",
            "example": "002"
          },
          "name_bank": {
            "type": "string",
            "example": "BRI"
          }
        }
      },
      "ValidationStatus": {
        "type": "string",
        "description": "Billing: valid dan not_kyc memotong 1 hit; invalid dan not_found (satu pool bersama) gratis hingga 100 per hari per kunci (WIB), selebihnya 1 hit; pending, provider_error, rate_limited, dan quota_exhausted tidak memotong dan tidak memakan jatah gratis. Kunci bersaldo 0 tetap ditolak 402 sebelum cek berjalan.",
        "enum": [
          "valid",
          "invalid",
          "not_found",
          "not_kyc",
          "pending",
          "provider_error",
          "rate_limited",
          "quota_exhausted"
        ]
      },
      "ValidationResult": {
        "type": "object",
        "properties": {
          "account_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor rekening/e-wallet yang divalidasi (bank & e-wallet).",
            "example": "1234667890"
          },
          "phone_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor WhatsApp yang divalidasi (layanan whatsapp).",
            "example": "6281234567890"
          },
          "service_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kode layanan yang divalidasi (kode bank 3 digit, slug e-wallet, atau whatsapp).",
            "example": "014"
          },
          "bank_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama bank sesuai katalog (khusus layanan bank).",
            "example": "BCA"
          },
          "account_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "NAMA PELANGGAN"
          },
          "tarif": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tarif listrik pelanggan (khusus produk pln, hanya bila tersedia).",
            "example": "R1"
          },
          "daya": {
            "type": [
              "string",
              "null"
            ],
            "description": "Daya tersambung pelanggan dalam VA (khusus produk pln, hanya bila tersedia).",
            "example": "1300"
          },
          "registered": {
            "type": [
              "boolean",
              "null"
            ],
            "example": true
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "example": "ref-20261021-001"
          }
        }
      },
      "ValidationMeta": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string"
          },
          "provider_latency_ms": {
            "type": "integer",
            "format": "int64"
          },
          "rajacek_processing_ms": {
            "type": "integer",
            "format": "int64"
          },
          "total_latency_ms": {
            "type": "integer",
            "format": "int64"
          },
          "cached": {
            "type": "boolean"
          },
          "remaining_quota": {
            "type": "integer",
            "format": "int64"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Masa aktif paket yang sedang dipakai (kedaluwarsa paket yang paling cepat habis), sama dengan yang tampil di portal member. null berarti kunci tidak pernah kedaluwarsa. Bukan tanggal kunci mati: kunci tetap bisa dipakai selama masih ada paket aktif, dan tanggal ini bergeser otomatis saat paket yang sedang dipakai habis atau lewat masa aktifnya."
          }
        }
      },
      "V3ValidateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "accountNumber"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Kode produk: kode bank 3 digit (mis. 002 BRI), slug e-wallet (dana, gopay, gopay_driver, ovo, shopeepay, linkaja, astrapay, isaku, kaspro), pln, atau whatsapp. Harus string.",
            "example": "002"
          },
          "accountNumber": {
            "type": "string",
            "description": "Nomor rekening / akun e-wallet / ID pelanggan PLN / nomor WhatsApp.",
            "example": "1234567890"
          },
          "api_key": {
            "type": "string",
            "description": "Kunci API member (opsional bila sudah dikirim lewat header)."
          }
        }
      },
      "ValidationResponse": {
        "type": "object",
        "required": [
          "request_id",
          "status",
          "valid",
          "product",
          "message",
          "meta"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/ValidationStatus"
          },
          "valid": {
            "type": "boolean",
            "description": "true hanya bila status = valid."
          },
          "product": {
            "type": "string",
            "enum": [
              "bank_account",
              "ewallet",
              "whatsapp",
              "pln"
            ]
          },
          "message": {
            "type": "string"
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ValidationResult"
              },
              {
                "type": "null"
              }
            ]
          },
          "meta": {
            "$ref": "#/components/schemas/ValidationMeta"
          }
        },
        "example": {
          "request_id": "Kx7fA92pQm",
          "status": "valid",
          "valid": true,
          "product": "bank_account",
          "message": "Respons diterima dari provider validasi.",
          "result": {
            "account_number": "1234667890",
            "service_code": "014",
            "bank_name": "BCA",
            "account_name": "NAMA PELANGGAN",
            "registered": true,
            "reference": "ref-001"
          },
          "meta": {
            "provider": "Rajacek",
            "provider_latency_ms": 812,
            "rajacek_processing_ms": 6,
            "total_latency_ms": 818,
            "cached": false,
            "remaining_quota": 4999,
            "expires_at": "2027-01-21T10:00:00Z"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "code",
          "message",
          "request_id"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "bad_request"
          },
          "message": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "details": {
            "type": [
              "object",
              "null"
            ]
          }
        },
        "example": {
          "code": "unauthorized",
          "message": "API key tidak valid",
          "request_id": "450abf0e-1b63-43a9-9ee8-6a86834d9330"
        }
      }
    }
  }
}
