{
  "openapi": "3.0.3",
  "info": {
    "title": "HP Gateway Merchant API — pg-lrvl (/api/v1)",
    "version": "1.0.1",
    "description": "Refunds, invoices, subscriptions, read-only balance/settlement history, and turning a gateway-pg order into a payable checkout link. Order creation/lookup/cancel itself lives on a separate service — see gateway-pg's own /v1/* Swagger docs for those. Both surfaces share the same sk_test_/sk_live_ API key.",
    "contact": { "name": "HP Gateway", "url": "https://dashboard.healthpay.com.eg/api/documentation" }
  },
  "servers": [
    { "url": "https://dashboard.healthpay.com.eg/api/v1", "description": "Production" }
  ],
  "security": [{ "ApiKeyAuth": [] }],
  "tags": [
    { "name": "Orders" },
    { "name": "Refunds" },
    { "name": "Invoices" },
    { "name": "Subscriptions" },
    { "name": "Balance & Settlements" }
  ],
  "paths": {
    "/orders/{uuid}/checkout-url": {
      "post": {
        "tags": ["Orders"], "summary": "Get a payable checkout link for an order", "operationId": "getOrderCheckoutUrl",
        "description": "POST /v1/orders (gateway-pg) creates an order but returns no checkout URL — building one needs a server-side encryption step gateway-pg doesn't hold the key for. Call this immediately after creating an order to get a link to send your customer to. Idempotent: calling it again for the same order returns a link to the same underlying checkout session, not a second one.",
        "parameters": [{ "name": "uuid", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The order's checkout UUID, from POST /v1/orders's response." }],
        "requestBody": { "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "customer_name": { "type": "string", "maxLength": 190 },
            "customer_email": { "type": "string", "format": "email" },
            "customer_mobile": { "type": "string" }
          }
        } } } },
        "responses": {
          "200": { "description": "Checkout link", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "checkoutUrl": { "type": "string", "format": "uri", "description": "Send the customer here to pay." },
              "expiresAt": { "type": "string", "description": "Y-m-d H:i — the link's due-date/expiry window, not a hard timeout." }
            }
          } } } },
          "404": { "description": "No order with this UUID belongs to this merchant", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/refunds": {
      "get": {
        "tags": ["Refunds"], "summary": "List your refund requests", "operationId": "listRefunds",
        "responses": { "200": { "description": "Paginated refund list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedRefunds" } } } } }
      },
      "post": {
        "tags": ["Refunds"], "summary": "Submit a refund request", "operationId": "createRefund",
        "description": "Full or partial refund against a settled order. Amount defaults to the full order amount if omitted.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["order_uuid"],
          "properties": {
            "order_uuid": { "type": "string", "maxLength": 64, "description": "The order's UUID, not the transaction/order number." },
            "amount": { "type": "number", "format": "float", "minimum": 0.01, "description": "Omit for a full refund." },
            "reason": { "type": "string", "maxLength": 500 }
          }
        } } } },
        "responses": {
          "201": { "description": "Refund created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refund" } } } },
          "409": { "description": "Not refundable (already refunded, wrong status, amount exceeds order, etc.)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/refunds/{refund}": {
      "get": {
        "tags": ["Refunds"], "summary": "Get a single refund", "operationId": "getRefund",
        "parameters": [{ "$ref": "#/components/parameters/RefundId" }],
        "responses": {
          "200": { "description": "Refund", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refund" } } } },
          "404": { "description": "Not found or not yours", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/invoices": {
      "get": {
        "tags": ["Invoices"], "summary": "List your invoices", "operationId": "listInvoices",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/InvoiceStatus" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": { "200": { "description": "Paginated invoice list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedInvoices" } } } } }
      },
      "post": {
        "tags": ["Invoices"], "summary": "Create a draft invoice", "operationId": "createInvoice",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceInput" } } } },
        "responses": { "201": { "description": "Invoice created as a draft", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } } } }
      }
    },
    "/invoices/{invoice}": {
      "get": {
        "tags": ["Invoices"], "summary": "Get a single invoice", "operationId": "getInvoice",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "responses": { "200": { "description": "Invoice", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } } } }
      },
      "put": {
        "tags": ["Invoices"], "summary": "Edit a draft invoice", "operationId": "updateInvoice",
        "description": "Only a draft invoice can be edited — once sent, void it and create a new one instead.",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceInput" } } } },
        "responses": {
          "200": { "description": "Updated invoice", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Invoice" } } } },
          "409": { "description": "Not a draft", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      },
      "delete": {
        "tags": ["Invoices"], "summary": "Delete a draft invoice", "operationId": "deleteInvoice",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "responses": {
          "204": { "description": "Deleted" },
          "409": { "description": "Not a draft", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/invoices/{invoice}/send": {
      "post": {
        "tags": ["Invoices"], "summary": "Send an invoice, or one installment of it", "operationId": "sendInvoice",
        "description": "Makes the invoice payable and emails/texts the customer a checkout link. Omit amount to send the full outstanding balance (a plain resend reuses the existing link until the first payment lands). Pass amount to send a specific installment instead — this always creates a fresh checkout link and the invoice's status tracks through 'partially_paid' as installments are paid.",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "requestBody": { "content": { "application/json": { "schema": {
          "type": "object", "properties": { "amount": { "type": "number", "format": "float", "minimum": 0.01, "description": "Installment amount. Omitted = full outstanding balance." } }
        } } } },
        "responses": {
          "200": { "description": "Sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } },
          "409": { "description": "Not sendable from its current status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/invoices/{invoice}/void": {
      "post": {
        "tags": ["Invoices"], "summary": "Void an invoice", "operationId": "voidInvoice",
        "description": "Stops the checkout link from accepting payment. If a payment slips through in the same moment, the invoice surfaces as paid rather than staying silently void.",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "responses": { "200": { "description": "Voided", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } }
      }
    },
    "/invoices/{invoice}/pdf": {
      "get": {
        "tags": ["Invoices"], "summary": "Download the invoice as a PDF", "operationId": "getInvoicePdf",
        "parameters": [{ "$ref": "#/components/parameters/InvoiceId" }],
        "responses": { "200": { "description": "PDF file", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } } }
      }
    },
    "/subscriptions": {
      "get": {
        "tags": ["Subscriptions"], "summary": "List your subscriptions", "operationId": "listSubscriptions",
        "parameters": [
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/SubscriptionStatus" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 100 } }
        ],
        "responses": { "200": { "description": "Paginated subscription list", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedSubscriptions" } } } } }
      },
      "post": {
        "tags": ["Subscriptions"], "summary": "Create a subscription", "operationId": "createSubscription",
        "description": "A subscription is a template that auto-generates and sends a normal Invoice every billing cycle — HP Gateway never silently re-charges a saved card, so this is recurring invoicing, not off-session billing.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionInput" } } } },
        "responses": { "201": { "description": "Subscription created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subscription" } } } } }
      }
    },
    "/subscriptions/{subscription}": {
      "get": {
        "tags": ["Subscriptions"], "summary": "Get a single subscription", "operationId": "getSubscription",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "responses": { "200": { "description": "Subscription", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subscription" } } } } }
      },
      "put": {
        "tags": ["Subscriptions"], "summary": "Edit a subscription", "operationId": "updateSubscription",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscriptionInput" } } } },
        "responses": {
          "200": { "description": "Updated subscription", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subscription" } } } },
          "409": { "description": "Cancelled subscriptions can't be edited", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      },
      "delete": {
        "tags": ["Subscriptions"], "summary": "Delete a subscription", "operationId": "deleteSubscription",
        "description": "Only a subscription that has never billed can be deleted — one that has already produced an invoice must be cancelled instead, to keep its billing history intact.",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "responses": {
          "204": { "description": "Deleted" },
          "409": { "description": "Already billed at least once", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/subscriptions/{subscription}/pause": {
      "post": {
        "tags": ["Subscriptions"], "summary": "Pause a subscription", "operationId": "pauseSubscription",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "responses": { "200": { "description": "Paused", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } }
      }
    },
    "/subscriptions/{subscription}/resume": {
      "post": {
        "tags": ["Subscriptions"], "summary": "Resume a paused subscription", "operationId": "resumeSubscription",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "responses": { "200": { "description": "Resumed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } }
      }
    },
    "/subscriptions/{subscription}/cancel": {
      "post": {
        "tags": ["Subscriptions"], "summary": "Cancel a subscription", "operationId": "cancelSubscription",
        "description": "Terminal — a cancelled subscription cannot be resumed. Stops future invoice generation; invoices already sent are unaffected.",
        "parameters": [{ "$ref": "#/components/parameters/SubscriptionId" }],
        "responses": { "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } } } }
      }
    },
    "/balance": {
      "get": {
        "tags": ["Balance & Settlements"], "summary": "Current ledger balance", "operationId": "getBalance",
        "responses": {
          "200": { "description": "Balance", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" } } } },
          "503": { "description": "Ledger temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    },
    "/settlements": {
      "get": {
        "tags": ["Balance & Settlements"], "summary": "Settlement batch history", "operationId": "getSettlements",
        "responses": {
          "200": { "description": "Settlement batches", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Settlements" } } } },
          "503": { "description": "Settlement service temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorMessage" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http", "scheme": "bearer",
        "description": "Authorization: Bearer sk_live_... (or sk_test_... for sandbox — never touches live money or data). Generate/revoke keys from the dashboard's Developer page. A read-only key gets a 403 on any non-GET request. Rate-limited to 100 requests per 10 seconds per key (429 when exceeded)."
      }
    },
    "parameters": {
      "InvoiceId": { "name": "invoice", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "SubscriptionId": { "name": "subscription", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "RefundId": { "name": "refund", "in": "path", "required": true, "schema": { "type": "integer" } }
    },
    "schemas": {
      "ErrorMessage": { "type": "object", "properties": { "message": { "type": "string" } } },
      "MessageResponse": { "type": "object", "properties": { "message": { "type": "string" } } },
      "InvoiceStatus": { "type": "string", "enum": ["draft", "sent", "partially_paid", "paid", "overdue", "void"] },
      "SubscriptionStatus": { "type": "string", "enum": ["active", "paused", "cancelled"] },
      "LineItemInput": {
        "type": "object", "required": ["description", "quantity", "unit_price"],
        "properties": {
          "description": { "type": "string", "maxLength": 255 },
          "quantity": { "type": "number", "format": "float", "minimum": 0.01 },
          "unit_price": { "type": "number", "format": "float", "minimum": 0 }
        }
      },
      "LineItem": {
        "allOf": [{ "$ref": "#/components/schemas/LineItemInput" }],
        "type": "object", "properties": { "amount": { "type": "number", "format": "float", "description": "quantity × unit_price" } }
      },
      "InvoiceInput": {
        "type": "object", "required": ["customer_name", "currency", "items"],
        "properties": {
          "customer_name": { "type": "string", "maxLength": 190 },
          "customer_email": { "type": "string", "format": "email", "nullable": true },
          "customer_mobile": { "type": "string", "nullable": true },
          "currency": { "type": "string", "maxLength": 8 },
          "due_date": { "type": "string", "format": "date", "nullable": true, "description": "Must not be in the past." },
          "notes": { "type": "string", "nullable": true },
          "tax_percent": { "type": "number", "format": "float", "minimum": 0, "maximum": 100, "nullable": true },
          "discount_amount": { "type": "number", "format": "float", "minimum": 0, "nullable": true },
          "items": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/LineItemInput" } }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "uuid": { "type": "string", "format": "uuid" },
          "invoice_number": { "type": "string" },
          "customer_name": { "type": "string" },
          "customer_email": { "type": "string", "nullable": true },
          "customer_mobile": { "type": "string", "nullable": true },
          "currency": { "type": "string" },
          "due_date": { "type": "string", "format": "date", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "subtotal": { "type": "number", "format": "float" },
          "discount_amount": { "type": "number", "format": "float" },
          "tax_percent": { "type": "number", "format": "float" },
          "tax_amount": { "type": "number", "format": "float" },
          "total": { "type": "number", "format": "float" },
          "amount_paid": { "type": "number", "format": "float", "description": "Rolls up from paid installments. Equals total once fully paid." },
          "status": { "$ref": "#/components/schemas/InvoiceStatus" },
          "short_link": { "type": "string", "nullable": true, "description": "The customer-facing checkout link, once sent." },
          "sent_at": { "type": "string", "format": "date-time", "nullable": true },
          "viewed_at": { "type": "string", "format": "date-time", "nullable": true },
          "paid_at": { "type": "string", "format": "date-time", "nullable": true },
          "reminder_count": { "type": "integer" },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/LineItem" } }
        }
      },
      "SubscriptionInput": {
        "type": "object", "required": ["customer_name", "currency", "interval_unit", "interval_count", "due_days", "items"],
        "properties": {
          "customer_name": { "type": "string", "maxLength": 190 },
          "customer_email": { "type": "string", "format": "email", "nullable": true },
          "customer_mobile": { "type": "string", "nullable": true },
          "currency": { "type": "string", "maxLength": 8 },
          "notes": { "type": "string", "nullable": true },
          "tax_percent": { "type": "number", "format": "float", "nullable": true },
          "discount_amount": { "type": "number", "format": "float", "nullable": true },
          "interval_unit": { "type": "string", "enum": ["day", "week", "month"] },
          "interval_count": { "type": "integer", "minimum": 1, "maximum": 365 },
          "due_days": { "type": "integer", "minimum": 0, "maximum": 90, "description": "Days after each cycle's invoice is sent before it's due." },
          "start_date": { "type": "string", "format": "date", "nullable": true },
          "items": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/LineItemInput" } }
        }
      },
      "Subscription": {
        "type": "object",
        "properties": {
          "uuid": { "type": "string", "format": "uuid" },
          "subscription_number": { "type": "string" },
          "customer_name": { "type": "string" },
          "customer_email": { "type": "string", "nullable": true },
          "customer_mobile": { "type": "string", "nullable": true },
          "currency": { "type": "string" },
          "interval_unit": { "type": "string", "enum": ["day", "week", "month"] },
          "interval_count": { "type": "integer" },
          "due_days": { "type": "integer" },
          "next_billing_date": { "type": "string", "format": "date", "nullable": true },
          "status": { "$ref": "#/components/schemas/SubscriptionStatus" },
          "last_invoice_id": { "type": "integer", "nullable": true },
          "consecutive_missed_cycles": { "type": "integer", "description": "Auto-pauses after 2 — see MAX_MISSED_CYCLES." },
          "cancelled_at": { "type": "string", "format": "date-time", "nullable": true },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/LineItem" } }
        }
      },
      "Refund": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "trx": { "type": "string" },
          "order_uuid": { "type": "string" },
          "amount": { "type": "number", "format": "float" },
          "status": { "type": "string", "description": "Refund state machine — see the human-readable docs for the full status list." },
          "engine_refund_uuid": { "type": "string", "nullable": true },
          "psp_reference": { "type": "string", "nullable": true },
          "reason": { "type": "string", "nullable": true },
          "processed_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "Balance": {
        "type": "object",
        "properties": {
          "current": { "type": "number", "format": "float" },
          "credit_total": { "type": "number", "format": "float" },
          "debit_total": { "type": "number", "format": "float" },
          "currency": { "type": "string" }
        }
      },
      "Settlements": {
        "type": "object",
        "properties": {
          "batches": { "type": "array", "items": { "type": "object", "description": "Raw settlement-pg batch record (id, status, netAmount, periodStart, periodEnd, …)." } },
          "paid_total": { "type": "number", "format": "float" },
          "paid_count": { "type": "integer" },
          "pending": { "type": "number", "format": "float", "description": "Sum of PENDING + RECONCILED batches not yet paid out." }
        }
      },
      "PaginatedInvoices": { "$ref": "#/components/schemas/LaravelPaginator" },
      "PaginatedSubscriptions": { "$ref": "#/components/schemas/LaravelPaginator" },
      "PaginatedRefunds": { "$ref": "#/components/schemas/LaravelPaginator" },
      "LaravelPaginator": {
        "type": "object",
        "description": "Standard Laravel paginate() envelope.",
        "properties": {
          "data": { "type": "array", "items": {} },
          "current_page": { "type": "integer" },
          "last_page": { "type": "integer" },
          "per_page": { "type": "integer" },
          "total": { "type": "integer" }
        }
      }
    }
  }
}
