{
  "openapi": "3.1.0",
  "info": {
    "title": "SwiftPayIQ API",
    "version": "1.0.0",
    "description": "# SwiftPayIQ Integration API\n\nThe SwiftPayIQ API lets approved businesses collect payments programmatically — create **payment links** and **invoices**, and receive **webhooks** when a payment is completed. It is a REST API: JSON request/response bodies, standard HTTP verbs and status codes.\n\nYou get API credentials from the **SwiftPayIQ merchant dashboard** once your business has passed KYC. There is nothing to install — call the endpoints over HTTPS.\n\n## Base URL\n\nAll endpoints share one base URL. Every route is versioned under `/api/v1`.\n\n```\nhttps://api.swiftpayiq.com/api/v1\n```\n\n> The base URL is the **same for test and live**. Whether a request runs in test or live mode is decided entirely by which API key you authenticate with — see *Test vs Live* below. There is no separate sandbox host to call.\n\n## Authentication\n\nEvery request must include your secret API key as a Bearer token:\n\n```http\nAuthorization: Bearer spi_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nKeys look like `spi_live_…` or `spi_test_…`. Create them in the dashboard under **Developers → API Keys**; the full secret is shown **only once at creation**, so store it securely. Keys are not tied to a session and do not expire — rotate or revoke them from the dashboard.\n\nRequests with a missing, malformed, revoked, or wrong-mode key are rejected with `401 Unauthorized`.\n\n## Test vs Live\n\nThe key prefix is the only switch between modes:\n\n| Key prefix | Mode | Behaviour |\n|---|---|---|\n| `spi_test_…` | **Test** | Sandbox gateway credentials. No real money moves. Use for development. |\n| `spi_live_…` | **Live** | Production gateway credentials. Real charges are processed. |\n\nThe two modes are fully isolated: a payment link, invoice, customer, or webhook created in test mode is never visible to a live key, and vice-versa. You point your code at the same URL throughout — just swap the key when you go live.\n\n## Scopes\n\nEach API key carries a set of scopes. A request to an endpoint whose scope your key lacks is rejected with `403 Forbidden`. Grant only what you need:\n\n| Scope | Allows |\n|---|---|\n| `payment_links:read` | List and fetch payment links |\n| `payment_links:write` | Create, update, disable, and delete payment links |\n| `invoices:read` | List and fetch invoices |\n| `invoices:write` | Create, update, cancel, and delete invoices |\n\n## Amounts & currency\n\nAll amounts are in **Iraqi Dinar (IQD)**, which has no minor unit (no fils). Money is sent and returned as **integer strings** — e.g. `\"50000\"` means 50,000 IQD — to preserve precision. Never send decimals.\n\n## Pagination\n\nList endpoints accept `page` (default `1`) and `limit` (default `20`, max `100`) query parameters and return:\n\n```json\n{\n  \"items\": [ /* … */ ],\n  \"meta\": { \"total\": 137, \"page\": 1, \"limit\": 20, \"pageCount\": 7, \"hasNextPage\": true, \"hasPreviousPage\": false }\n}\n```\n\n## Errors\n\nErrors use standard HTTP status codes (`400` validation, `401` auth, `403` scope/KYC, `404` not found) with a JSON body describing the problem. Validation errors list the offending fields.\n\n## Webhooks\n\nWebhooks push real-time payment events to your server, so you don't have to poll. Configure your endpoint URL and subscribed events from the dashboard (**Developers → Webhooks**) — one endpoint per mode (test / live). Full payload shape, signature verification, and retry behaviour are documented in the **Webhooks** section below.\n\n## Getting started\n\n1. Complete KYC and get approved.\n2. Create an API key in the dashboard, choosing the scopes you need. Start in **test** mode.\n3. Create a payment link or invoice with `POST /payment-links` or `POST /invoices`; share the returned pay-page URL with your customer.\n4. Add a webhook endpoint in the dashboard and verify the signature on every delivery.\n5. When everything works in test, swap to your `spi_live_…` key — same code, same URL.",
    "contact": {
      "name": "SwiftPayIQ Support",
      "url": "https://swiftpayiq.com",
      "email": "info@swiftpayiq.com"
    }
  },
  "servers": [
    {
      "url": "https://api.swiftpayiq.com/api/v1",
      "description": "SwiftPayIQ API (test or live is selected by your API key)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Payment Links",
      "description": "Create shareable payment links. A link can be single-use or reusable, fixed-amount or open-amount (customer enters the amount, e.g. tips/donations)."
    },
    {
      "name": "Invoices",
      "description": "Create itemized invoices with line items, tax, and discounts. The invoice is issued immediately and the response returns a hosted pay-page URL (`payPage.url`) — share that link with your customer so they can pay on the SwiftPayIQ checkout page."
    },
    {
      "name": "Partner Connections",
      "description": "Connecting a merchant to your platform. You call this once per merchant, with your own partner key, to receive that merchant's API key. Everything after that uses the merchant key against the Payment Links and Invoices endpoints."
    },
    {
      "name": "Partner Merchants",
      "description": "Onboarding a merchant from your own signup flow. Creates an unclaimed account and returns a link for the merchant to claim it themselves."
    }
  ],
  "paths": {
    "/payment-links": {
      "post": {
        "tags": [
          "Payment Links"
        ],
        "summary": "Create a payment link",
        "description": "Creates a payment link and returns its full record, including `payPageUrl` — the shareable URL you send to the customer. Omit `amount` to create an open-amount link where the customer enters the amount at checkout.",
        "operationId": "createPaymentLink",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentLink"
              },
              "example": {
                "title": "Order #1042",
                "amount": "50000",
                "isReusable": false,
                "customerName": "Ahmed Ali",
                "customerPhone": "+9647801234567"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment link created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "get": {
        "tags": [
          "Payment Links"
        ],
        "summary": "List payment links",
        "description": "Returns a paginated list of payment links for the active business in the current mode.",
        "operationId": "listPaymentLinks",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/PaymentLinkStatus"
            },
            "description": "Filter by status."
          },
          {
            "name": "customerId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Filter by linked customer id."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated payment links",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedPaymentLinks"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/payment-links/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdPath"
        }
      ],
      "get": {
        "tags": [
          "Payment Links"
        ],
        "summary": "Fetch a payment link",
        "operationId": "getPaymentLink",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Payment link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Payment Links"
        ],
        "summary": "Update a payment link",
        "description": "Partially updates a payment link. Only the fields you send are changed.",
        "operationId": "updatePaymentLink",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePaymentLink"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated payment link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Payment Links"
        ],
        "summary": "Delete a payment link",
        "operationId": "deletePaymentLink",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:write"
            ]
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/payment-links/{id}/disable": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdPath"
        }
      ],
      "post": {
        "tags": [
          "Payment Links"
        ],
        "summary": "Disable a payment link",
        "description": "Disables a payment link so it can no longer be paid. Returns the updated record with `status: DISABLED`.",
        "operationId": "disablePaymentLink",
        "security": [
          {
            "ApiKeyAuth": [
              "payment_links:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Disabled payment link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/invoices": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Create an invoice",
        "description": "Creates an invoice from line items and issues it immediately. SwiftPayIQ does **not** message the customer for you over this API — there is no WhatsApp/SMS channel. Instead the response returns `payPage.url`, the hosted checkout page; deliver that link to your customer however you like (your own email/SMS, a redirect, a QR code, etc.). API-created invoices come back with `status: SENT`, `source: API`, and `deliveryChannel: API`.",
        "operationId": "createInvoice",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoice"
              },
              "example": {
                "customerName": "API-generated customer",
                "customerPhone": "+9647701112233",
                "items": [
                  {
                    "description": "SaaS subscription — monthly",
                    "quantity": 1,
                    "unitPrice": "45000"
                  }
                ],
                "discount": "5000",
                "taxAmount": "0",
                "notes": "Issued via server-to-server integration"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invoice created and issued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateInvoiceResult"
                },
                "example": {
                  "invoice": {
                    "id": "cmptoobsi0007xnlvtyx4iux2",
                    "number": "INV-000002",
                    "slug": "1tinjslkqp",
                    "isLive": false,
                    "customerId": null,
                    "customerName": "API-generated customer",
                    "customerPhone": "+9647701112233",
                    "status": "SENT",
                    "source": "API",
                    "deliveryChannel": "API",
                    "sentAt": "2026-05-31T11:15:33.387Z",
                    "subtotal": "45000",
                    "discount": "5000",
                    "discountPercentage": null,
                    "taxAmount": "0",
                    "customerFeeAmount": "0",
                    "total": "40000",
                    "amountPaid": "0",
                    "feeRate": "0",
                    "feeAmount": "0",
                    "customerFeeShare": "0.0000",
                    "netAmount": "40000",
                    "dueDate": null,
                    "issuedAt": "2026-05-31T11:15:33.387Z",
                    "paidAt": null,
                    "notes": "Issued via server-to-server integration",
                    "createdByType": "API_KEY",
                    "createdAt": "2026-05-31T11:15:33.426Z",
                    "updatedAt": "2026-05-31T11:15:33.426Z",
                    "items": [
                      {
                        "id": "cmptoobso0008xnlvz938stgm",
                        "productId": null,
                        "description": "SaaS subscription — monthly",
                        "quantity": 1,
                        "unitPrice": "45000",
                        "lineTotal": "45000",
                        "position": 0
                      }
                    ],
                    "payments": [],
                    "collections": []
                  },
                  "payPage": {
                    "url": "https://swiftpayiq.com/pay?slug=1tinjslkqp"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "List invoices",
        "description": "Returns a paginated list of invoices for the active business in the current mode, with optional status, customer, date-range, and sort filters.",
        "operationId": "listInvoices",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            },
            "description": "Filter by status."
          },
          {
            "name": "customerId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Filter by linked customer id."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Inclusive lower bound on createdAt (ISO-8601)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Inclusive upper bound on createdAt (ISO-8601)."
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "updatedAt",
                "dueDate",
                "issuedAt",
                "total",
                "number"
              ],
              "default": "createdAt"
            }
          },
          {
            "name": "sortDir",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated invoices",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedInvoices"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/invoices/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdPath"
        }
      ],
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Fetch an invoice",
        "operationId": "getInvoice",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Invoices"
        ],
        "summary": "Update an invoice",
        "description": "Partially updates an invoice. Only `DRAFT` invoices can be edited. Sending `items` replaces the full line-item list.",
        "operationId": "updateInvoice",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInvoice"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated invoice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Invoices"
        ],
        "summary": "Delete an invoice draft",
        "description": "Deletes a `DRAFT` invoice. Issued or paid invoices cannot be deleted — cancel them instead.",
        "operationId": "deleteInvoice",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:write"
            ]
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/invoices/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/IdPath"
        }
      ],
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Cancel an invoice",
        "description": "Cancels an issued or sent invoice. Returns the updated record with `status: CANCELLED`.",
        "operationId": "cancelInvoice",
        "security": [
          {
            "ApiKeyAuth": [
              "invoices:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled invoice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/partner/connections/exchange": {
      "post": {
        "tags": [
          "Partner Connections"
        ],
        "summary": "Swap a merchant's one-time code for their API key",
        "description": "The second half of the connection handover.\n\nThe merchant authorises your platform inside their SwiftPayIQ portal and is redirected back to you carrying a short-lived `code`. That code grants nothing on its own — your server swaps it here, authenticated with your **partner** key, and receives the merchant's own API key in return.\n\n**The merchant key is returned exactly once.** It is stored hashed and can never be retrieved again, so persist it before you respond to the request. Lose it and the merchant has to authorise you again.\n\nSingle use, and codes expire five minutes after they are issued.\n\n## A code does not always come back\n\nIf the merchant connects a business **they already had** — rather than one created through the setup page you sent them to — the link is recorded as **pending review** and waits for a SwiftPayIQ admin to approve it. Their consent is real and dated; what is missing is our decision.\n\nIn that case **no code is issued**, and the merchant returns to your redirect address **without** a `?code=` parameter. This is deliberate: an established merchant's business is not handed to a platform on the strength of one button press.\n\n**Handle the no-code return.** Treat it as \"waiting on SwiftPayIQ\", not as a failure and not as a finished connection. Send the merchant back through the setup page and you will get `partner.link_already_pending_review` — the request is already on file.",
        "security": [
          {
            "PartnerApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The one-time code from the merchant redirect.",
                    "example": "pcc_9f2a7c41e8b3d650"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Connected. Store `apiKey` now — it is never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiKey": {
                      "type": "string",
                      "description": "The merchant's API key. Returned once, never retrievable.",
                      "example": "spi_live_7d3f1a92c4e85b06..."
                    },
                    "businessId": {
                      "type": "string",
                      "example": "biz_2f8a91"
                    },
                    "businessName": {
                      "type": "string",
                      "description": "So you can label the connection in your own interface.",
                      "example": "متجر الأمل"
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Exactly what the merchant agreed to.",
                      "example": [
                        "payment_links:write",
                        "invoices:write",
                        "payments:read"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`partner.connection_code_invalid` — one message covers unknown, expired, already-used and issued-for-another-partner. They are deliberately not distinguished: telling you which codes exist would tell an attacker the same."
          },
          "401": {
            "description": "Missing or invalid partner key."
          },
          "429": {
            "description": "Rate limited. This endpoint allows 30 requests a minute."
          },
          "409": {
            "description": "`partner.link_already_pending_review` — this merchant already has a request waiting on our review for that business. Nothing to retry; wait for the decision.\n\n`partner.business_already_linked` — the business already belongs to a live link, either yours or another platform's. A business has one partner at a time."
          }
        }
      }
    },
    "/partner/merchants": {
      "post": {
        "tags": [
          "Partner Merchants"
        ],
        "summary": "Create a referred merchant from your own signup flow",
        "description": "For a platform whose merchants sign up on **your** side rather than ours.\n\nCreates an unverified account, a DRAFT business and a pending link, and returns a **claim URL** to send the merchant.\n\n**What it does not do:** issue a password, send a one-time code, or earn you anything yet. The merchant opens the claim URL, sets their own password and verifies their own phone — and that verification is the only thing that activates the link and starts commission. You cannot consent on their behalf.\n\nAn existing SwiftPayIQ phone number is refused rather than claimed: invite that merchant through the connection flow instead.",
        "security": [
          {
            "PartnerApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone",
                  "name",
                  "attestedConsent",
                  "business"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "description": "The merchant's own phone, E.164.",
                    "example": "+9647701234567"
                  },
                  "name": {
                    "type": "string",
                    "example": "اسم صاحب النشاط"
                  },
                  "email": {
                    "type": "string",
                    "nullable": true
                  },
                  "attestedConsent": {
                    "type": "boolean",
                    "description": "You are asserting the merchant asked you to do this. Recorded on the consent trail against your partner account, with the key on the audit entry. Required.",
                    "example": true
                  },
                  "business": {
                    "type": "object",
                    "required": [
                      "type",
                      "commercialNameAr",
                      "governorate",
                      "address",
                      "businessPhone",
                      "settlementMethod"
                    ],
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "COMPANY",
                          "SOLE_MERCHANT"
                        ]
                      },
                      "commercialNameAr": {
                        "type": "string",
                        "example": "متجر الأمل"
                      },
                      "commercialNameEn": {
                        "type": "string",
                        "nullable": true
                      },
                      "governorate": {
                        "type": "string",
                        "example": "BAGHDAD"
                      },
                      "address": {
                        "type": "string"
                      },
                      "businessPhone": {
                        "type": "string",
                        "example": "07711111111"
                      },
                      "settlementMethod": {
                        "type": "string",
                        "enum": [
                          "MERCHANT_CARD",
                          "BANK_ACCOUNT"
                        ]
                      },
                      "description": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Send `claimUrl` to the merchant — nothing happens until they use it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "referredOwnerAccountId": {
                      "type": "string"
                    },
                    "businessId": {
                      "type": "string"
                    },
                    "linkId": {
                      "type": "string"
                    },
                    "claimUrl": {
                      "type": "string",
                      "example": "https://my.swiftpayiq.com/register?phone=%2B9647701234567"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`partner.consent_not_attested` — `attestedConsent` was not true."
          },
          "401": {
            "description": "Missing or invalid partner key."
          },
          "409": {
            "description": "`partner.phone_in_use` — that phone already belongs to a SwiftPayIQ account; invite them through the connection flow instead. `partner.too_many_unclaimed` — 100 accounts you created are still unclaimed."
          },
          "429": {
            "description": "Rate limited. 20 requests an hour."
          }
        }
      }
    },
    "/partner/onboarding-sessions": {
      "post": {
        "tags": [
          "Partner Merchants"
        ],
        "summary": "Send a merchant to a setup page we host",
        "description": "For a platform that would rather not build sign-up, one-time codes and document upload itself.\n\nReturns a single-use URL. Open it for the merchant in a new tab; every screen that touches a password or a one-time code stays on our pages, so the merchant's credentials never pass through your servers.\n\n**The page reads the merchant's own records each time** rather than storing a step number, so somebody who stops halfway through uploading documents and comes back later lands on exactly the same screen.\n\n**Register a return address first.** Without one this call fails with `partner.no_redirect_uri` — a journey that ends nowhere is not a finished journey. Ask our team to set it for your account.\n\nWhen the merchant finishes we send them to that address with `?code=`, the same one-time code described on `POST /partner/connections/exchange`, swapped the same way from your server.\n\n## Two ways the journey can end\n\n**A business created through this page** is yours to connect: the merchant presses connect and comes back to you with `?code=`.\n\n**A business the merchant already had** goes to review instead. They come back with no `?code=`, and a SwiftPayIQ admin decides. Build for both endings — the second one is the normal case for any merchant who was already trading with us.",
        "security": [
          {
            "PartnerApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "example": "+9647701234567",
                    "description": "The merchant's own phone, E.164. The one-time code goes here, which is what stops you completing this on their behalf."
                  },
                  "prefill": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Anything you already know, to save them typing: trade name, governorate. Convenience only — they can change all of it and none of it is trusted.",
                    "example": {
                      "commercialNameAr": "متجر الأمل",
                      "governorate": "BAGHDAD"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Link created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "example": "https://my.swiftpayiq.com/setup/DWmRNc4Fp_GwHR5eq75PLadRuPVCtdzG7uBvsxbTD-A",
                      "description": "Single use, and it expires. Mint a fresh one rather than storing it."
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`phone` is not a valid Iraqi mobile in E.164."
          },
          "401": {
            "description": "Missing or invalid partner key."
          },
          "409": {
            "description": "`partner.no_redirect_uri` — no return address registered for your account."
          }
        }
      }
    },
    "/partner/connections/pending": {
      "get": {
        "tags": [
          "Partner Connections"
        ],
        "summary": "Merchants an admin approved that you have not collected yet",
        "description": "The other half of the review flow.\n\nWhen a merchant connects a business they already had, the link waits for a SwiftPayIQ admin. That decision happens out of band, days after the merchant consented, so there is no redirect to carry a code back to you. **This is where your server finds out.**\n\nEach entry carries a fresh one-time code. Swap it at `/partner/connections/exchange` exactly as you would one that arrived by redirect.\n\n**Codes are minted per call and live five minutes**, so call this when you are ready to exchange rather than on a slow timer. An entry disappears once you hold a key for that merchant, so a run that fails halfway is safe to repeat — you will simply be offered the ones you did not finish.\n\nAn empty array means nothing is waiting. Poll it a few times a day, or when the `PARTNER_CONNECTION_READY` notification reaches your SwiftPayIQ inbox.",
        "security": [
          {
            "PartnerApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Approved merchants awaiting collection. Empty when there are none.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "businessId",
                      "businessName",
                      "code",
                      "scopes",
                      "expiresAt"
                    ],
                    "properties": {
                      "businessId": {
                        "type": "string"
                      },
                      "businessName": {
                        "type": "string",
                        "description": "English trading name where there is one, otherwise Arabic."
                      },
                      "code": {
                        "type": "string",
                        "description": "One-time code. Swap it at /partner/connections/exchange."
                      },
                      "scopes": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "What the merchant granted, frozen at consent."
                      },
                      "expiresAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Five minutes out. Ask again if you miss it — nothing is lost."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid partner key."
          },
          "429": {
            "description": "Rate limited — 30 requests a minute."
          }
        }
      }
    }
  },
  "webhooks": {
    "payment.succeeded": {
      "post": {
        "summary": "payment.succeeded",
        "description": "Sent when a payment completes successfully. This is the event most integrations care about — fulfil the order when you receive it (after verifying the signature).\n\n## How webhooks are delivered\n\nSwiftPayIQ sends an HTTP `POST` to the endpoint URL you configured in the dashboard, with a JSON body of the shape:\n\n```json\n{ \"event\": \"payment.succeeded\", \"data\": { /* … */ } }\n```\n\nHeaders on every delivery:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `X-SwiftPay-Signature` | `t=<unix-seconds>,v1=<hex-hmac>` |\n\nYour endpoint should respond with any `2xx` status as quickly as possible. A non-2xx response (or a timeout — the request is aborted after **10 seconds**) is treated as a failure and retried.\n\n## Verifying the signature\n\nEvery delivery is signed with the **signing secret** of your webhook (shown when you create or rotate it in the dashboard; it starts with `whsec_`). Verify it before trusting the payload:\n\n1. Read the `X-SwiftPay-Signature` header and parse the `t` (timestamp) and `v1` (signature) values.\n2. Reject the request if `t` is more than **5 minutes** away from your current time (replay protection).\n3. Build the signed string: `\"{t}.{rawRequestBody}\"` — use the **raw, unparsed** request body bytes exactly as received.\n4. Compute `HMAC-SHA256(signingSecret, signedString)` and hex-encode it.\n5. Compare it to `v1` using a constant-time comparison. If they match, the request is authentic.\n\n```js\n// Node.js (Express) example\nconst crypto = require('crypto');\n\nfunction verify(rawBody, header, secret) {\n  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));\n  const t = parts.t, v1 = parts.v1;\n  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min\n  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');\n  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));\n}\n```\n\n> Capture the **raw** request body for signature verification — re-serializing parsed JSON will change the bytes and the signature will not match.\n\n## Retries\n\nIf your endpoint doesn't return `2xx`, delivery is retried up to **6 attempts total** (5 retries) with increasing back-off: **1 min → 5 min → 30 min → 2 hours → 8 hours** (about 10.5 hours end-to-end). A `4xx` response other than `408`/`429` is treated as a permanent failure and is **not** retried. Make your handler **idempotent** — the same event may arrive more than once. You can inspect delivery history and re-send from the dashboard.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "payment.succeeded",
                "data": {
                  "paymentId": "9b2c0e1a-1f3d-4a55-8c2e-1a2b3c4d5e6f",
                  "businessId": "3a1d…",
                  "isLive": true,
                  "amount": "50000",
                  "status": "SUCCEEDED",
                  "provider": "ALQASEH",
                  "gatewayTxnId": "alq_8f21c…",
                  "invoiceId": null,
                  "paymentLinkId": "7c9e…",
                  "customerId": null,
                  "customerPhone": "+9647801234567"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge receipt."
          }
        }
      }
    },
    "payment.failed": {
      "post": {
        "summary": "payment.failed",
        "description": "Sent when a payment attempt fails. Same delivery, signing, and retry rules as `payment.succeeded`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "payment.failed",
                "data": {
                  "paymentId": "…",
                  "status": "FAILED",
                  "amount": "50000",
                  "isLive": true,
                  "provider": "ALQASEH",
                  "customerPhone": "+9647801234567",
                  "invoiceId": null,
                  "paymentLinkId": "7c9e…",
                  "customerId": null,
                  "gatewayTxnId": "alq_…",
                  "businessId": "3a1d…"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge receipt."
          }
        }
      }
    },
    "payment.refunded": {
      "post": {
        "summary": "payment.refunded",
        "description": "Sent when a previously successful payment is refunded. Same delivery, signing, and retry rules as `payment.succeeded`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "payment.refunded",
                "data": {
                  "paymentId": "…",
                  "status": "REFUNDED",
                  "amount": "50000",
                  "isLive": true,
                  "provider": "ALQASEH",
                  "customerPhone": "+9647801234567",
                  "invoiceId": null,
                  "paymentLinkId": "7c9e…",
                  "customerId": null,
                  "gatewayTxnId": "alq_…",
                  "businessId": "3a1d…"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge receipt."
          }
        }
      }
    },
    "invoice.paid": {
      "post": {
        "summary": "invoice.paid",
        "description": "Sent when an invoice is fully paid. Only fired for payments linked to an invoice — `data.invoiceId` is populated. Same delivery, signing, and retry rules as `payment.succeeded`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "invoice.paid",
                "data": {
                  "paymentId": "…",
                  "status": "SUCCEEDED",
                  "amount": "85000",
                  "isLive": true,
                  "provider": "ALQASEH",
                  "customerPhone": "+9647801234567",
                  "invoiceId": "f01a…",
                  "paymentLinkId": null,
                  "customerId": null,
                  "gatewayTxnId": "alq_…",
                  "businessId": "3a1d…"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge receipt."
          }
        }
      }
    },
    "payment_link.paid": {
      "post": {
        "summary": "payment_link.paid",
        "description": "Sent when a payment link is paid. Only fired for payments linked to a payment link — `data.paymentLinkId` is populated. Same delivery, signing, and retry rules as `payment.succeeded`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "event": "payment_link.paid",
                "data": {
                  "paymentId": "…",
                  "status": "SUCCEEDED",
                  "amount": "50000",
                  "isLive": true,
                  "provider": "ALQASEH",
                  "customerPhone": "+9647801234567",
                  "invoiceId": null,
                  "paymentLinkId": "7c9e…",
                  "customerId": null,
                  "gatewayTxnId": "alq_…",
                  "businessId": "3a1d…"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge receipt."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your secret API key as a Bearer token: `Authorization: Bearer spi_live_…` (or `spi_test_…`)."
      },
      "PartnerApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your **partner** key as a Bearer token: `Authorization: Bearer spp_live_…`.\n\nThis is not a merchant key. It identifies your platform, and it is the only thing that can exchange a merchant's connection code — which is what makes an intercepted code useless on its own. It is issued to you by SwiftPayIQ; you cannot create one yourself."
      }
    },
    "parameters": {
      "IdPath": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Resource id."
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        },
        "description": "Page number (1-based)."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "description": "Items per page (max 100)."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, revoked, or wrong-mode API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The API key lacks the required scope, or the business is not KYC-approved.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found in the active business and mode.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Request body or query failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "statusCode": {
            "type": "integer",
            "example": 400
          },
          "message": {
            "description": "A string, or an array of validation messages.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "example": [
              "amount must be a positive integer string (IQD)."
            ]
          },
          "error": {
            "type": "string",
            "example": "Bad Request"
          }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "required": [
          "total",
          "page",
          "limit",
          "pageCount",
          "hasNextPage",
          "hasPreviousPage"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "example": 137
          },
          "page": {
            "type": "integer",
            "example": 1
          },
          "limit": {
            "type": "integer",
            "example": 20
          },
          "pageCount": {
            "type": "integer",
            "example": 7
          },
          "hasNextPage": {
            "type": "boolean",
            "example": true
          },
          "hasPreviousPage": {
            "type": "boolean",
            "example": false
          }
        }
      },
      "Money": {
        "type": "string",
        "description": "An amount in IQD (no decimals), serialized as an integer string to preserve precision.",
        "example": "50000"
      },
      "IraqiPhone": {
        "type": "string",
        "description": "A valid Iraqi phone number in E.164 form.",
        "example": "+9647801234567"
      },
      "PaymentLinkStatus": {
        "type": "string",
        "enum": [
          "ACTIVE",
          "DISABLED",
          "EXPIRED"
        ]
      },
      "PaymentStatus": {
        "type": "string",
        "enum": [
          "PENDING",
          "SUCCEEDED",
          "FAILED",
          "REFUNDED"
        ]
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": [
          "DRAFT",
          "ISSUED",
          "SENT",
          "PAID",
          "PARTIALLY_PAID",
          "OVERDUE",
          "CANCELLED"
        ]
      },
      "InvoiceSource": {
        "type": "string",
        "enum": [
          "WEB",
          "MOBILE",
          "API"
        ],
        "description": "How the invoice was created. Invoices created via this API are always `API`."
      },
      "InvoiceDeliveryChannel": {
        "type": "string",
        "enum": [
          "WHATSAPP",
          "SMS",
          "API",
          "NONE"
        ],
        "description": "How the pay-link was delivered. Invoices created via this API are always `API` (the response returns the pay-page URL; SwiftPayIQ does not message the customer)."
      },
      "CreatePaymentLink": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Short title shown to the customer on the checkout page."
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Amount in IQD. Omit for an open-amount link (customer picks the amount — tip jars / donations)."
          },
          "isReusable": {
            "type": "boolean",
            "default": false,
            "description": "If true, the link can be paid more than once. Default: single-use."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Optional expiry. ISO-8601 (YYYY-MM-DD or full timestamp)."
          },
          "customerId": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional existing customer id."
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "customerPhone": {
            "$ref": "#/components/schemas/IraqiPhone"
          }
        }
      },
      "UpdatePaymentLink": {
        "type": "object",
        "description": "All fields optional — only the ones you send are changed.",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "amount": {
            "$ref": "#/components/schemas/Money"
          },
          "isReusable": {
            "type": "boolean"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "customerId": {
            "type": "string",
            "maxLength": 64
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "customerPhone": {
            "$ref": "#/components/schemas/IraqiPhone"
          }
        }
      },
      "PaymentLinkPayment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "description": "Payment gateway.",
            "example": "ALQASEH"
          },
          "gatewayTxnId": {
            "type": "string",
            "description": "Gateway transaction id."
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Gross amount charged."
          },
          "feeAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Gateway fee on SUCCEEDED rows, else \"0\"."
          },
          "netAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Net to merchant on SUCCEEDED rows, else \"0\"."
          },
          "paidAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaymentLink": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "payPageUrl": {
            "type": "string",
            "description": "Full shareable checkout URL — send this to the customer.",
            "example": "https://swiftpayiq.com/pay?slug=xa7fi5brv1"
          },
          "isLive": {
            "type": "boolean"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentLinkStatus"
          },
          "isReusable": {
            "type": "boolean"
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "nullable": true,
            "description": "Customer-facing amount, or null for open-amount links."
          },
          "feeRate": {
            "type": "string",
            "description": "Gateway fee rate snapshot, as a percentage with up to 5 decimals.",
            "example": "0.60325"
          },
          "feeAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Gateway's cut in IQD."
          },
          "customerFeeShare": {
            "type": "string",
            "description": "Fraction of the fee the customer paid (0..1).",
            "example": "0.5000"
          },
          "netAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "nullable": true,
            "description": "Net to merchant, or null for open-amount links."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "customerId": {
            "type": "string",
            "nullable": true
          },
          "customerName": {
            "type": "string",
            "nullable": true
          },
          "customerPhone": {
            "type": "string",
            "nullable": true
          },
          "createdByType": {
            "type": "string",
            "description": "Actor that created the link.",
            "example": "API_KEY"
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentLinkPayment"
            }
          },
          "sentAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "lastSentAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "sendCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaginatedPaymentLinks": {
        "type": "object",
        "required": [
          "items",
          "meta"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentLink"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "InvoiceItemInput": {
        "type": "object",
        "required": [
          "description",
          "quantity",
          "unitPrice"
        ],
        "properties": {
          "productId": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional pointer to a catalog product. Description and unit price are still snapshotted per line."
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "unitPrice": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Unit price in IQD.",
            "example": "25000"
          }
        }
      },
      "CreateInvoice": {
        "type": "object",
        "required": [
          "customerName",
          "customerPhone",
          "items"
        ],
        "properties": {
          "customerId": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional existing customer id."
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Snapshot kept on the invoice even if the customer row changes later."
          },
          "customerPhone": {
            "$ref": "#/components/schemas/IraqiPhone"
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/InvoiceItemInput"
            }
          },
          "taxAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Extra tax on top of the subtotal. Default \"0\".",
            "example": "0"
          },
          "discount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Fixed IQD discount. Mutually exclusive with discountPercentage.",
            "example": "0"
          },
          "discountPercentage": {
            "type": "string",
            "description": "Percentage discount (0–99.99, up to 2 decimals). Mutually exclusive with discount.",
            "example": "10"
          },
          "dueDate": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 (YYYY-MM-DD or full timestamp)."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "UpdateInvoice": {
        "type": "object",
        "description": "All fields optional. Only DRAFT invoices can be updated. Sending `items` replaces the full list.",
        "properties": {
          "customerId": {
            "type": "string",
            "maxLength": 64
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "customerPhone": {
            "$ref": "#/components/schemas/IraqiPhone"
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/InvoiceItemInput"
            }
          },
          "taxAmount": {
            "$ref": "#/components/schemas/Money"
          },
          "discount": {
            "$ref": "#/components/schemas/Money"
          },
          "discountPercentage": {
            "type": "string",
            "example": "10"
          },
          "dueDate": {
            "type": "string",
            "format": "date-time"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        }
      },
      "InvoiceItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "productId": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "unitPrice": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "example": "25000"
          },
          "lineTotal": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "example": "50000"
          },
          "position": {
            "type": "integer"
          }
        }
      },
      "InvoicePayment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "example": "ALQASEH"
          },
          "gatewayTxnId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "amount": {
            "$ref": "#/components/schemas/Money"
          },
          "feeAmount": {
            "$ref": "#/components/schemas/Money"
          },
          "netAmount": {
            "$ref": "#/components/schemas/Money"
          },
          "collectionId": {
            "type": "string",
            "nullable": true,
            "description": "Linked installment-collection id, if any."
          },
          "paidAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string",
            "description": "Human-readable invoice number."
          },
          "slug": {
            "type": "string"
          },
          "isLive": {
            "type": "boolean"
          },
          "customerId": {
            "type": "string",
            "nullable": true
          },
          "customerName": {
            "type": "string"
          },
          "customerPhone": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "source": {
            "$ref": "#/components/schemas/InvoiceSource"
          },
          "deliveryChannel": {
            "$ref": "#/components/schemas/InvoiceDeliveryChannel"
          },
          "sentAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "subtotal": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Sum of line totals."
          },
          "discount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Resolved IQD discount applied."
          },
          "discountPercentage": {
            "type": "string",
            "nullable": true,
            "description": "Percentage that produced the discount; null for fixed discounts.",
            "example": "10.00"
          },
          "taxAmount": {
            "$ref": "#/components/schemas/Money"
          },
          "customerFeeAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Portion of the gateway fee absorbed by the customer (already baked into total)."
          },
          "total": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Customer-facing total."
          },
          "amountPaid": {
            "$ref": "#/components/schemas/Money"
          },
          "feeRate": {
            "type": "string",
            "description": "Gateway fee rate snapshot at issue time, as a percentage.",
            "example": "0.60325"
          },
          "feeAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Gateway's cut in IQD."
          },
          "customerFeeShare": {
            "type": "string",
            "description": "Fraction of the fee the customer paid (0..1).",
            "example": "0.5000"
          },
          "netAmount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "What the merchant nets — total − feeAmount."
          },
          "dueDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "paidAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "createdByType": {
            "type": "string",
            "example": "API_KEY"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceItem"
            }
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoicePayment"
            }
          },
          "collections": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Installment collections linked to this invoice. Empty for invoices charged in full."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayPage": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Hosted pay-page URL — redirect the customer here to complete payment.",
            "example": "https://swiftpayiq.com/pay?slug=xa7fi5brv1"
          }
        }
      },
      "CreateInvoiceResult": {
        "type": "object",
        "required": [
          "invoice",
          "payPage"
        ],
        "properties": {
          "invoice": {
            "$ref": "#/components/schemas/Invoice"
          },
          "payPage": {
            "$ref": "#/components/schemas/PayPage"
          }
        }
      },
      "PaginatedInvoices": {
        "type": "object",
        "required": [
          "items",
          "meta"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "WebhookEventData": {
        "type": "object",
        "description": "The payment that triggered the event. Amounts are IQD integer strings.",
        "properties": {
          "paymentId": {
            "type": "string"
          },
          "businessId": {
            "type": "string"
          },
          "isLive": {
            "type": "boolean",
            "description": "true = live mode, false = test mode."
          },
          "amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "description": "Gross customer charge."
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "provider": {
            "type": "string",
            "example": "ALQASEH"
          },
          "gatewayTxnId": {
            "type": "string",
            "description": "Gateway's transaction id."
          },
          "invoiceId": {
            "type": "string",
            "nullable": true,
            "description": "Set when the payment is tied to an invoice."
          },
          "paymentLinkId": {
            "type": "string",
            "nullable": true,
            "description": "Set when the payment is tied to a payment link."
          },
          "customerId": {
            "type": "string",
            "nullable": true
          },
          "customerPhone": {
            "type": "string",
            "description": "Phone of the customer who paid."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "event",
          "data"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "payment.succeeded",
              "payment.failed",
              "payment.refunded",
              "invoice.paid",
              "payment_link.paid"
            ],
            "description": "The event type."
          },
          "data": {
            "$ref": "#/components/schemas/WebhookEventData"
          }
        }
      }
    }
  }
}
