{
  "openapi": "3.1.0",
  "info": {
    "title": "TemanQRIS API",
    "version": "1.3.4",
    "summary": "QRIS dinamis, payment link, payment gateway otomatis, dan widget.",
    "description": "Spesifikasi API merchant TemanQRIS. Dokumentasi lengkap: https://temanqris.com/docs. Versi Markdown: https://temanqris.com/llms-full.txt\n\nUntuk AI agent:\n\n- Autentikasi lewat header X-API-Key, tidak pernah lewat URL. Key live 64 karakter heksadesimal, key sandbox tq_test_ + 48 karakter heksadesimal. Mulai dari key sandbox.\n- Alur gateway: GET /api/payment-gateway/status, lalu POST /api/payment-gateway/payments dengan header Idempotency-Key, lalu terima webhook payment.confirmed (HMAC-SHA256 dari raw body di X-TemanQRIS-Signature) atau polling GET /api/qris/orders/{orderId}.\n- Bercabang dengan field code (daftar lengkap di components.schemas.Error), bukan teks error.\n- 429 dengan Retry-After: tunggu sebanyak detik itu, lalu kirim ulang request yang sama. 5xx: ulangi dengan Idempotency-Key yang sama.\n- Panduan langkah demi langkah: https://temanqris.com/llms-full.txt",
    "contact": {
      "name": "TemanQRIS",
      "url": "https://temanqris.com/contact",
      "email": "support@temanqris.com"
    }
  },
  "servers": [
    {
      "url": "https://temanqris.com"
    }
  ],
  "externalDocs": {
    "description": "Dokumentasi API TemanQRIS",
    "url": "https://temanqris.com/docs"
  },
  "tags": [
    {
      "name": "QRIS"
    },
    {
      "name": "Payment Link"
    },
    {
      "name": "Payment Gateway"
    },
    {
      "name": "Widget"
    },
    {
      "name": "Akun"
    }
  ],
  "paths": {
    "/api/qris/upload": {
      "post": {
        "tags": [
          "QRIS"
        ],
        "summary": "Upload QRIS Statis",
        "operationId": "postUploadQrisStatis",
        "description": "Upload QRIS statis Anda sebagai template. Cukup sekali saat setup. Setelah itu langsung pakai /generate atau /payment-link tanpa upload ulang. Pakai /my-qris untuk mengecek apakah QRIS sudah ada.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#upload",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "qris_image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Gambar QRIS statis: JPG, PNG, GIF, BMP, atau TIFF. Maksimal 1,5 MB (bawaan), 6000 px, 16 megapiksel."
                  }
                },
                "required": [
                  "qris_image"
                ]
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "qris_string": {
                    "type": "string",
                    "description": "Isi QRIS statis hasil decode."
                  }
                },
                "required": [
                  "qris_string"
                ]
              },
              "example": {
                "qris_string": "00020101021126670016COM.NOBUBANK.WWW..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "message": "QRIS uploaded successfully",
                      "qris_string": "00020101021126...",
                      "merchant": {
                        "name": "TOKO EXAMPLE",
                        "city": "JAKARTA"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "Tidak ada file maupun string.",
                    "value": {
                      "error": "QRIS string or image is required"
                    }
                  },
                  "contoh_3": {
                    "summary": "Gambar tidak terbaca. Coba foto yang lebih jelas atau kirim string.",
                    "value": {
                      "error": "Could not decode QR code. Please try a clearer image or input manually."
                    }
                  },
                  "contoh_4": {
                    "summary": "Tipe file tidak didukung, misalnya WebP.",
                    "value": {
                      "error": "Only image files are allowed (jpeg, jpg, png, gif, bmp, tiff)",
                      "code": 400
                    }
                  },
                  "contoh_5": {
                    "summary": "File terlalu besar.",
                    "value": {
                      "error": "File too large. Max size is 1.5MB"
                    }
                  },
                  "contoh_6": {
                    "summary": "Lebih dari 6000 px atau 16 megapiksel.",
                    "value": {
                      "error": "Image dimensions are unsupported or too large"
                    }
                  },
                  "contoh_7": {
                    "summary": "Isi QR bukan QRIS yang valid.",
                    "value": {
                      "error": "Invalid QRIS format",
                      "details": [
                        "..."
                      ]
                    }
                  },
                  "contoh_8": {
                    "summary": "QRIS yang dikirim dinamis.",
                    "value": {
                      "error": "Please upload a static QRIS code. Dynamic QRIS codes cannot be used as templates."
                    }
                  },
                  "contoh_9": {
                    "summary": "Premium sudah mencapai batas QRIS paket (bawaan 100).",
                    "value": {
                      "error": "Merchant limit reached",
                      "message": "Your tier allows 100 merchants. Delete an existing QRIS first."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/my-qris": {
      "get": {
        "tags": [
          "QRIS"
        ],
        "summary": "Cek QRIS",
        "operationId": "getCekQris",
        "description": "Cek apakah Anda sudah pernah upload QRIS statis. Panggil sebelum /upload supaya tidak upload ulang tanpa perlu. qris adalah QRIS yang paling baru diperbarui, dan QRIS itulah yang dipakai /generate dan /payment-link kalau Anda tidak memilih qris_id.\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#my-qris",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "QRIS_sudah_ada_1": {
                    "summary": "QRIS sudah ada",
                    "value": {
                      "success": true,
                      "has_qris": true,
                      "qris": {
                        "id": 123,
                        "qris_string": "00020101021126...",
                        "merchant_name": "TOKO EXAMPLE",
                        "merchant_city": "JAKARTA",
                        "created_at": "2026-10-01T03:30:00.000Z",
                        "updated_at": "2026-10-01T03:30:00.000Z"
                      },
                      "qris_list": [
                        {
                          "id": 123,
                          "qris_string": "00020101021126...",
                          "merchant_name": "TOKO EXAMPLE",
                          "merchant_city": "JAKARTA",
                          "created_at": "2026-10-01T03:30:00.000Z",
                          "updated_at": "2026-10-01T03:30:00.000Z"
                        }
                      ],
                      "total": 1,
                      "default_qris_id": 123
                    }
                  },
                  "belum_ada_QRIS_2": {
                    "summary": "belum ada QRIS",
                    "value": {
                      "success": true,
                      "has_qris": false,
                      "message": "No QRIS found. Please upload your static QRIS first (only needed once)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "401 Unauthorized",
                    "value": {
                      "error": "Invalid API key"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/generate": {
      "post": {
        "tags": [
          "QRIS"
        ],
        "summary": "Generate QRIS Dinamis",
        "operationId": "postGenerateQrisDinamis",
        "description": "Endpoint utama untuk membuat QRIS dinamis dengan nominal transaksi. Setiap panggilan juga membuat (atau memperbarui) payment link untuk order_id itu.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#generate",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "description": "Nominal dalam rupiah, bilangan bulat 1 sampai 100.000.000. Angka dalam string seperti \"50000\" juga diterima."
                  },
                  "fee_type": {
                    "type": "string",
                    "description": "\"rupiah\" atau \"percent\". Wajib kalau fee_value bukan 0."
                  },
                  "fee_value": {
                    "type": "number",
                    "description": "Biaya tambahan yang dibayar pelanggan. percent: 0 sampai 100, maksimal 4 desimal, dibulatkan ke rupiah terdekat. rupiah: bilangan bulat, maksimal 10 kali amount."
                  },
                  "qris_id": {
                    "type": "integer",
                    "description": "ID QRIS tertentu dari my-qris. Bawaan: QRIS yang paling baru diperbarui."
                  },
                  "order_id": {
                    "type": "string",
                    "description": "ID order Anda, maksimal 30 karakter. Kalau kosong dibuat otomatis (ORD-A + 12 karakter)."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL penerima webhook. Harus mengarah ke alamat publik."
                  },
                  "callback_url": {
                    "type": "string",
                    "description": "URL http(s) tujuan pelanggan setelah menekan Sudah Bayar."
                  }
                },
                "required": [
                  "amount"
                ]
              },
              "example": {
                "amount": 50000,
                "fee_type": "rupiah",
                "fee_value": 1000,
                "order_id": "INV-2026-001",
                "webhook_url": "https://yoursite.com/webhook",
                "callback_url": "https://yoursite.com/thank-you"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "qris": "00020101021226...",
                      "qr_image": "data:image/png;base64,...",
                      "amount": 50000,
                      "fee": {
                        "type": "rupiah",
                        "value": 1000
                      },
                      "fee_amount": 1000,
                      "total_amount": 51000,
                      "expires_at": "2026-10-18T03:00:00.000Z",
                      "payment_link": {
                        "id": 456,
                        "link_code": "BTN3EV2XK7QP",
                        "order_id": "INV-2026-001",
                        "url": "/p/BTN3EV2XK7QP",
                        "amount": 50000,
                        "fee_type": "rupiah",
                        "fee_value": 1000,
                        "fee_amount": 1000,
                        "total_amount": 51000,
                        "merchant_name": "TOKO EXAMPLE",
                        "webhook_url": "https://yoursite.com/webhook",
                        "callback_url": "https://yoursite.com/thank-you",
                        "expires_at": "2026-10-18T03:00:00.000Z"
                      }
                    }
                  },
                  "sudah_lunas_2": {
                    "summary": "sudah lunas",
                    "description": "Order paid dikembalikan dengan existing: true. qris dan qr_image bernilai null karena tidak ada lagi yang perlu dipindai. Order awaiting_confirmation tetap membawa keduanya.",
                    "value": {
                      "success": true,
                      "existing": true,
                      "status": "paid",
                      "message": "This order_id is already paid. The stored order is returned unchanged.",
                      "qris": null,
                      "qr_image": null,
                      "amount": 50000,
                      "fee": {
                        "type": "rupiah",
                        "value": 1000
                      },
                      "fee_amount": 1000,
                      "total_amount": 51000,
                      "expires_at": "2026-10-18T03:00:00.000Z",
                      "payment_link": {
                        "id": 456,
                        "link_code": "BTN3EV2XK7QP",
                        "order_id": "INV-2026-001",
                        "url": "/p/BTN3EV2XK7QP",
                        "amount": 50000,
                        "total_amount": 51000,
                        "status": "paid",
                        "paid_at": "2026-10-17T03:12:40.000Z",
                        "existing": true,
                        "...": "..."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "409 Conflict",
                    "value": {
                      "error": "Order ID sudah dibayar atau menunggu konfirmasi",
                      "message": "Order ID 'INV-2026-001' sudah memiliki pembayaran yang nominalnya tidak bisa diubah. Gunakan Order ID baru untuk nominal berbeda.",
                      "existing_link_code": "BTN3EV2XK7QP"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/render": {
      "post": {
        "tags": [
          "QRIS"
        ],
        "summary": "Render QR",
        "operationId": "postRenderQr",
        "description": "Buat gambar QR dari string QRIS. Kirim salah satu: qris_string atau qris_data_id (ID QRIS tersimpan dari /my-qris).\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#render",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "qris_string": {
                    "type": "string",
                    "description": "String QRIS. Kirim ini atau qris_data_id."
                  },
                  "qris_data_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "ID QRIS tersimpan dari /api/qris/my-qris. Kirim ini atau qris_string."
                  }
                }
              },
              "example": {
                "qris_string": "00020101021126..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "qr_image": "data:image/png;base64,iVBOR..."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "QRIS string or qris_data_id required"
                    }
                  },
                  "contoh_3": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "qris_string must be a string"
                    }
                  },
                  "contoh_4": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "qris_data_id must be a positive integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_5": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "QRIS data not found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/payment-link": {
      "post": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Buat Payment Link",
        "operationId": "postBuatPaymentLink",
        "description": "Buat link pembayaran manual yang bisa dibagikan ke pelanggan lewat WhatsApp, email, atau media sosial.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#payment-link-create",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "description": "Bilangan bulat 1 sampai 100.000.000."
                  },
                  "description": {
                    "type": "string",
                    "description": "Deskripsi, misalnya \"Order #123\". Maksimal 255 karakter."
                  },
                  "order_id": {
                    "type": "string",
                    "description": "ID order Anda, maksimal 30 karakter, harus unik di akun Anda. Kalau kosong dibuat otomatis."
                  },
                  "fee_type": {
                    "type": "string",
                    "description": "\"rupiah\" atau \"percent\"."
                  },
                  "fee_value": {
                    "type": "number",
                    "description": "Biaya tambahan. percent maksimal 100."
                  },
                  "qris_data_id": {
                    "type": "integer",
                    "description": "ID QRIS tertentu dari my-qris."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "HTTPS (di production), alamat publik."
                  },
                  "callback_url": {
                    "type": "string",
                    "description": "URL http(s) tujuan pelanggan setelah menekan Sudah Bayar."
                  }
                },
                "required": [
                  "amount"
                ]
              },
              "example": {
                "amount": 75000,
                "description": "Pembayaran Order #12345",
                "order_id": "INV-2026-001",
                "webhook_url": "https://yoursite.com/webhook",
                "callback_url": "https://yoursite.com/thank-you"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "payment_link": {
                        "id": 456,
                        "link_code": "ABC12345XYZW",
                        "order_id": "INV-2026-001",
                        "url": "/p/ABC12345XYZW",
                        "amount": 75000,
                        "fee_type": "none",
                        "fee_value": 0,
                        "fee_amount": 0,
                        "total_amount": 75000,
                        "description": "Pembayaran Order #12345",
                        "merchant_name": "TOKO EXAMPLE",
                        "webhook_url": "https://yoursite.com/webhook",
                        "callback_url": "https://yoursite.com/thank-you",
                        "expires_at": "2026-10-18T07:00:00.000Z",
                        "qr_image": "data:image/png;base64,iVBOR..."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Order ID already exists",
                      "message": "Order ID 'INV-2026-001' is already used. Please use a unique order_id.",
                      "existing_link_code": "ABC12345XYZW"
                    }
                  },
                  "contoh_3": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "No active QRIS found. Please upload a static QRIS first."
                    }
                  },
                  "contoh_4": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Webhook URL must use HTTPS"
                    }
                  },
                  "contoh_5": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Invalid webhook URL format"
                    }
                  },
                  "contoh_6": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Amount is required and must be a whole number between 1 and 100000000"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/payment-links": {
      "get": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Daftar Payment Link",
        "operationId": "getDaftarPaymentLink",
        "description": "Ambil daftar payment link Anda, terbaru lebih dulu.\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#payment-links",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "Halaman."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            },
            "description": "Jumlah per halaman, maksimal 1000."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Cari di link_code, order_id, atau nama merchant (maksimal 100 karakter)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "payment_links": [
                        {
                          "id": 456,
                          "order_id": "INV-2026-001",
                          "link_code": "ABC12345XYZW",
                          "url": "/p/ABC12345XYZW",
                          "amount": 75000,
                          "fee_type": "none",
                          "fee_value": 0,
                          "fee_amount": 0,
                          "total_amount": 75000,
                          "description": "Order #12345",
                          "merchant_name": "TOKO EXAMPLE",
                          "payment_mode": "manual",
                          "is_active": 1,
                          "status": "pending",
                          "view_count": 5,
                          "expires_at": "2026-10-18T07:00:00.000Z",
                          "created_at": "2026-10-17T07:00:00.000Z",
                          "is_expired": false
                        },
                        {
                          "id": 457,
                          "order_id": "INV-2026-0001",
                          "link_code": "aZ3kQ9mP2xLw",
                          "url": "/p/aZ3kQ9mP2xLw",
                          "amount": 150000,
                          "fee_type": "rupiah",
                          "fee_value": 0,
                          "fee_amount": 321,
                          "total_amount": 150321,
                          "provider_fee_amount": 1200,
                          "net_amount": 148800,
                          "description": "2 item, Toko Kopi Senja",
                          "merchant_name": null,
                          "payment_mode": "gateway",
                          "is_active": 1,
                          "status": "paid",
                          "view_count": 2,
                          "expires_at": "2026-10-17T03:15:00.000Z",
                          "created_at": "2026-10-17T03:00:00.000Z",
                          "is_expired": false
                        }
                      ],
                      "pagination": {
                        "page": 1,
                        "limit": 20,
                        "total": 25,
                        "totalPages": 2
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "401 Unauthorized",
                    "value": {
                      "error": "Invalid API key"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/orders/{orderId}": {
      "get": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Cek Status Order",
        "operationId": "getCekStatusOrder",
        "description": "Cek status satu order, manual maupun gateway. Ini juga cara membaca status pembayaran gateway live (tidak ada endpoint baca terpisah untuk gateway live).\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#check-order",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "manual_1": {
                    "summary": "manual",
                    "value": {
                      "success": true,
                      "order": {
                        "order_id": "ORD-ABC123",
                        "title": "Pembayaran Invoice #001",
                        "description": "Pembayaran QRIS",
                        "amount": 50000,
                        "fee_type": "rupiah",
                        "fee_value": 1000,
                        "total_amount": 51000,
                        "status": "paid",
                        "payment_method": "qris",
                        "created_at": "2026-10-17T03:30:00.000Z",
                        "paid_at": "2026-10-17T03:35:00.000Z",
                        "expires_at": "2026-10-18T03:30:00.000Z",
                        "is_paid": true,
                        "is_expired": false
                      }
                    }
                  },
                  "gateway_2": {
                    "summary": "gateway",
                    "value": {
                      "success": true,
                      "order": {
                        "order_id": "INV-2026-0001",
                        "title": "Pesanan INV-2026-0001",
                        "description": "2 item, Toko Kopi Senja",
                        "amount": 150000,
                        "fee_type": "rupiah",
                        "fee_value": 0,
                        "total_amount": 150321,
                        "provider_fee_amount": 1200,
                        "net_amount": 148800,
                        "status": "paid",
                        "payment_method": "gateway",
                        "created_at": "2026-10-17T03:00:00.000Z",
                        "paid_at": "2026-10-17T03:08:12.000Z",
                        "expires_at": "2026-10-17T03:15:00.000Z",
                        "payable_until": null,
                        "is_paid": true,
                        "is_expired": false
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "404 Not Found",
                    "value": {
                      "success": false,
                      "error": "Order not found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/orders": {
      "get": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Daftar Order",
        "operationId": "getDaftarOrder",
        "description": "Order gateway di daftar ini juga membawa provider_fee_amount dan net_amount.\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#list-orders",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "awaiting_confirmation",
                "paid",
                "expired",
                "cancelled"
              ]
            },
            "description": "pending, awaiting_confirmation, paid, expired, atau cancelled. Nilai lain ditolak 400."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Maksimal 100."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Untuk halaman berikutnya."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "orders": [
                        {
                          "order_id": "ORD-ABC123",
                          "title": "Invoice #001",
                          "amount": 50000,
                          "total_amount": 51000,
                          "status": "paid",
                          "is_paid": true,
                          "created_at": "2026-10-17T03:30:00.000Z",
                          "paid_at": "2026-10-17T03:35:00.000Z"
                        }
                      ],
                      "pagination": {
                        "total": 25,
                        "limit": 10,
                        "offset": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "status must be one of: pending, awaiting_confirmation, paid, expired, cancelled"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/orders/{orderId}/verify": {
      "post": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Verifikasi Order",
        "operationId": "postVerifikasiOrder",
        "description": "Tandai order manual sebagai paid lewat API, tanpa membuka dashboard. Panggil hanya setelah dana benar-benar terlihat di rekening atau e-wallet Anda.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#verify-order",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "order_id Anda, misalnya INV-2026-001."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payer_name": {
                    "type": "string",
                    "description": "Nama pembayar, maksimal 100 karakter."
                  },
                  "payer_note": {
                    "type": "string",
                    "description": "Catatan, maksimal 500 karakter."
                  }
                }
              },
              "example": {
                "payer_name": "John Doe",
                "payer_note": "Pembayaran via OVO"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "message": "Payment verified successfully",
                      "order": {
                        "order_id": "INV-2026-001",
                        "amount": 75000,
                        "status": "paid",
                        "paid_at": "2026-10-17T09:30:00.000Z",
                        "confirmed_by": "api"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "Order already verified",
                      "message": "This order has already been marked as paid."
                    }
                  },
                  "contoh_4": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "Order expired",
                      "message": "Cannot verify an expired order."
                    }
                  },
                  "contoh_5": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "Order cancelled",
                      "message": "Cannot verify a cancelled order."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "success": false,
                      "error": "Order not found",
                      "message": "No order found with order_id \"INV-999\" for your account."
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_6": {
                    "summary": "409 Conflict",
                    "value": {
                      "success": false,
                      "error": "Gateway order requires automatic settlement reconciliation",
                      "message": "Manual verification is disabled for payment gateway orders."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/orders/{orderId}/cancel": {
      "post": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Batalkan Order",
        "operationId": "postBatalkanOrder",
        "description": "Batalkan order yang belum dibayar. Order pending, awaiting_confirmation, dan expired bisa dibatalkan. Link langsung tidak bisa dibayar lagi lewat halaman bayar.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#cancel-order",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "message": "Order cancelled successfully",
                      "order": {
                        "order_id": "INV-2026-001",
                        "status": "cancelled"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "Cannot cancel paid order",
                      "message": "This order has already been verified as paid and cannot be cancelled."
                    }
                  },
                  "contoh_3": {
                    "summary": "400 Bad Request",
                    "value": {
                      "success": false,
                      "error": "Order already cancelled",
                      "message": "This order has already been cancelled."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_4": {
                    "summary": "404 Not Found",
                    "value": {
                      "success": false,
                      "error": "Order not found",
                      "message": "..."
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_5": {
                    "summary": "409 Conflict",
                    "value": {
                      "success": false,
                      "error": "Order state changed",
                      "message": "Order could not be cancelled because its status is now \"paid\"."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/payment-link/{id}": {
      "delete": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Arsipkan Payment Link",
        "operationId": "deleteArsipkanPaymentLink",
        "description": "Arsipkan link yang tidak dipakai lagi. Link dinonaktifkan dan statusnya menjadi cancelled (atau tetap expired). Data tetap tersimpan untuk riwayat. :id adalah payment_link.id numerik dari response, bukan link_code.\n\nCatatan: memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#payment-link-delete",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "message": "Payment link archived"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Payment link not found or unauthorized"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "409 Conflict",
                    "value": {
                      "error": "Paid or confirmed payment links cannot be removed"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_harian_habis": {
                    "summary": "Kuota harian habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily limit of 50 requests",
                      "tier": "free",
                      "limit": 50,
                      "used": 50,
                      "resetTime": "Midnight (server time)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/pay/{link_code}/confirm": {
      "post": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Pelanggan Sudah Bayar, publik",
        "operationId": "postPelangganSudahBayarPublik",
        "description": "Mencatat bahwa pelanggan mengaku sudah membayar. Status berubah menjadi awaiting_confirmation dan Anda perlu memverifikasi.\n\nCatatan: publik, tanpa API key, 30 request per menit per IP.\n\nDokumentasi: https://temanqris.com/docs#payment-confirm",
        "security": [],
        "parameters": [
          {
            "name": "link_code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Kode payment link, misalnya ABC12345XYZW."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "message": "Konfirmasi pembayaran berhasil dikirim",
                      "order_id": "ORD-ABC12345XYZ",
                      "status": "awaiting_confirmation"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "examples": {
                  "link_gateway_2": {
                    "summary": "link gateway",
                    "value": {
                      "success": true,
                      "payment_mode": "gateway",
                      "message": "Pembayaran gateway diverifikasi otomatis. Tombol konfirmasi manual tidak diperlukan.",
                      "status": "pending"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_4": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Pembayaran sudah dikonfirmasi, mohon tunggu verifikasi merchant."
                    }
                  },
                  "contoh_5": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Link pembayaran sudah kedaluwarsa. Minta link baru ke merchant."
                    }
                  },
                  "contoh_6": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Pesanan sudah cancelled. Tidak dapat dikonfirmasi."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Link pembayaran tidak ditemukan"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/pay/{link_code}": {
      "get": {
        "tags": [
          "Payment Link"
        ],
        "summary": "Status Link, publik",
        "operationId": "getStatusLinkPublik",
        "description": "Data yang dipakai halaman bayar. Berguna kalau Anda membuat halaman bayar sendiri dan ingin menampilkan QR serta status ke pelanggan. Response tidak pernah memuat biaya layanan gateway atau data internal.\n\nCatatan: publik, tanpa API key, 120 request per menit per IP.\n\nDokumentasi: https://temanqris.com/docs#pay-status",
        "security": [],
        "parameters": [
          {
            "name": "link_code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "merchant_name": "TOKO EXAMPLE",
                      "amount": 75000,
                      "base_amount": 75000,
                      "fee_amount": 0,
                      "fee_type": "none",
                      "description": "Pembayaran Order #12345",
                      "qr_image": "data:image/png;base64,iVBOR...",
                      "expires_at": "2026-10-18T07:00:00.000Z",
                      "order_id": "INV-2026-001",
                      "branding": "Powered by TemanQRIS",
                      "callback_url": "https://yoursite.com/thank-you",
                      "payment_mode": "manual",
                      "unique_code": null,
                      "manual_confirmation_allowed": true,
                      "status": "pending",
                      "is_paid": false,
                      "is_awaiting": false,
                      "is_expired": false
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Link pembayaran tidak ditemukan"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/payment-gateway/payments": {
      "post": {
        "tags": [
          "Payment Gateway"
        ],
        "summary": "Buat Pembayaran Gateway",
        "operationId": "postBuatPembayaranGateway",
        "description": "Buat satu invoice gateway berisi QRIS dinamis, kode unik, dan link pembayaran.\n\nCatatan: tanpa kuota harian untuk key live, 60 invoice per menit (bawaan), kuota sandbox untuk key tq_test_.\n\nDokumentasi: https://temanqris.com/docs#gateway-create",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            },
            "description": "Key unik per percobaan create. Pakai key yang sama saat retry timeout. Body idempotency_key diterima sebagai cadangan kalau header tidak bisa dikirim."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "order_id": {
                    "type": "string",
                    "description": "Maksimal 30 karakter. Identitas order Anda. Satu order_id hanya punya satu pembayaran gateway."
                  },
                  "amount": {
                    "type": "integer",
                    "description": "Nominal dasar positif, maksimal 1.000.000. Angka bulat seperti 150000 atau \"150000\". Desimal, titik ribuan, atau notasi seperti \"1e5\" ditolak."
                  },
                  "fee_amount": {
                    "type": "integer",
                    "description": "Biaya tambahan (opsional): ditetapkan merchant sendiri, dibayar pelanggan, masuk penuh ke saldo Anda. Rupiah bulat 0 atau lebih, maksimal sama dengan amount. Bawaan 0."
                  },
                  "title": {
                    "type": "string",
                    "description": "Nama invoice, dipotong maksimal 100 karakter."
                  },
                  "description": {
                    "type": "string",
                    "description": "Deskripsi, dipotong maksimal 255 karakter."
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTP(S), wajib HTTPS di production. Host harus mengarah ke alamat publik. Alamat private, loopback, link-local, dan range khusus lain ditolak."
                  },
                  "callback_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Redirect http(s) untuk pelanggan. Bukan bukti pembayaran."
                  }
                },
                "required": [
                  "order_id",
                  "amount"
                ]
              },
              "example": {
                "order_id": "INV-2026-0001",
                "amount": 150000,
                "fee_amount": 0,
                "title": "Pembayaran Paket Premium",
                "description": "Pesanan pelanggan INV-2026-0001",
                "webhook_url": "https://merchant.example/webhooks/temanqris",
                "callback_url": "https://merchant.example/orders/INV-2026-0001/success"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "replay_2": {
                    "summary": "replay",
                    "value": {
                      "success": true,
                      "existing": true,
                      "payment_mode": "gateway",
                      "payment_attempt_id": "2de4a4d5-3b0e-4b7a-9a3e-4c9f7a2b1e55",
                      "payment_link": {
                        "id": 456,
                        "linkCode": "aZ3kQ9mP2xLw",
                        "orderId": "INV-2026-0001",
                        "amount": 150000,
                        "feeAmount": 0,
                        "provider_fee_amount": 1200,
                        "net_amount": 148800,
                        "uniqueCode": 321,
                        "totalAmount": 150321,
                        "description": "Pesanan pelanggan INV-2026-0001",
                        "expiresAt": "2026-10-17T03:15:00.000Z",
                        "payable_until": "2026-10-17T04:15:00.000Z",
                        "url": "/p/aZ3kQ9mP2xLw",
                        "qr_image": "data:image/png;base64,iVBOR..."
                      },
                      "mode": "live"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "201 Created",
                    "value": {
                      "success": true,
                      "existing": false,
                      "payment_mode": "gateway",
                      "payment_attempt_id": "2de4a4d5-3b0e-4b7a-9a3e-4c9f7a2b1e55",
                      "payment_link": {
                        "id": 456,
                        "linkCode": "aZ3kQ9mP2xLw",
                        "orderId": "INV-2026-0001",
                        "amount": 150000,
                        "feeAmount": 0,
                        "provider_fee_amount": 1200,
                        "net_amount": 148800,
                        "uniqueCode": 321,
                        "totalAmount": 150321,
                        "description": "Pesanan pelanggan INV-2026-0001",
                        "expiresAt": "2026-10-17T03:15:00.000Z",
                        "payable_until": "2026-10-17T04:15:00.000Z",
                        "url": "/p/aZ3kQ9mP2xLw",
                        "qr_image": "data:image/png;base64,iVBOR..."
                      },
                      "mode": "live"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request. Input atau idempotency tidak valid. Selalu ada code. Body JSON rusak.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "input_tidak_valid_5": {
                    "summary": "input tidak valid",
                    "value": {
                      "error": "fee amount must not exceed base amount",
                      "code": "fee_exceeds_base"
                    }
                  },
                  "400_8": {
                    "summary": "Body JSON rusak.",
                    "value": {
                      "error": "...",
                      "code": 400
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. Credential tidak ada atau salah.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_9": {
                    "summary": "Credential tidak ada atau salah.",
                    "value": {
                      "error": "API key required (send it in the x-api-key header)"
                    }
                  },
                  "contoh_10": {
                    "summary": "Credential tidak ada atau salah.",
                    "value": {
                      "error": "Invalid API key"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. KYC belum disetujui. Akun nonaktif atau JWT tidak valid. Total nominal hari ini sudah mencapai batas akun yang baru lolos KYC. Kode new_account_daily_amount_cap. Hanya kalau diaktifkan admin.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/NewAccountDailyAmountCapError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "batas_nominal_akun_baru_6": {
                    "summary": "batas nominal akun baru",
                    "value": {
                      "error": "Daily payment gateway amount limit reached for a newly verified account",
                      "code": "new_account_daily_amount_cap",
                      "limit": 5000000,
                      "used": 4900000,
                      "resetTime": "Midnight (UTC)"
                    }
                  },
                  "KYC_REQUIRED_11": {
                    "summary": "KYC belum disetujui.",
                    "value": {
                      "error": "KYC verification is required before using automatic payment gateway",
                      "code": "KYC_REQUIRED",
                      "kyc_status": "pending"
                    }
                  },
                  "contoh_12": {
                    "summary": "Akun nonaktif atau JWT tidak valid.",
                    "value": {
                      "error": "Account is deactivated"
                    }
                  },
                  "account_inactive_13": {
                    "summary": "Akun nonaktif atau JWT tidak valid.",
                    "value": {
                      "error": "merchant account is inactive",
                      "code": "account_inactive"
                    }
                  },
                  "contoh_14": {
                    "summary": "Akun nonaktif atau JWT tidak valid.",
                    "value": {
                      "error": "Invalid or expired token"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict. order_id sudah punya pembayaran gateway (berjalan, gagal, atau kedaluwarsa). Kode gateway_order_id_used. order_id dipakai pembayaran lain milik Anda, misalnya QRIS manual. Kode gateway_order_id_used. Key sedang dipakai request lain untuk order yang sama. Kode idempotency_key_in_use. Request pertama untuk key ini masih diproses. Kode gateway_creation_in_progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "409 Conflict",
                    "value": {
                      "error": "This order_id already has a gateway payment. Use a new order_id to try again.",
                      "code": "gateway_order_id_used"
                    }
                  },
                  "gateway_order_id_used_15": {
                    "summary": "order_id dipakai pembayaran lain milik Anda, misalnya QRIS manual. Kode gateway_order_id_used.",
                    "value": {
                      "error": "This order_id is already used by another payment. Use a new order_id to try again.",
                      "code": "gateway_order_id_used"
                    }
                  },
                  "idempotency_key_in_use_16": {
                    "summary": "Key sedang dipakai request lain untuk order yang sama. Kode idempotency_key_in_use.",
                    "value": {
                      "error": "This Idempotency-Key is already in use for this order. Retry with the same request to get the existing payment.",
                      "code": "idempotency_key_in_use"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. Key sudah dipakai untuk isi berbeda. Kode idempotency_key_reused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_4": {
                    "summary": "422 Unprocessable Content",
                    "value": {
                      "error": "This Idempotency-Key was already used for a different request. Send the original request again to get its payment, or use a new Idempotency-Key.",
                      "code": "idempotency_key_reused"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. Terlalu banyak invoice terbuka. Kode gateway_open_limit. Terlalu banyak invoice dibuat dalam satu menit (key live). Kode gateway_rate_limited, dengan header Retry-After. Kuota harian Sandbox habis (key tq_test_, tanpa code, \"mode\": \"sandbox\").",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/GatewayRateLimitedError"
                    },
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "batas_per_menit_7": {
                    "summary": "batas per menit",
                    "value": {
                      "error": "Too many gateway payments created in a short time. Try again shortly.",
                      "code": "gateway_rate_limited",
                      "limit": 60,
                      "window_seconds": 60,
                      "retry_after_seconds": 18
                    }
                  },
                  "gateway_open_limit_17": {
                    "summary": "Terlalu banyak invoice terbuka. Kode gateway_open_limit.",
                    "value": {
                      "error": "Too many open gateway payments. Wait for some to be paid or expire.",
                      "code": "gateway_open_limit"
                    }
                  },
                  "contoh_18": {
                    "summary": "Kuota harian Sandbox habis (key tq_test_, tanpa code, \"mode\": \"sandbox\").",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily sandbox limit of 500 requests",
                      "mode": "sandbox",
                      "limit": 500,
                      "used": 500,
                      "resetTime": "Midnight (UTC)"
                    }
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          },
          "500": {
            "description": "Internal Server Error. Kesalahan internal saat menyiapkan pembayaran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_21": {
                    "summary": "Kesalahan internal saat menyiapkan pembayaran.",
                    "value": {
                      "error": "Failed to create gateway payment"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable. Semua kode unik untuk nominal ini dipakai. Kode gateway_amount_busy. Reservasi nominal lepas saat menyiapkan. Kode gateway_amount_reservation_lost. DNS sedang bermasalah saat memeriksa webhook_url. Kode webhook_dns_unavailable. Gateway dimatikan admin atau belum dikonfigurasi (tanpa code).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "gateway_amount_busy_19": {
                    "summary": "Semua kode unik untuk nominal ini dipakai. Kode gateway_amount_busy.",
                    "value": {
                      "error": "All unique codes for this amount are in use. Try again in a few minutes or use a different amount.",
                      "code": "gateway_amount_busy"
                    }
                  },
                  "contoh_20": {
                    "summary": "Gateway dimatikan admin atau belum dikonfigurasi (tanpa code).",
                    "value": {
                      "error": "Payment gateway mode is disabled by admin"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/payment-gateway/status": {
      "get": {
        "tags": [
          "Payment Gateway"
        ],
        "summary": "Status Gateway",
        "operationId": "getStatusGateway",
        "description": "Ambil status gateway, akses KYC, tarif biaya layanan, dan jumlah invoice yang masih terbuka. Tetap bisa dipanggil saat gateway dimatikan (enabled: false). Dengan key tq_test_, response menggambarkan Sandbox.\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#gateway-status",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "enabled": true,
                      "payment_mode": "gateway",
                      "mode": "live",
                      "max_base_amount": 1000000,
                      "max_total_amount": 1000999,
                      "open_attempt_count": 2,
                      "provider_fee": {
                        "percent_bps": 80,
                        "flat_amount": 0,
                        "threshold_amount": 1,
                        "below_threshold_percent_bps": 80
                      },
                      "kyc_status": "approved",
                      "gateway_access": "available"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "401 Unauthorized",
                    "value": {
                      "error": "Invalid API key"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/payment-gateway/sandbox/payments/{order_id}": {
      "get": {
        "tags": [
          "Payment Gateway"
        ],
        "summary": "Cek invoice sandbox",
        "operationId": "getCekInvoiceSandbox",
        "description": "Response sama dengan saat membuat invoice (dengan existing: true), ditambah status (qr_issued, paid, expired), paid_at, dan paid_late. Invoice terbuka yang melewati waktu kedaluwarsa tampil expired. Tidak ditemukan: 404 sandbox_payment_not_found.\n\nCatatan: sandbox key saja.\n\nDokumentasi: https://temanqris.com/docs#gateway-sandbox-cek-invoice-sandbox",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "existing": true,
                      "payment_mode": "gateway",
                      "payment_attempt_id": "7b0f3c2e-9a41-4d5e-8c3b-1f2e3d4c5b6a",
                      "payment_link": {
                        "id": 31,
                        "linkCode": "sb_Xk2mQ9aZ3pLw7RtY",
                        "orderId": "INV-2026-0001",
                        "amount": 150000,
                        "feeAmount": 0,
                        "provider_fee_amount": 1200,
                        "net_amount": 148800,
                        "uniqueCode": 87,
                        "totalAmount": 150087,
                        "description": null,
                        "expiresAt": "2026-10-17T03:15:00.000Z",
                        "payable_until": "2026-10-17T04:15:00.000Z",
                        "url": "/p/sb_Xk2mQ9aZ3pLw7RtY",
                        "qr_image": null
                      },
                      "mode": "sandbox",
                      "paid_late": false,
                      "status": "qr_issued",
                      "paid_at": null
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Pembayaran sandbox tidak ditemukan.",
                      "code": "sandbox_payment_not_found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/payment-gateway/sandbox/payments/{order_id}/simulate": {
      "post": {
        "tags": [
          "Payment Gateway"
        ],
        "summary": "Simulasikan hasil pembayaran",
        "operationId": "postSimulasikanHasilPembayaran",
        "description": "Response: bentuk yang sama dengan cek invoice, ditambah changed (true), webhook_queued (true atau false), dan webhook_reason (null kalau webhook masuk antrean; not_paid untuk expired; no_webhook_url; atau no_webhook_secret).\n\nCatatan: sandbox key saja, memakai kuota sandbox.\n\nDokumentasi: https://temanqris.com/docs#gateway-sandbox-simulasikan-hasil-pembayaran",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "order_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "result": {
                    "type": "string",
                    "enum": [
                      "paid",
                      "expired",
                      "late_paid"
                    ],
                    "description": "Hasil yang disimulasikan."
                  }
                },
                "required": [
                  "result"
                ]
              },
              "example": {
                "result": "paid"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "existing": true,
                      "payment_mode": "gateway",
                      "payment_attempt_id": "7b0f3c2e-9a41-4d5e-8c3b-1f2e3d4c5b6a",
                      "payment_link": {
                        "linkCode": "sb_Xk2mQ9aZ3pLw7RtY",
                        "orderId": "INV-2026-0001",
                        "totalAmount": 150087,
                        "...": "..."
                      },
                      "mode": "sandbox",
                      "paid_late": false,
                      "changed": true,
                      "webhook_queued": true,
                      "webhook_reason": null,
                      "status": "paid",
                      "paid_at": "2026-10-17T03:05:41.000Z"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Bad Request. sandbox_invalid_result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "result must be paid, expired, or late_paid",
                      "code": "sandbox_invalid_result"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. wrong_gateway_mode.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {}
              }
            }
          },
          "404": {
            "description": "Not Found. sandbox_payment_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {}
              }
            }
          },
          "409": {
            "description": "Conflict. sandbox_invalid_transition. sandbox_webhook_pending. sandbox_no_webhook_event. sandbox_no_webhook_url. sandbox_no_webhook_secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {}
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited atau tanpa code.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/DailyQuotaExceededError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "kuota_sandbox_habis": {
                    "summary": "Kuota harian Sandbox habis",
                    "value": {
                      "error": "Rate limit exceeded",
                      "message": "You have exceeded your daily sandbox limit of 500 requests",
                      "mode": "sandbox",
                      "limit": 500,
                      "used": 500,
                      "resetTime": "Midnight (UTC)"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/generate": {
      "post": {
        "tags": [
          "Widget"
        ],
        "summary": "Buat QR widget",
        "operationId": "postBuatQrWidget",
        "description": "Untuk gateway, amount adalah total yang harus dibayar (sudah termasuk kode unik). Biaya layanan tidak ditampilkan ke pelanggan, Anda melihatnya di webhook dan dashboard. order_id dibuat dari idempotency_key.\n\nCatatan: publik, 60 request per menit per IP.\n\nDokumentasi: https://temanqris.com/docs#widget-api-buat-qr-widget",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "merchant_id": {
                    "type": "string",
                    "description": "Merchant ID, maksimal 64 karakter."
                  },
                  "amount": {
                    "type": "integer",
                    "description": "Manual: 1 sampai 100.000.000. Gateway: maksimal 1.000.000."
                  },
                  "description": {
                    "type": "string",
                    "description": "Maksimal 255 karakter."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL publik."
                  },
                  "callback_url": {
                    "type": "string",
                    "description": "URL http(s)."
                  },
                  "payment_mode": {
                    "type": "string",
                    "description": "Kosongkan untuk manual. Isi \"gateway\" untuk pembayaran otomatis (butuh KYC disetujui)."
                  },
                  "idempotency_key": {
                    "type": "string",
                    "description": "Hanya untuk gateway, maksimal 256 karakter. Key yang sama mengembalikan invoice yang sama. Tanpa key, setiap panggilan membuat invoice baru."
                  }
                },
                "required": [
                  "merchant_id",
                  "amount"
                ]
              },
              "example": {
                "merchant_id": "abc123xyz",
                "amount": 50000,
                "description": "Pembayaran Order #123",
                "webhook_url": "https://yoursite.com/webhook",
                "callback_url": "https://yoursite.com/thank-you"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "manual_1": {
                    "summary": "manual",
                    "value": {
                      "success": true,
                      "order_id": "ORD-W5E1A9C03B7D2F4E6",
                      "link_code": "W8F3A1C7E92B4D605",
                      "amount": 50000,
                      "merchant_name": "TOKO EXAMPLE",
                      "qr_image": "data:image/png;base64,iVBOR...",
                      "qris_string": "00020101021226...",
                      "expires_at": "2026-10-17T04:00:00.000Z",
                      "description": "Pembayaran Order #123"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "examples": {
                  "gateway_2": {
                    "summary": "gateway",
                    "value": {
                      "success": true,
                      "payment_mode": "gateway",
                      "order_id": "ORD-W3F9A1C7E92B4D605A1B2",
                      "link_code": "Qm7tR2xP9aKd",
                      "amount": 50642,
                      "base_amount": 50000,
                      "unique_code": 642,
                      "total_amount": 50642,
                      "merchant_name": "TemanQRIS",
                      "qr_image": "data:image/png;base64,iVBOR...",
                      "qris_string": "00020101021226...",
                      "expires_at": "2026-10-17T03:15:00.000Z",
                      "description": "Pembayaran via Widget"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_4": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "merchant_id is required"
                    }
                  },
                  "contoh_5": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "amount must be a whole number between 1 and 100000000"
                    }
                  },
                  "contoh_6": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "Gateway amount must not exceed Rp1.000.000"
                    }
                  },
                  "contoh_7": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "idempotency_key must be at most 256 characters"
                    }
                  },
                  "amount_too_small_8": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "This amount is too small for an automatic payment. Use a larger amount.",
                      "code": "amount_too_small"
                    }
                  },
                  "invalid_webhook_url_9": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "webhook_url rejected: ...",
                      "code": "invalid_webhook_url"
                    }
                  },
                  "webhook_host_mismatch_11": {
                    "summary": "400 Bad Request",
                    "value": {
                      "error": "webhook_url must use the website or app domain from your approved KYC",
                      "code": "webhook_host_mismatch"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/NewAccountDailyAmountCapError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "new_account_daily_amount_cap_12": {
                    "summary": "403 Forbidden",
                    "value": {
                      "error": "Merchant ini sudah mencapai batas transaksi harian. Coba lagi besok.",
                      "code": "new_account_daily_amount_cap"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_3": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Merchant not available"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "gateway_attempt_terminal_13": {
                    "summary": "409 Conflict",
                    "value": {
                      "error": "This payment attempt already failed or expired. Use a new idempotency_key to try again.",
                      "code": "gateway_attempt_terminal"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_14": {
                    "summary": "429 Too Many Requests",
                    "value": {
                      "error": "Daily limit exceeded for this merchant"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_webhook_url_10": {
                    "summary": "503 Service Unavailable",
                    "value": {
                      "error": "webhook_url rejected: ...",
                      "code": "invalid_webhook_url"
                    }
                  },
                  "contoh_15": {
                    "summary": "503 Service Unavailable",
                    "value": {
                      "error": "Payment gateway mode is disabled by admin"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/confirm/{orderId}": {
      "post": {
        "tags": [
          "Widget"
        ],
        "summary": "Konfirmasi dari widget",
        "operationId": "postKonfirmasiDariWidget",
        "description": "Sama seperti tombol Sudah Bayar: order menjadi awaiting_confirmation. Kirim link_code di body supaya order teridentifikasi tepat.\n\nCatatan: publik, 30 request per menit per IP.\n\nDokumentasi: https://temanqris.com/docs#widget-api-konfirmasi-dari-widget",
        "security": [],
        "parameters": [
          {
            "name": "orderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "link_code": {
                    "type": "string",
                    "description": "Kode payment link dari response /api/widget/generate, supaya order teridentifikasi tepat."
                  }
                }
              },
              "example": {
                "link_code": "W8F3A1C7E92B4D605"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "message": "Konfirmasi berhasil. Merchant akan segera memproses pesanan Anda.",
                      "status": "awaiting_confirmation"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Order ID tidak ditemukan"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/widget/merchant/{merchantId}": {
      "get": {
        "tags": [
          "Widget"
        ],
        "summary": "Info merchant",
        "operationId": "getInfoMerchant",
        "description": "Tidak ditemukan atau akun nonaktif: 404 {\"error\":\"Merchant not available\"}.\n\nCatatan: publik.\n\nDokumentasi: https://temanqris.com/docs#widget-api-info-merchant",
        "security": [],
        "parameters": [
          {
            "name": "merchantId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "success": true,
                      "merchant_name": "TOKO EXAMPLE"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "404 Not Found",
                    "value": {
                      "error": "Merchant not available"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/qris/usage": {
      "get": {
        "tags": [
          "Akun"
        ],
        "summary": "Penggunaan API",
        "operationId": "getPenggunaanApi",
        "description": "Pantau pemakaian API dan sisa kuota harian.\n\nCatatan: tidak memakai kuota.\n\nDokumentasi: https://temanqris.com/docs#usage",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "examples": {
                  "contoh_1": {
                    "summary": "200 OK",
                    "value": {
                      "tier": "free",
                      "daily_limit": 50,
                      "usage": {
                        "today": {
                          "total": 15,
                          "remaining": 35,
                          "by_endpoint": [
                            {
                              "endpoint": "/api/qris/generate",
                              "request_count": 10
                            },
                            {
                              "endpoint": "/api/qris/payment-link",
                              "request_count": 4
                            },
                            {
                              "endpoint": "/api/qris/orders/:orderId/verify",
                              "request_count": 1
                            }
                          ]
                        },
                        "this_month": {
                          "total": 250,
                          "by_endpoint": [
                            {
                              "endpoint": "/api/qris/generate",
                              "total_requests": "180"
                            },
                            {
                              "endpoint": "/api/qris/payment-link",
                              "total_requests": "70"
                            }
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "contoh_2": {
                    "summary": "401 Unauthorized",
                    "value": {
                      "error": "Invalid API key"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "payment.awaiting_confirmation": {
      "post": {
        "summary": "Pelanggan menekan Sudah Bayar (jalur manual)",
        "description": "Dikirim ke webhook_url Anda, ditandatangani HMAC-SHA256 dari raw body (header X-TemanQRIS-Signature). Dokumentasi: https://temanqris.com/docs#webhook-verify",
        "parameters": [
          {
            "name": "X-TemanQRIS-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^sha256=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-TemanQRIS-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TemanQRIS-Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TemanQRIS-Mode",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "sandbox"
              ]
            }
          },
          {
            "name": "X-TemanQRIS-Retry",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "event": "payment.awaiting_confirmation",
                "idempotency_key": "a3f1c2d4-5b6e-4f70-8a9b-0c1d2e3f4a5b",
                "timestamp": "2026-10-17T09:00:00.000Z",
                "data": {
                  "order_id": "INV-2026-001",
                  "link_code": "ABC12345XYZW",
                  "amount": 75000,
                  "description": "Pembayaran Order #12345",
                  "status": "awaiting_confirmation",
                  "paid_at": null,
                  "created_at": "2026-10-17T08:55:00.000Z",
                  "mode": "live"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Balas 2xx dalam 10 detik setelah data tersimpan."
          }
        }
      }
    },
    "payment.confirmed": {
      "post": {
        "summary": "Pembayaran sah (verifikasi manual atau gateway otomatis)",
        "description": "Dikirim ke webhook_url Anda, ditandatangani HMAC-SHA256 dari raw body (header X-TemanQRIS-Signature). Dokumentasi: https://temanqris.com/docs#webhook-verify",
        "parameters": [
          {
            "name": "X-TemanQRIS-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^sha256=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-TemanQRIS-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TemanQRIS-Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TemanQRIS-Mode",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "live",
                "sandbox"
              ]
            }
          },
          {
            "name": "X-TemanQRIS-Retry",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "example": {
                "event": "payment.confirmed",
                "idempotency_key": "8f4e2b1a-6c3d-4e5f-9a0b-1c2d3e4f5a6b",
                "timestamp": "2026-10-17T03:08:30.000Z",
                "data": {
                  "order_id": "INV-2026-0001",
                  "link_code": "aZ3kQ9mP2xLw",
                  "amount": 150000,
                  "description": "Pesanan pelanggan INV-2026-0001",
                  "status": "paid",
                  "paid_at": "2026-10-17T03:08:12.000Z",
                  "created_at": "2026-10-17T03:00:00.000Z",
                  "mode": "live",
                  "payment_mode": "gateway",
                  "payment_attempt_id": "2de4a4d5-3b0e-4b7a-9a3e-4c9f7a2b1e55",
                  "base_amount": 150000,
                  "fee_amount": 0,
                  "unique_code": 321,
                  "total_amount": 150321,
                  "provider_fee_amount": 1200,
                  "net_amount": 148800,
                  "confirmed_by": "gateway"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Balas 2xx dalam 10 detik setelah data tersimpan."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key live (64 karakter heksadesimal) atau key sandbox (tq_test_ + 48 karakter heksadesimal, hanya untuk endpoint Payment Gateway)."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT sesi dashboard."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Bentuk umum response error. Pakai code untuk logika program, jangan mencocokkan teks error. Endpoint /api/qris tidak membawa code string.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Pesan untuk manusia. Teksnya bisa berubah."
          },
          "code": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "invalid_order_id",
                  "missing_idempotency_key",
                  "invalid_idempotency_key",
                  "invalid_amount",
                  "amount_too_large",
                  "invalid_fee_amount",
                  "fee_exceeds_base",
                  "amount_below_provider_fee",
                  "total_amount_too_large",
                  "gateway_total_exceeds_cap",
                  "invalid_webhook_url",
                  "invalid_callback_url",
                  "sandbox_invalid_result",
                  "amount_too_small",
                  "KYC_REQUIRED",
                  "account_inactive",
                  "wrong_gateway_mode",
                  "new_account_daily_amount_cap",
                  "sandbox_payment_not_found",
                  "gateway_order_id_used",
                  "idempotency_key_in_use",
                  "gateway_creation_in_progress",
                  "gateway_attempt_terminal",
                  "sandbox_invalid_transition",
                  "sandbox_webhook_pending",
                  "sandbox_no_webhook_event",
                  "sandbox_no_webhook_url",
                  "sandbox_no_webhook_secret",
                  "idempotency_key_reused",
                  "gateway_rate_limited",
                  "gateway_open_limit",
                  "rate_limited",
                  "gateway_amount_busy",
                  "gateway_amount_reservation_lost",
                  "provider_timeout",
                  "webhook_dns_unavailable"
                ],
                "description": "Kode mesin yang stabil untuk error Payment Gateway, Sandbox, dan Widget API gateway."
              },
              {
                "type": "integer",
                "minimum": 400,
                "maximum": 599,
                "description": "Status HTTP untuk error umum: JSON rusak, body terlalu besar, atau error server."
              }
            ]
          },
          "message": {
            "type": "string",
            "description": "Penjelasan tambahan, terutama di endpoint /api/qris."
          },
          "success": {
            "type": "boolean",
            "const": false
          },
          "kyc_status": {
            "type": "string",
            "description": "Ada pada KYC_REQUIRED."
          },
          "existing_link_code": {
            "type": "string",
            "description": "Link yang sudah ada untuk order_id itu."
          }
        },
        "additionalProperties": true
      },
      "GatewayRateLimitedError": {
        "type": "object",
        "description": "Batas pembuatan invoice gateway live per menit terlewat. Tunggu Retry-After detik, lalu kirim ulang request yang sama dengan Idempotency-Key yang sama.",
        "required": [
          "error",
          "code",
          "limit",
          "window_seconds",
          "retry_after_seconds"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "const": "gateway_rate_limited"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "description": "Batas invoice per menit untuk merchant ini (bawaan 60)."
          },
          "window_seconds": {
            "const": 60
          },
          "retry_after_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60,
            "description": "Sama dengan header Retry-After."
          }
        }
      },
      "NewAccountDailyAmountCapError": {
        "type": "object",
        "description": "Total nominal invoice gateway live hari ini sudah mencapai batas untuk akun yang baru lolos KYC. Hanya aktif kalau diatur admin. Reset 00.00 UTC. Widget API hanya mengirim error dan code.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "const": "new_account_daily_amount_cap"
          },
          "limit": {
            "type": "integer",
            "minimum": 0,
            "description": "Batas total amount per hari dalam rupiah."
          },
          "used": {
            "type": "integer",
            "minimum": 0,
            "description": "Total amount yang sudah terpakai hari ini, belum termasuk request yang ditolak."
          },
          "resetTime": {
            "const": "Midnight (UTC)"
          }
        }
      },
      "DailyQuotaExceededError": {
        "type": "object",
        "description": "Kuota harian habis: kuota tier untuk fitur QRIS manual, atau kuota Sandbox (mode: sandbox). Reset 00.00 UTC.",
        "required": [
          "error",
          "limit",
          "used",
          "resetTime"
        ],
        "properties": {
          "error": {
            "const": "Rate limit exceeded"
          },
          "message": {
            "type": "string"
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "premium"
            ]
          },
          "mode": {
            "const": "sandbox"
          },
          "limit": {
            "type": "integer"
          },
          "used": {
            "type": "integer"
          },
          "resetTime": {
            "type": "string"
          }
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Batas yang berlaku: invoice per menit untuk create gateway live, atau request per hari untuk endpoint yang memakai kuota harian dan Sandbox.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Sisa dalam jendela yang sedang berjalan.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp (detik) saat jendela berganti: awal menit berikutnya untuk create gateway live, akhir hari UTC untuk kuota harian.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Detik sampai boleh mencoba lagi. Dikirim bersama 429 gateway_rate_limited.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 60
        }
      }
    }
  }
}
