{
  "openapi": "3.1.0",
  "info": {
    "title": "API Publik Jual-Akun.com",
    "version": "1.0.0",
    "description": "Integrasi server-to-server untuk katalog lokal, marketplace reseller dan supplier yang dipublikasikan di Jual-Akun.com. API metadata menyediakan judul, deskripsi teks, URL gambar, kategori dan durasi garansi. Webhook menyediakan harga, stok, status aktif/ketersediaan dan pemberitahuan ID+metadata_version; ambil metadata melalui API hanya saat diperlukan, tanpa polling katalog. Simpan ID produk sebagai string, hingga 8999999999999999, per sumber+ID dan versi terpisah. API juga membaca akun/kategori, membeli dengan saldo dan mengambil hasil pesanan. Buat pembelian hanya setelah pembayaran pelanggan terkonfirmasi lunas. PAID tidak menjamin item selesai; processing bukan gagal. Jangan membeli ulang atau refund otomatis hanya karena timeout/processing. Simpan key dan item di backend."
  },
  "servers": [{"url": "/api/v1/public", "description": "Origin yang menyajikan spesifikasi ini"}],
  "security": [{"PublicApiKey": []}],
  "tags": [{"name": "Akun"}, {"name": "Metadata katalog"}, {"name": "Pesanan"}, {"name": "Webhook katalog"}],
  "x-rate-limits": {
    "read": {"accountRequests": 60, "ipRequests": 120, "windowSeconds": 60, "sharedAcross": ["/profile.php", "/categories.php", "/orders.php", "/products.php", "/product.php"]},
    "buy": {"accountRequests": 10, "ipRequests": 30, "windowSeconds": 60, "sharedAcross": ["/buy_product.php"]},
    "description": "Kuota baca dan beli terpisah. Batas IP berlaku sebelum autentikasi dan mencakup gabungan akun pada IP yang sama. Retry dan request valid-key yang gagal validasi memakai kuota. Mengganti key tidak mereset kuota akun. Ikuti Retry-After saat HTTP 429."
  },
  "x-web-settings-rate-limits": {
    "description": "Tindakan melalui halaman dokumentasi, bukan endpoint API-key. Memerlukan sesi login, CSRF, dan pemeriksaan 2FA. Setiap tindakan memiliki penghitung per pengguna yang terpisah.",
    "generateKey": {"requests": 6, "windowSeconds": 60},
    "saveWebhook": {"requests": 3, "windowSeconds": 600},
    "disableWebhook": {"requests": 6, "windowSeconds": 60},
    "resendSnapshot": {"requests": 1, "windowSeconds": 600, "description": "Webhook harus aktif dan terverifikasi, tanpa pengiriman/snapshot sebelumnya yang masih berjalan."}
  },
  "paths": {
    "/profile.php": {"get": {
      "operationId": "readPublicProfile", "tags": ["Akun"], "summary": "Membaca profil dan saldo pemilik key",
      "description": "Tidak memerlukan parameter query. Semua endpoint GET berbagi kuota baca 60/menit per akun dan 120/menit per IP.",
      "responses": {
        "200": {"description": "Profil akun", "headers": {"X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}, "X-RateLimit-Remaining": {"$ref": "#/components/headers/RateRemaining"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProfileResponse"}, "examples": {"profile": {"$ref": "#/components/examples/Profile"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "429": {"$ref": "#/components/responses/RateLimited"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }},
    "/categories.php": {"get": {
      "operationId": "listPublicCategories", "tags": ["Akun"], "summary": "Membaca kategori",
      "description": "Tidak memerlukan parameter query. data adalah array, termasuk array kosong. Harga/stok/ketersediaan produk diterima melalui webhook. Berbagi kuota baca dengan seluruh endpoint GET.",
      "responses": {
        "200": {"description": "Daftar kategori", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CategoriesResponse"}, "examples": {"categories": {"$ref": "#/components/examples/Categories"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "429": {"$ref": "#/components/responses/RateLimited"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }},
    "/products.php": {"get": {
      "operationId": "listPublicCatalogMetadata", "tags": ["Metadata katalog"], "summary": "Membaca metadata katalog dengan cursor atau batch ID",
      "x-allowed-query-parameters": ["limit", "after_id", "ids"],
      "x-unknown-query-parameter-status": 422,
      "description": "Hanya judul/deskripsi teks/URL gambar/kategori/garansi dan metadata_version, tanpa harga/stok/status. Data disiapkan worker setelah webhook terverifikasi. Gunakan ids untuk hingga 50 ID dari snapshot atau metadata_changes dalam satu request. Jangan menggabungkan ids dengan limit/after_id. Alternatif daftar awal memakai cursor after_id, bukan page/offset. Daftar tidak menentukan produk aktif atau stok; gunakan webhook untuk keputusan tampilan. Terapkan metadata jika versinya tidak lebih lama, dan retry dengan backoff bila lebih rendah dari metadata_version yang diumumkan. missing_ids berarti metadata tidak tersedia: jangan membuat produk aktif berdasarkan API metadata atau menghapus produk sumber lain. Jangan polling atau meminta API setiap kunjungan pembeli. Berbagi kuota seluruh GET 60/akun/menit dan 120/IP/menit.",
      "parameters": [
        {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}, "description": "Ukuran halaman cursor; tidak boleh bersama ids."},
        {"name": "after_id", "in": "query", "required": false, "schema": {"type": "string", "pattern": "^(?:0|[1-9][0-9]{0,14}|[1-8][0-9]{15})$", "default": "0"}, "description": "ID terakhir dari meta.next_after_id; 0 sampai 8999999999999999. Tidak boleh bersama ids."},
        {"name": "ids", "in": "query", "required": false, "schema": {"type": "string", "maxLength": 849, "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})(?:,(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})){0,49}$"}, "example": "101,102", "description": "1–50 ID positif, maksimal 8999999999999999, dipisahkan koma tanpa spasi. Duplikat dihapus. Tidak boleh bersama limit/after_id."}
      ],
      "responses": {
        "200": {"description": "Metadata tanpa data komersial", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MetadataListResponse"}, "examples": {"cursor": {"$ref": "#/components/examples/MetadataList"}, "batch": {"$ref": "#/components/examples/MetadataBatch"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "422": {"$ref": "#/components/responses/MetadataValidation"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/MetadataUnavailable"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }},
    "/product.php": {"get": {
      "operationId": "readPublicProductMetadata", "tags": ["Metadata katalog"], "summary": "Membaca metadata satu produk",
      "x-allowed-query-parameters": ["product"],
      "x-unknown-query-parameter-status": 422,
      "description": "Membaca metadata terbaru yang sudah diproyeksikan, bukan harga/stok/status. Hanya parameter product diterima; parameter lain ditolak 422. Saat pembangunan awal belum lengkap, 503 dengan Retry-After, bukan data awal setengah jadi. Bandingkan data.metadata_version dengan versi yang diumumkan webhook. 404 berarti metadata tidak tersedia/produk tidak aktif; jangan gunakan data ini untuk menimpa status webhook atau membeli ulang. Berbagi kuota seluruh GET.",
      "parameters": [{"name": "product", "in": "query", "required": true, "schema": {"type": "string", "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})$"}, "example": "101", "description": "ID positif dari webhook, maksimal 8999999999999999."}],
      "responses": {
        "200": {"description": "Metadata satu produk", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MetadataDetailResponse"}, "examples": {"product": {"$ref": "#/components/examples/MetadataDetail"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"$ref": "#/components/responses/MetadataValidation"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/MetadataUnavailable"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }},
    "/buy_product.php": {"post": {
      "operationId": "createPublicPurchase", "tags": ["Pesanan"], "summary": "Membeli produk menggunakan saldo",
      "description": "Memotong saldo dan membuat pesanan. Batas 10 request/menit per akun dan 30/menit per IP. invoice_external unik per akun; retry dengan invoice, produk dan jumlah yang sama mengambil transaksi yang sama. max_total wajib untuk pembelian baru dan membatasi total IDR, bukan harga satu item. Jangan mengganti invoice karena timeout. success=true hanya menyatakan request berhasil diproses; periksa payment_status dan fulfillment_status sebelum mengirim item ke pelanggan.",
      "requestBody": {"required": true, "content": {
        "application/json": {"schema": {"$ref": "#/components/schemas/PurchaseRequest"}, "examples": {"purchase": {"$ref": "#/components/examples/PurchaseRequest"}}},
        "application/x-www-form-urlencoded": {"schema": {"$ref": "#/components/schemas/PurchaseRequest"}},
        "multipart/form-data": {"schema": {"$ref": "#/components/schemas/PurchaseRequest"}}
      }},
      "responses": {
        "200": {"description": "Pesanan dibuat atau transaksi yang sama ditemukan untuk retry. Item dapat sudah selesai atau masih processing.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderResponse"}, "examples": {"completed": {"$ref": "#/components/examples/PurchaseCompleted"}, "processing": {"$ref": "#/components/examples/PurchaseProcessing"}, "retry": {"$ref": "#/components/examples/PurchaseRetry"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"},
        "409": {"description": "Harga/produk berubah atau invoice bertentangan. PRICE_LIMIT_EXCEEDED tidak memotong saldo. Jangan mengubah harga/invoice otomatis tanpa persetujuan pelanggan.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiError"}, "examples": {"price": {"$ref": "#/components/examples/PriceLimitError"}, "invoice": {"$ref": "#/components/examples/InvoiceConflict"}}}}},
        "422": {"description": "Parameter, saldo, kupon atau stok tidak memenuhi. Perbaiki penyebab sebelum retry.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiError"}, "examples": {"validation": {"$ref": "#/components/examples/ValidationError"}, "balance": {"$ref": "#/components/examples/BalanceError"}, "quantity": {"$ref": "#/components/examples/QuantityError"}}}}},
        "429": {"$ref": "#/components/responses/RateLimited"}, "500": {"$ref": "#/components/responses/UncertainError"}, "503": {"$ref": "#/components/responses/Unavailable"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }},
    "/orders.php": {"get": {
      "operationId": "readPublicOrder", "tags": ["Pesanan"], "summary": "Membaca status dan item pesanan milik akun",
      "description": "trans_id, status dan items berada di tingkat utama respons, bukan di dalam data. data pada respons ini adalah array teks item. Gunakan trans_id dari pembelian, bukan invoice. PAID+processing berarti tunggu/periksa; jangan langsung membeli ulang atau refund. Periksa bertahap, misalnya 5, 10, 20 lalu 30 detik, dengan batas waktu operasional Anda; jika terlalu lama hubungi admin. Hentikan pemeriksaan otomatis pada completed atau status pembayaran akhir. Webhook katalog tidak mengirim status order. Jangan menafsirkan status yang tidak dikenal sebagai sukses atau gagal.",
      "parameters": [{"name": "order", "in": "query", "required": true, "description": "trans_id dari respons pembelian. Bukan invoice Jual-Akun.com atau invoice_external.", "schema": {"type": "string", "minLength": 1}, "example": "API-202610-EXAMPLE0001"}],
      "responses": {
        "200": {"description": "Status dan item pesanan", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OrderResponse"}, "examples": {"completed": {"$ref": "#/components/examples/OrderCompleted"}, "processing": {"$ref": "#/components/examples/OrderProcessing"}, "refunded": {"$ref": "#/components/examples/OrderRefunded"}}}}},
        "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"$ref": "#/components/responses/Error"}, "429": {"$ref": "#/components/responses/RateLimited"}, "default": {"$ref": "#/components/responses/Error"}
      }
    }}
  },
  "webhooks": {
    "publicCatalogEvent": {"post": {
      "operationId": "receivePublicCatalogEvent", "tags": ["Webhook katalog"], "summary": "Menerima verifikasi, snapshot dan pembaruan katalog",
      "description": "URL penerima diatur melalui halaman dokumentasi. Wajib HTTPS publik port 443 dengan sertifikat valid; tanpa query, fragment, kredensial, redirect, atau jaringan internal. Verifikasi Ed25519, SHA256 raw body, subscription/event ID dan timestamp selisih maksimal 5 menit. Simpan inbox permanen sebelum ACK; deduplikasi event_id dan proses sequence per subscription. Snapshot berisi harga/stok/status dan referensi metadata_version, bukan teks metadata. Terapkan snapshot setelah complete=true, hanya pada sumber ini. Ambil metadata melalui API batch untuk ID yang belum ada/versinya berubah. products dan metadata_changes memiliki versi independen: abaikan state version lama tanpa membuang pemberitahuan metadata yang lebih baru. catalog.updated boleh products kosong saat hanya metadata berubah, atau metadata_changes kosong saat hanya stok/harga berubah. Stok absolut. Maksimal 50 ID unik dan 512 KiB per pesan. ACK JSON maksimal 8 KiB dalam 4 detik; pengambilan metadata dilakukan worker penerima setelah ACK, bukan sebelum ACK. Retry mempertahankan event/body/sequence dengan timestamp signature baru. Setelah 12 kegagalan webhook dijeda. Jangan aktifkan produk sebelum metadata dan state tersedia, dan jangan menimpa metadata baru oleh respons API lama. Tidak memuat credentials pembelian.",
      "security": [{"WebhookSignature": []}],
      "parameters": [
        {"name": "X-SiJual-Public-Subscription", "in": "header", "required": true, "schema": {"type": "string", "format": "uuid"}},
        {"name": "X-SiJual-Public-Event-Id", "in": "header", "required": true, "schema": {"type": "string", "format": "uuid"}},
        {"name": "X-SiJual-Public-Timestamp", "in": "header", "required": true, "schema": {"type": "string", "pattern": "^[0-9]+$"}},
        {"name": "X-SiJual-Public-Content-SHA256", "in": "header", "required": true, "schema": {"type": "string", "pattern": "^[a-f0-9]{64}$"}}
      ],
      "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookEvent"}, "examples": {"verify": {"$ref": "#/components/examples/WebhookVerify"}, "snapshot": {"$ref": "#/components/examples/CatalogSnapshot"}, "updated": {"$ref": "#/components/examples/CatalogUpdated"}, "metadata": {"$ref": "#/components/examples/CatalogMetadataChanged"}}}}},
      "responses": {
        "200": {"description": "ACK setelah signature valid dan event tersimpan. webhook.verify wajib mengembalikan challenge yang sama.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookAck"}, "examples": {"verify": {"$ref": "#/components/examples/VerifyAck"}, "catalog": {"$ref": "#/components/examples/CatalogAck"}}}}},
        "400": {"description": "Event tidak valid; tidak di-ACK."}, "401": {"description": "Signature, timestamp atau subscription tidak valid."}, "409": {"description": "Event/sequence yang sama memiliki isi berbeda."}, "413": {"description": "Payload terlalu besar."}, "503": {"description": "Inbox belum berhasil disimpan; pengirim akan retry."}
      }
    }}
  },
  "components": {
    "securitySchemes": {
      "PublicApiKey": {"type": "apiKey", "in": "header", "name": "X-Api-Key", "description": "Key rahasia dari tombol Generate API Key. Simpan hanya di backend. Tidak diterima dari query URL."},
      "WebhookSignature": {"type": "apiKey", "in": "header", "name": "X-SiJual-Public-Signature", "description": "Bukan token statis: signature Ed25519 base64 unik per pengiriman, diverifikasi dengan public key base64 dari halaman dokumentasi. String canonical terdiri dari lima baris berurutan: sijual-public-v1, SUBSCRIPTION_ID, UNIX_TIMESTAMP, EVENT_ID, SHA256_RAW_BODY. Gabungkan dengan LF (byte 0x0A), tanpa newline tambahan di akhir."}
    },
    "headers": {
      "RateLimit": {"description": "Batas limiter yang menghasilkan header ini.", "schema": {"type": "integer", "minimum": 1}},
      "RateRemaining": {"description": "Sisa kuota limiter.", "schema": {"type": "integer", "minimum": 0}},
      "RetryAfter": {"description": "Detik yang harus ditunggu sebelum mencoba lagi.", "schema": {"type": "integer", "minimum": 1}}
    },
    "responses": {
      "Unauthorized": {"description": "Key tidak valid/kedaluwarsa; periksa key, jangan retry berulang tanpa perbaikan.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiError"}, "examples": {"unauthorized": {"$ref": "#/components/examples/Unauthorized"}}}}},
      "Forbidden": {"description": "Akun pemilik key tidak aktif atau akses ditolak.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}},
      "NotFound": {"description": "Produk atau order tidak ditemukan untuk akun tersebut.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiError"}, "examples": {"missing": {"$ref": "#/components/examples/NotFound"}}}}},
      "RateLimited": {"description": "Kuota IP atau akun tercapai. Tunggu Retry-After dan gunakan backoff; jangan mengganti key untuk melewati batas.", "headers": {"Retry-After": {"$ref": "#/components/headers/RetryAfter"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}, "X-RateLimit-Remaining": {"$ref": "#/components/headers/RateRemaining"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}, "examples": {"account": {"$ref": "#/components/examples/RateLimitError"}, "ip": {"$ref": "#/components/examples/FrameworkRateLimitError"}}}}},
      "UncertainError": {"description": "Gangguan server/jaringan tidak membuktikan transaksi gagal. Rekonsiliasi melalui retry invoice_external yang sama; jangan membuat invoice baru atau refund otomatis.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}, "examples": {"server": {"$ref": "#/components/examples/ServerError"}}}}},
      "Unavailable": {"description": "Layanan pembelian belum tersedia; hentikan pembuatan order baru dan hubungi admin.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}, "examples": {"unavailable": {"$ref": "#/components/examples/Unavailable"}}}}},
      "MetadataUnavailable": {"description": "Pembangunan awal metadata belum selesai, data belum dapat dibaca, atau layanan tidak aktif. API tidak mengembalikan daftar awal setengah jadi sebagai katalog lengkap. Pastikan webhook terverifikasi, tunggu snapshot awal dan retry dengan backoff sesuai Retry-After; hubungi admin bila berlanjut. Jangan menghapus katalog atau menafsirkan ini sebagai gagal transaksi.", "headers": {"Retry-After": {"$ref": "#/components/headers/RetryAfter"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiError"}, "examples": {"notReady": {"$ref": "#/components/examples/MetadataUnavailable"}, "readError": {"$ref": "#/components/examples/MetadataReadError"}}}}},
      "MetadataValidation": {"description": "Parameter di luar daftar atau nilai parameter tidak valid. Perbaiki parameter sesuai endpoint; jangan memakai page/offset karena API menggunakan after_id. Tidak ada pembelian/saldo yang diproses.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}, "examples": {"unsupported": {"$ref": "#/components/examples/UnsupportedMetadataParameters"}}}}},
      "Error": {"description": "Periksa kode HTTP dan message. Beberapa kesalahan framework/routing hanya memiliki message, tanpa status/success. Error jaringan/5xx bukan bukti pembelian gagal.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}
    },
    "schemas": {
      "PurchaseRequest": {
        "type": "object", "required": ["id", "max_total", "invoice_external"],
        "properties": {
          "action": {"type": "string", "enum": ["buyProduct"], "description": "Opsional; jika dikirim harus buyProduct."},
          "id": {"oneOf": [{"type": "integer", "minimum": 1, "maximum": 8999999999999999, "format": "int64"}, {"type": "string", "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})$"}], "description": "ID produk dari webhook, termasuk ID marketplace. Simpan/kirim sebagai string; batas 1 sampai 8999999999999999."},
          "amount": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 1, "description": "Opsional, default 1. Batas produk tertentu dapat lebih rendah."},
          "max_total": {"type": "number", "exclusiveMinimum": 0, "maximum": 999999999999.99, "multipleOf": 0.01, "description": "Total IDR maksimal yang disetujui untuk seluruh quantity. Maksimal 12 digit sebelum desimal dan 2 digit sesudahnya; tanpa pemisah ribuan. Wajib saat membuat pembelian baru; tetap kirim saat retry."},
          "invoice_external": {"type": "string", "minLength": 1, "maxLength": 191, "pattern": "\\S", "description": "ID pesanan toko Anda, unik per akun. Panjang dihitung setelah trim. Gunakan nilai yang sama untuk retry pesanan yang sama."},
          "coupon": {"type": "string", "maxLength": 64, "x-maxUtf8Bytes": 64, "description": "Opsional. Maksimal 64 byte UTF-8 setelah trim (64 karakter bila semuanya ASCII)."}
        }
      },
      "ProfileResponse": {"type": "object", "required": ["status", "success", "msg", "message", "data"], "properties": {
        "status": {"const": "success"}, "success": {"const": true}, "msg": {"type": "string"}, "message": {"type": "string"},
        "data": {"type": "object", "required": ["user_id", "username", "email", "phone", "balance", "is_active", "created_at"], "properties": {
          "user_id": {"type": "integer"}, "username": {"type": ["string", "null"]}, "email": {"type": ["string", "null"]}, "phone": {"type": ["string", "null"]}, "balance": {"type": "number", "description": "Saldo akun dalam IDR."}, "is_active": {"type": "boolean"}, "created_at": {"type": ["string", "null"], "format": "date-time"}
        }}
      }},
      "CategoriesResponse": {"type": "object", "required": ["status", "success", "msg", "message", "data"], "properties": {
        "status": {"const": "success"}, "success": {"const": true}, "msg": {"type": "string"}, "message": {"type": "string"},
        "data": {"type": "array", "items": {"type": "object", "required": ["id", "name", "slug", "product_count"], "properties": {"id": {"type": "integer"}, "name": {"type": ["string", "null"]}, "slug": {"type": "string"}, "product_count": {"type": "integer", "minimum": 0}}}}
      }},
      "OrderResponse": {"type": "object", "required": ["status", "success", "msg", "message", "trans_id", "invoice", "invoice_external", "external_order_id", "product_id", "product_title", "product_name", "product_type", "quantity", "unit_price", "total_price", "payment_method", "payment_name", "payment_status", "fulfillment_status", "paid_at", "created_at", "pre_order", "data", "items"], "properties": {
        "status": {"const": "success"}, "success": {"const": true}, "msg": {"type": "string"}, "message": {"type": "string"},
        "trans_id": {"type": "string", "description": "ID opaque untuk query orders.php; simpan/kirim persis seperti respons tanpa mengasumsikan prefix atau format."}, "invoice": {"type": "string"}, "invoice_external": {"type": ["string", "null"]}, "external_order_id": {"type": ["string", "null"], "description": "Nilai yang sama dengan invoice_external."},
        "product_id": {"type": "integer"}, "product_title": {"type": ["string", "null"]}, "product_name": {"type": ["string", "null"]}, "product_type": {"type": "string", "enum": ["ready_stock", "pre_order"]}, "quantity": {"type": "integer", "minimum": 1},
        "unit_price": {"type": "number"}, "total_price": {"type": "number", "description": "Harga snapshot transaksi dalam IDR, bukan harga katalog terkini."}, "payment_method": {"type": ["string", "null"]}, "payment_name": {"type": ["string", "null"]},
        "payment_status": {"type": "string", "description": "Nilai umum: UNPAID, PAID, EXPIRED, FAILED, REFUNDED. Nilai yang tidak dikenal harus diperiksa, bukan diasumsikan sukses/gagal.", "examples": ["UNPAID", "PAID", "EXPIRED", "FAILED", "REFUNDED"]},
        "fulfillment_status": {"type": "string", "enum": ["pending_payment", "processing", "completed"], "description": "completed berarti PAID dan item lengkap. processing berarti belum selesai, bukan gagal. pending_payment dipakai untuk pembayaran selain PAID; tetap periksa payment_status untuk status akhir."},
        "paid_at": {"type": ["string", "null"], "format": "date-time"}, "created_at": {"type": ["string", "null"], "format": "date-time"},
        "pre_order": {"oneOf": [{"type": "null"}, {"type": "object", "required": ["is_ready", "message", "detail_items", "item_count"], "properties": {"is_ready": {"type": "boolean"}, "message": {"type": "string"}, "detail_items": {"type": ["string", "null"]}, "item_count": {"type": "integer", "minimum": 0}}}]},
        "data": {"type": "array", "description": "Teks item; kosong jika pembayaran/fulfillment belum lengkap. Bukan envelope metadata order.", "items": {"type": "string"}},
        "items": {"type": "array", "description": "Detail item dan item pengganti garansi. Jumlah elemen dapat melebihi quantity karena penggantian. Jangan log credentials.", "items": {"type": "object", "required": ["content"], "properties": {"content": {"type": "string"}, "transaction_detail_id": {"type": "integer"}, "product_detail_id": {"type": "integer"}, "marketplace_order_item_id": {"type": "integer"}, "pre_order_item_id": {"type": "integer"}, "pre_order_id": {"type": "integer"}, "source_item_id": {"type": "string"}, "sequence": {"type": "integer"}}}},
        "idempotent": {"type": "boolean", "description": "Dapat disertakan pada respons retry transaksi yang sudah ada."}
      }},
      "ApiError": {"type": "object", "required": ["status", "success", "msg", "message"], "properties": {"status": {"const": "error"}, "success": {"const": false}, "msg": {"type": "string"}, "message": {"type": "string"}, "error_code": {"type": "string"}, "required_balance": {"type": "number"}, "balance": {"type": "number"}}},
      "FrameworkError": {"type": "object", "required": ["message"], "properties": {"message": {"type": "string"}}},
      "ErrorResponse": {"anyOf": [{"$ref": "#/components/schemas/ApiError"}, {"$ref": "#/components/schemas/FrameworkError"}]},
      "CatalogMetadata": {"type": "object", "additionalProperties": false, "required": ["id", "metadata_version", "title", "description", "image_url", "categories", "warranty_days", "max_quantity"], "properties": {
        "id": {"type": "string", "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})$"}, "metadata_version": {"type": "string", "pattern": "^[1-9][0-9]*$"},
        "title": {"type": "string", "maxLength": 500}, "description": {"type": "string", "maxLength": 12000}, "image_url": {"type": ["string", "null"], "format": "uri", "maxLength": 2000, "description": "URL HTTP(S) absolut tanpa username/password, atau null bila gambar tidak tersedia atau URL tidak valid. URL terlalu panjang tidak dipotong."},
        "categories": {"type": "array", "maxItems": 20, "items": {"type": "object", "required": ["id", "name"], "properties": {"id": {"type": "string", "maxLength": 64}, "name": {"type": "string", "maxLength": 250}}}},
        "warranty_days": {"type": "integer", "minimum": 0, "description": "Durasi garansi, bukan API untuk mengajukan klaim."},
        "max_quantity": {"type": "integer", "minimum": 1, "maximum": 1000, "description": "Batas teknis jumlah per transaksi untuk produk ini, bukan stok. Ready stock juga dibatasi stok dari webhook; pre-order tidak dibatasi stok. Pembelian tetap divalidasi server."}
      }, "description": "Metadata teks tanpa harga/stok/status. Escape teks saat ditampilkan dan batasi pengambilan URL gambar sesuai kebijakan media aplikasi. Bandingkan metadata_version secara numerik; perubahan stok tidak mengubah versi ini."},
      "MetadataDetailResponse": {"type": "object", "required": ["status", "success", "msg", "message", "data"], "properties": {"status": {"const": "success"}, "success": {"const": true}, "msg": {"type": "string"}, "message": {"type": "string"}, "data": {"$ref": "#/components/schemas/CatalogMetadata"}}},
      "MetadataListResponse": {"type": "object", "required": ["status", "success", "msg", "message", "data", "meta"], "properties": {"status": {"const": "success"}, "success": {"const": true}, "msg": {"type": "string"}, "message": {"type": "string"}, "data": {"type": "array", "maxItems": 50, "items": {"$ref": "#/components/schemas/CatalogMetadata"}}, "meta": {"oneOf": [{"type": "object", "additionalProperties": false, "required": ["limit", "has_more", "next_after_id"], "properties": {"limit": {"type": "integer", "minimum": 1, "maximum": 50}, "has_more": {"type": "boolean"}, "next_after_id": {"type": ["string", "null"]}}}, {"type": "object", "additionalProperties": false, "required": ["missing_ids"], "properties": {"missing_ids": {"type": "array", "maxItems": 50, "items": {"type": "string"}}}}]}}},
      "MetadataChange": {"type": "object", "additionalProperties": false, "required": ["id", "metadata_version"], "properties": {"id": {"type": "string", "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})$"}, "metadata_version": {"type": "string", "pattern": "^[1-9][0-9]*$"}}},
      "WebhookProduct": {"type": "object", "additionalProperties": false, "required": ["id", "version", "metadata_version", "is_active", "is_available", "status", "stock", "price", "currency"], "properties": {
        "id": {"type": "string", "pattern": "^(?:[1-9][0-9]{0,14}|[1-8][0-9]{15})$"}, "version": {"type": "string", "pattern": "^[1-9][0-9]*$"}, "metadata_version": {"type": "string", "pattern": "^[0-9]+$"}, "is_active": {"type": "boolean"}, "is_available": {"type": "boolean"}, "status": {"type": "string", "enum": ["active", "inactive"]}, "stock": {"type": "integer", "minimum": 0}, "price": {"type": ["number", "null"], "minimum": 0}, "currency": {"const": "IDR"},
        "product_type": {"type": "string", "enum": ["ready_stock", "pre_order"]}
      }, "description": "State harga/stok/status saja. Produk nonaktif stock=0, price=null, is_available=false. version adalah versi state, terpisah dari metadata_version. Jangan menghapus produk lokal saat menerima tombstone/snapshot."},
      "WebhookEnvelope": {"type": "object", "required": ["schema_version", "subscription_id", "event_id", "sequence", "type", "occurred_at", "data"], "properties": {"schema_version": {"const": 1}, "subscription_id": {"type": "string", "format": "uuid"}, "event_id": {"type": "string", "format": "uuid"}, "sequence": {"type": "string", "pattern": "^[1-9][0-9]{0,18}$"}, "type": {"type": "string"}, "occurred_at": {"type": "string", "format": "date-time"}, "data": {"type": "object"}}},
      "VerifyEvent": {"allOf": [{"$ref": "#/components/schemas/WebhookEnvelope"}, {"properties": {"type": {"const": "webhook.verify"}, "data": {"type": "object", "required": ["challenge"], "properties": {"challenge": {"type": "string", "minLength": 1}}}}}]},
      "SnapshotEvent": {"allOf": [{"$ref": "#/components/schemas/WebhookEnvelope"}, {"properties": {"type": {"const": "catalog.snapshot"}, "data": {"type": "object", "required": ["snapshot_id", "complete", "products", "metadata_changes"], "properties": {"snapshot_id": {"type": "string", "format": "uuid"}, "complete": {"type": "boolean"}, "products": {"type": "array", "maxItems": 50, "items": {"$ref": "#/components/schemas/WebhookProduct"}}, "metadata_changes": {"type": "array", "maxItems": 50, "items": {"$ref": "#/components/schemas/MetadataChange"}}}}}}]},
      "UpdatedEvent": {"allOf": [{"$ref": "#/components/schemas/WebhookEnvelope"}, {"properties": {"type": {"const": "catalog.updated"}, "data": {"type": "object", "required": ["from_version", "to_version", "products", "metadata_changes"], "properties": {"from_version": {"type": "string", "pattern": "^[0-9]+$"}, "to_version": {"type": "string", "pattern": "^[0-9]+$"}, "products": {"type": "array", "maxItems": 50, "items": {"$ref": "#/components/schemas/WebhookProduct"}}, "metadata_changes": {"type": "array", "maxItems": 50, "items": {"$ref": "#/components/schemas/MetadataChange"}}}}}}]},
      "WebhookEvent": {"oneOf": [{"$ref": "#/components/schemas/VerifyEvent"}, {"$ref": "#/components/schemas/SnapshotEvent"}, {"$ref": "#/components/schemas/UpdatedEvent"}]},
      "WebhookAck": {"type": "object", "required": ["received", "event_id"], "properties": {"received": {"const": true}, "event_id": {"type": "string", "format": "uuid"}, "challenge": {"type": "string", "description": "Wajib sama dengan challenge pada event webhook.verify."}}}
    },
    "examples": {
      "PurchaseRequest": {"summary": "Request pembelian baru", "value": {"action": "buyProduct", "id": 101, "amount": 1, "max_total": 25000, "invoice_external": "TOKO-ORDER-000123"}},
      "Profile": {"summary": "200 — profil", "value": {"status": "success", "success": true, "msg": "Profile fetched successfully.", "message": "Profile fetched successfully.", "data": {"user_id": 5, "username": "contoh_user", "email": "buyer@example.com", "phone": "6281234567890", "balance": 150000, "is_active": true, "created_at": "2026-10-13T10:00:00+07:00"}}},
      "Categories": {"summary": "200 — kategori", "value": {"status": "success", "success": true, "msg": "Categories fetched successfully.", "message": "Categories fetched successfully.", "data": [{"id": 8, "name": "Digital", "slug": "digital", "product_count": 12}]}},
      "PurchaseCompleted": {"summary": "200 — pembelian dan item selesai", "value": {"status": "success", "success": true, "msg": "Order created successfully.", "message": "Order created successfully.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "PAID", "fulfillment_status": "completed", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": ["Email : buyer@example.com\nPassword : CONTOH-BUKAN-AKUN-ASLI"], "items": [{"transaction_detail_id": 9001, "product_detail_id": 12001, "content": "Email : buyer@example.com\nPassword : CONTOH-BUKAN-AKUN-ASLI"}]}},
      "PurchaseProcessing": {"summary": "200 — pembelian sudah dibayar, item masih diproses", "value": {"status": "success", "success": true, "msg": "Order created successfully.", "message": "Order created successfully.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "PAID", "fulfillment_status": "processing", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": [], "items": []}},
      "PurchaseRetry": {"summary": "200 — retry invoice yang sudah ada", "value": {"status": "success", "success": true, "msg": "Order already exists for this invoice_external.", "message": "Order already exists for this invoice_external.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "PAID", "fulfillment_status": "processing", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": [], "items": [], "idempotent": true}},
      "OrderCompleted": {"summary": "200 — hasil order selesai", "value": {"status": "success", "success": true, "msg": "Order fetched successfully.", "message": "Order fetched successfully.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "PAID", "fulfillment_status": "completed", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": ["Email : buyer@example.com\nPassword : CONTOH-BUKAN-AKUN-ASLI"], "items": [{"transaction_detail_id": 9001, "product_detail_id": 12001, "content": "Email : buyer@example.com\nPassword : CONTOH-BUKAN-AKUN-ASLI"}]}},
      "OrderProcessing": {"summary": "200 — hasil order masih diproses", "value": {"status": "success", "success": true, "msg": "Order fetched successfully.", "message": "Order fetched successfully.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "PAID", "fulfillment_status": "processing", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": [], "items": []}},
      "OrderRefunded": {"summary": "200 — pembayaran sudah direfund oleh server", "value": {"status": "success", "success": true, "msg": "Order fetched successfully.", "message": "Order fetched successfully.", "trans_id": "API-202610-EXAMPLE0001", "invoice": "INV-EXAMPLE-0001", "invoice_external": "TOKO-ORDER-000123", "external_order_id": "TOKO-ORDER-000123", "product_id": 101, "product_title": "Contoh produk digital", "product_name": "Contoh produk digital", "product_type": "ready_stock", "quantity": 1, "unit_price": 25000, "total_price": 25000, "payment_method": "saldo", "payment_name": "Saldo", "payment_status": "REFUNDED", "fulfillment_status": "pending_payment", "paid_at": "2026-10-13T10:00:00+07:00", "created_at": "2026-10-13T10:00:00+07:00", "pre_order": null, "data": [], "items": []}},
      "Unauthorized": {"summary": "401 — key tidak valid", "value": {"status": "error", "success": false, "msg": "API key tidak valid atau sudah kedaluwarsa.", "message": "API key tidak valid atau sudah kedaluwarsa."}},
      "NotFound": {"summary": "404 — order tidak dimiliki akun/tidak ditemukan", "value": {"status": "error", "success": false, "msg": "Order tidak ditemukan untuk API key ini.", "message": "Order tidak ditemukan untuk API key ini."}},
      "ValidationError": {"summary": "422 — parameter pembelian tidak lengkap", "value": {"status": "error", "success": false, "msg": "invoice_external wajib diisi agar retry pembelian tidak memotong saldo dua kali.", "message": "invoice_external wajib diisi agar retry pembelian tidak memotong saldo dua kali."}},
      "BalanceError": {"summary": "422 — saldo tidak cukup", "value": {"status": "error", "success": false, "msg": "Saldo tidak mencukupi.", "message": "Saldo tidak mencukupi.", "required_balance": 25000, "balance": 10000}},
      "QuantityError": {"summary": "422 — quantity melewati batas", "value": {"status": "error", "success": false, "msg": "Jumlah pembelian API maksimal 1000 per transaksi.", "message": "Jumlah pembelian API maksimal 1000 per transaksi.", "error_code": "QUANTITY_LIMIT_EXCEEDED", "max_quantity": 1000}},
      "PriceLimitError": {"summary": "409 — harga melampaui persetujuan pelanggan", "value": {"status": "error", "success": false, "msg": "Harga melebihi max_total. Tunggu pembaruan webhook sebelum mengonfirmasi harga baru.", "message": "Harga melebihi max_total. Tunggu pembaruan webhook sebelum mengonfirmasi harga baru.", "error_code": "PRICE_LIMIT_EXCEEDED"}},
      "InvoiceConflict": {"summary": "409 — invoice digunakan untuk detail berbeda", "value": {"status": "error", "success": false, "msg": "invoice_external sudah pernah digunakan untuk order dengan detail yang berbeda. Gunakan invoice_external baru untuk order baru, atau kirim ulang detail yang sama untuk retry.", "message": "invoice_external sudah pernah digunakan untuk order dengan detail yang berbeda. Gunakan invoice_external baru untuk order baru, atau kirim ulang detail yang sama untuk retry.", "error_code": "INVOICE_EXTERNAL_CONFLICT", "existing_order": {"trans_id": "API-202610-EXAMPLE0001", "product_id": 101, "quantity": 1, "total_price": 25000}, "requested_order": {"product_id": 102, "quantity": 1, "total_price": null}}},
      "RateLimitError": {"summary": "429 — kuota akun; contoh Retry-After: 60", "value": {"status": "error", "success": false, "msg": "Terlalu banyak request. Silakan coba lagi setelah 60 detik.", "message": "Terlalu banyak request. Silakan coba lagi setelah 60 detik."}},
      "FrameworkRateLimitError": {"summary": "429 — kuota IP/framework", "value": {"message": "Too Many Attempts."}},
      "ServerError": {"summary": "500 — rekonsiliasi dengan invoice yang sama", "value": {"status": "error", "success": false, "msg": "Gagal memproses transaksi. Silakan coba beberapa saat lagi.", "message": "Gagal memproses transaksi. Silakan coba beberapa saat lagi."}},
      "Unavailable": {"summary": "503 — layanan pembelian belum tersedia", "value": {"status": "error", "success": false, "msg": "Pembelian API belum siap. Hubungi admin agar penyimpanan invoice eksternal diaktifkan.", "message": "Pembelian API belum siap. Hubungi admin agar penyimpanan invoice eksternal diaktifkan.", "error_code": "PUBLIC_API_NOT_READY"}},
      "WebhookVerify": {"summary": "Verifikasi kepemilikan endpoint", "value": {"schema_version": 1, "subscription_id": "11111111-1111-4111-8111-111111111111", "event_id": "22222222-2222-4222-8222-222222222222", "sequence": "1", "type": "webhook.verify", "occurred_at": "2026-10-13T10:00:00+07:00", "data": {"challenge": "challenge-contoh-balas-dengan-nilai-yang-sama"}}},
      "MetadataList": {"summary": "200 — halaman metadata dengan cursor", "value": {"status": "success", "success": true, "msg": "Catalog metadata fetched successfully.", "message": "Catalog metadata fetched successfully.", "data": [{"id": "101", "title": "Contoh produk digital", "description": "Deskripsi produk", "image_url": "https://jual-akun.example.test/images/contoh.webp", "categories": [{"id": "8", "name": "Digital"}], "warranty_days": 1, "max_quantity": 1000, "metadata_version": "40"}], "meta": {"limit": 1, "has_more": true, "next_after_id": "101"}}},
      "MetadataBatch": {"summary": "200 — batch ids=101,999", "value": {"status": "success", "success": true, "msg": "Catalog metadata fetched successfully.", "message": "Catalog metadata fetched successfully.", "data": [{"id": "101", "title": "Contoh produk digital", "description": "Deskripsi produk", "image_url": "https://jual-akun.example.test/images/contoh.webp", "categories": [{"id": "8", "name": "Digital"}], "warranty_days": 1, "max_quantity": 1000, "metadata_version": "40"}], "meta": {"missing_ids": ["999"]}}},
      "MetadataDetail": {"summary": "200 — metadata satu produk", "value": {"status": "success", "success": true, "msg": "Product metadata fetched successfully.", "message": "Product metadata fetched successfully.", "data": {"id": "101", "title": "Contoh produk digital", "description": "Deskripsi produk", "image_url": "https://jual-akun.example.test/images/contoh.webp", "categories": [{"id": "8", "name": "Digital"}], "warranty_days": 1, "max_quantity": 1000, "metadata_version": "40"}}},
      "MetadataUnavailable": {"summary": "503 — metadata belum siap", "value": {"status": "error", "success": false, "msg": "Metadata katalog belum siap. Pastikan webhook terverifikasi dan tunggu snapshot awal.", "message": "Metadata katalog belum siap. Pastikan webhook terverifikasi dan tunggu snapshot awal."}},
      "UnsupportedMetadataParameters": {"summary": "422 — parameter metadata tidak didukung", "value": {"status": "error", "success": false, "msg": "Parameter query tidak didukung. Gunakan parameter metadata sesuai dokumentasi.", "message": "Parameter query tidak didukung. Gunakan parameter metadata sesuai dokumentasi."}},
      "MetadataReadError": {"summary": "503 — metadata belum dapat dibaca", "value": {"status": "error", "success": false, "msg": "Metadata katalog belum dapat dibaca. Silakan coba kembali atau hubungi admin.", "message": "Metadata katalog belum dapat dibaca. Silakan coba kembali atau hubungi admin."}},
      "CatalogSnapshot": {"summary": "Snapshot harga/stok/status dan referensi metadata", "value": {"schema_version": 1, "subscription_id": "11111111-1111-4111-8111-111111111111", "event_id": "33333333-3333-4333-8333-333333333333", "sequence": "2", "type": "catalog.snapshot", "occurred_at": "2026-10-13T10:00:00+07:00", "data": {"snapshot_id": "44444444-4444-4444-8444-444444444444", "complete": true, "products": [{"id": "101", "version": "40", "metadata_version": "40", "price": 25000, "currency": "IDR", "stock": 12, "is_active": true, "status": "active", "is_available": true, "product_type": "ready_stock"}], "metadata_changes": [{"id": "101", "metadata_version": "40"}]}}},
      "CatalogUpdated": {"summary": "Pembaruan stok saja", "value": {"schema_version": 1, "subscription_id": "11111111-1111-4111-8111-111111111111", "event_id": "55555555-5555-4555-8555-555555555555", "sequence": "3", "type": "catalog.updated", "occurred_at": "2026-10-13T10:00:00+07:00", "data": {"from_version": "40", "to_version": "43", "products": [{"id": "101", "version": "43", "metadata_version": "40", "price": 25000, "currency": "IDR", "stock": 10, "is_active": true, "status": "active", "is_available": true, "product_type": "ready_stock"}], "metadata_changes": []}}},
      "CatalogMetadataChanged": {"summary": "Pemberitahuan metadata berubah tanpa mengirim teks", "value": {"schema_version": 1, "subscription_id": "11111111-1111-4111-8111-111111111111", "event_id": "66666666-6666-4666-8666-666666666666", "sequence": "4", "type": "catalog.updated", "occurred_at": "2026-10-13T10:00:00+07:00", "data": {"from_version": "43", "to_version": "44", "products": [], "metadata_changes": [{"id": "101", "metadata_version": "44"}]}}},
      "VerifyAck": {"summary": "ACK verifikasi", "value": {"received": true, "event_id": "22222222-2222-4222-8222-222222222222", "challenge": "challenge-contoh-balas-dengan-nilai-yang-sama"}},
      "CatalogAck": {"summary": "ACK katalog", "value": {"received": true, "event_id": "33333333-3333-4333-8333-333333333333"}}
    }
  }
}
