{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "FahiPay Merchant API",
    "version": "2027-07-01",
    "description": "Accept payments through hosted checkout links and customer-approved authorizations. Create payment links, request customer approval for future payments, charge approved authorizations, and retrieve payment history.\n\nThis documentation covers API version **2027-07-01**. Compatible additions may be introduced within this version. Breaking changes will receive a new version. Integrations should ignore unrecognized response fields. Send the optional `FahiPay-Version: 2027-07-01` request header to select this version. If omitted, the server uses the latest API version. The `FahiPay-Version` response header identifies the version used. Unsupported or empty version values return HTTP 400 with `unsupported_api_version`. Pin a version when you want to control when your integration adopts a new release.\n\nGet your merchant ID and secret_key from [the FahiPay merchant portal](https://fahipay.mv/merchants/portal/). Choose the authentication method that suits your integration:\n\n- **API key:** a straightforward option for server-to-server integrations that need full Merchant API access. Send `X-API-Key: YOUR_SECRET_KEY`; no merchant ID or prefix is needed.\n- **HTTP Basic:** another full-access option for clients that support Basic authentication. Use your merchant_id as the username and secret_key as the password.\n- **Bearer token:** use an OAuth access token when an integration should have access only to selected operations. Send `Authorization: Bearer YOUR_ACCESS_TOKEN`. The token must include the scope required by the endpoint.\n\nUse HTTPS and send one authentication method per request. Authentication precedence is Bearer, then X-API-Key, then HTTP Basic. If the selected method fails, the request returns HTTP 401 without falling back to another method. Your credentials identify your merchant account and its current live or sandbox mode. An active merchant account is required for authenticated operations. Keep secret keys and access tokens on your server.\n\n### Obtain an access token\n\nUse the Client Credentials flow with your OAuth client ID and client secret. The token endpoint is `https://api.fahipay.mv/merchants/oauth/token`. Send a form-encoded POST and request only the scopes your integration needs, separated by spaces:\n\n```sh\ncurl --request POST 'https://api.fahipay.mv/merchants/oauth/token' \\\n  --user 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' \\\n  --header 'Content-Type: application/x-www-form-urlencoded' \\\n  --data-urlencode 'grant_type=client_credentials' \\\n  --data-urlencode 'scope=payments:read payments:create'\n```\n\nUse the returned `access_token` in the Bearer header on subsequent merchant requests. Keep the token and client secret on your server.\n\nIn Postman, choose **OAuth 2.0**, select **Client Credentials**, set **Access Token URL** to `https://api.fahipay.mv/merchants/oauth/token`, enter your OAuth client credentials and required scopes, and choose **Send as Basic Auth header** for client authentication. If you already have an access token, choose **Bearer Token** and enter the token without the `Bearer` prefix.\n\n### Endpoint scopes\n\nAPI key and HTTP Basic authentication grant all Merchant API scopes. Bearer access tokens grant only their assigned scopes. Each scope is independent: permission to create an authorization does not grant permission to charge it. Payment history under an authorization requires `payments:read`. Tokens with no recognized scopes cannot access authenticated endpoints. Scopes do not change merchant ownership checks or the account's current live or sandbox mode. The public QR endpoint requires no scope.\n\n| Scope | Endpoints |\n| --- | --- |\n| `payments:create` | `POST /payments` |\n| `payments:read` | `GET /payments`, `GET /payments/{id}`, `GET /authorizations/{id}/payments` |\n| `payments:cancel` | `POST /payments/{id}/cancel` |\n| `payment_requests:create` | `POST /payment-requests` |\n| `payment_requests:read` | `GET /payment-requests`, `GET /payment-requests/{id}` |\n| `payment_requests:cancel` | `POST /payment-requests/{id}/cancel` |\n| `authorizations:create` | `POST /authorizations` |\n| `authorizations:read` | `GET /authorizations`, `GET /authorizations/{id}` |\n| `authorizations:charge` | `POST /authorizations/{id}/charge` |\n| `authorizations:cancel` | `POST /authorizations/{id}/cancel` (cancel pending approval or revoke an active authorization) |\n\nA valid credential without the required scope receives HTTP 403 with error code `insufficient_scope`. The error message identifies the required scope. Use a token granted that permission before retrying. Invalid or expired tokens receive HTTP 401.\n\nSend request bodies as JSON objects. Express amounts and monetary limits as integer laari: 100 laari = MVR 1.00. For example, use 2000 for MVR 20.00. Payment responses use flat amount, fee, and net_amount fields in laari, with MVR implicit. Fee and net_amount are null until paid. After payment, fee is a non-negative integer and net_amount equals amount minus fee. Timestamps include a timezone offset; expiry values are returned in +05:00.\n\nUse your reference to identify a payment or authorization request. Payment references must be unique across all payments for your merchant, including hosted payments and authorization charges. When retrying a payment request, keep the original reference and amount. List endpoints support cursor pagination: pass pagination.next_cursor as after to retrieve the next page.\n\nAPI responses include Cache-Control: no-store.\n\n### Webhook delivery\n\nYour webhook URL must use HTTPS and a public domain name with a valid TLS certificate. The domain must resolve exclusively to public IPv4 or IPv6 addresses. IP-address URLs, localhost, embedded credentials, and URL fragments are not supported. Redirects are not followed. Webhook requests include User-Agent: FahiPay-Merchant-Webhook/1.0.\n\n`payload_version` identifies the webhook payload format. The current version is `1` and stays the same across events and delivery retries. A new version would indicate a breaking change to the payload format. New optional fields can be added within the same version, so integrations should ignore fields they do not recognize.\n\n### Verify webhook signatures\n\nVerify each webhook before processing it. The `FahiPay-Signature` header has the form `t=<unix_timestamp>,v1=<hex_signature>`. The signature is HMAC-SHA256 over the timestamp, a literal dot, and the exact raw HTTP request body, using your merchant API `secret_key`.\n\n`v1` identifies the signature scheme: HMAC-SHA256 over the timestamp, a dot, and the raw request body. It stays `v1` across events and delivery retries. A new signature version would indicate a change to the signing or verification rules. This label is separate from the event's `payload_version`.\n\nRead the body before decoding JSON. Reformatting, reordering fields or re-encoding JSON changes the signed bytes. Use `hash_equals()` for the comparison. The PHP example below rejects timestamps more than five minutes in the past or future; keep your server clock accurate.\n\n```php\nfunction verifyFahiPaySignature(string $body, string $header, string $secret): bool\n{\n    if ($secret === '' || !preg_match('/\\At=([0-9]{1,12}),v1=([a-f0-9]{64})\\z/D', $header, $parts))\n    {\n        return false;\n    }\n    if (abs(time() - (int) $parts[1]) > 300)\n    {\n        return false;\n    }\n\n    $expected = hash_hmac('sha256', $parts[1] . '.' . $body, $secret);\n    return hash_equals($expected, $parts[2]);\n}\n```\n\nUse it in your webhook endpoint. A missing or invalid signature returns HTTP 401. Configure FAHIPAY_SECRET_KEY on your server before using this example:\n\n```php\n$body = file_get_contents('php://input');\n$header = $_SERVER['HTTP_FAHIPAY_SIGNATURE'] ?? '';\n$secret = getenv('FAHIPAY_SECRET_KEY'); // Your merchant API secret_key.\n\nif ($body === false || $secret === false || !verifyFahiPaySignature($body, $header, $secret))\n{\n    http_response_code(401);\n    exit('Invalid webhook signature.');\n}\n\n$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);\n// Persist the event and deduplicate $event['id'] before processing it.\n// Return a 2xx response only after durable acceptance or for an already accepted event.\n```\n\nStore your `secret_key` securely on your server. Never accept the verification key from the webhook request. Deduplicate using the payload's `id`; the timestamp check alone does not prevent repeated delivery within the allowed time window.\n\n### Request a feature\n\nMissing an API feature? Email [info@fahipay.mv](mailto:info@fahipay.mv?subject=FahiPay%20API%20feature%20request) with a short description of what you are building, the feature you need, and how it would help your integration.\n\n### Example URLs\n\nFahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.\n\n### Merchant error handling\n\nMerchant API errors contain error.code and error.message. Validation errors may also contain error.field. Branch on the stable code rather than parsing the human-readable message. For unexpected server errors, use the error code and request details when contacting support. A timeout or HTTP 500 does not establish whether a payment succeeded; retain the original reference and amount when retrying."
  },
  "servers": [
    {
      "url": "https://api.fahipay.mv/merchants",
      "description": "FahiPay merchant API"
    }
  ],
  "tags": [
    {
      "name": "OAuth",
      "description": "Obtain scoped access tokens."
    },
    {
      "name": "Payments",
      "description": "Create hosted checkout links and retrieve payment details and history."
    },
    {
      "name": "Payment Requests",
      "description": "Create payment requests for customers by mobile, email, or FahiPay VPA."
    },
    {
      "name": "Authorizations",
      "description": "Use authorizations to collect payments with customer approval and agreed limits on spending and the number of payments.\n\nCommon use cases include:\n\n- **Subscriptions and memberships** (`recurring`): collect payments for monthly services, software subscriptions, or membership renewals.\n- **Instalment plans** (`recurring`): collect a series of payments, with limits on the total amount and number of successful charges.\n- **Usage-based billing and repeat purchases** (`unscheduled`): charge for services used or orders placed at irregular intervals, within the approved limits.\n- **One-time payments** (`one_time`): collect one agreed payment, such as an order charged when it is ready to fulfil.\n\nCreate an authorization and direct the customer to approval_url to review and approve it. Once the authorization is active, submit each payment through POST /authorizations/{id}/charge. Your application controls the billing schedule and submits each subscription renewal or other charge when it is due."
    }
  ],
  "security": [
    {
      "MerchantBearer": []
    },
    {
      "MerchantApiKey": []
    },
    {
      "MerchantBasic": []
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Get access token",
        "operationId": "getAccessToken",
        "description": "Obtain a Bearer access token using your OAuth client credentials. Request only the scopes your integration needs.\n\nUse the returned token in `Authorization: Bearer YOUR_ACCESS_TOKEN`. Tokens expire after 24 hours.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "default": "client_credentials"
                  },
                  "client_id": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "OAuth client ID. Required when not using HTTP Basic."
                  },
                  "client_secret": {
                    "type": "string",
                    "format": "password",
                    "maxLength": 256,
                    "description": "OAuth client secret. Required when not using HTTP Basic."
                  },
                  "scope": {
                    "type": "string",
                    "description": "Space-separated scopes. Omit to use the defaults configured for your OAuth client/server. Check the granted scope in the response.",
                    "example": "payments:read payments:create"
                  }
                }
              },
              "example": {
                "grant_type": "client_credentials",
                "client_id": "YOUR_CLIENT_ID",
                "client_secret": "YOUR_CLIENT_SECRET",
                "scope": "payments:read payments:create"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access token issued. Store it securely.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              },
              "Pragma": {
                "schema": {
                  "type": "string"
                },
                "example": "no-cache"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "access_token",
                    "token_type",
                    "expires_in"
                  ],
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string"
                    },
                    "expires_in": {
                      "type": "integer"
                    },
                    "scope": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "access_token": "YOUR_ACCESS_TOKEN",
                  "token_type": "Bearer",
                  "expires_in": 86400,
                  "scope": "payments:read payments:create"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, grant type or scope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "invalid_scope",
                  "error_description": "Invalid request, grant type or scope."
                }
              }
            }
          },
          "401": {
            "description": "Invalid OAuth client credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "invalid_client",
                  "error_description": "Invalid OAuth client credentials."
                }
              }
            }
          },
          "403": {
            "description": "HTTPS is required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "invalid_request",
                  "error_description": "HTTPS is required."
                }
              }
            }
          },
          "405": {
            "description": "Use POST.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "invalid_request",
                  "error_description": "Use POST."
                }
              }
            }
          }
        }
      }
    },
    "/payments": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "List Payments",
        "operationId": "listPayments",
        "description": "List your payments in the current live or sandbox mode, newest first.\n\n- Combine the filters below to narrow the results. Keep them unchanged when loading the next page.\n- Returned pending payments past their deadline are saved as `expired`, unless a payment attempt is still processing. Each expiry emits `payment.expired` once when webhooks are enabled.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          },
          {
            "$ref": "#/components/parameters/PaymentStatus"
          },
          {
            "$ref": "#/components/parameters/PaymentTerminalLabel"
          },
          {
            "$ref": "#/components/parameters/PaymentBillNumber"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedFrom"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Payments and pagination. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "a1234567890b4cde8f0123456789abcd",
                      "reference": "ORDER-2026-001",
                      "amount": 2000,
                      "fee": null,
                      "net_amount": null,
                      "title": "Order #001",
                      "description": "Online order",
                      "terminal_label": "COUNTER01",
                      "bill_number": "BILL-2026-001",
                      "status": "pending",
                      "payment_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                      "payment_reference": null,
                      "qr_url": "https://api.fahipay.mv/merchants/public/qr/YOUR_PAYMENT_ID",
                      "return_url": "https://merchant.example/payment/complete",
                      "created_at": "2026-01-01T12:00:00+05:00",
                      "paid_at": null,
                      "expires_at": "2026-12-31T23:59:59+05:00",
                      "metadata": {}
                    }
                  ],
                  "pagination": {
                    "limit": 20,
                    "has_more": false,
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "payments:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payments:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      },
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create Payment",
        "operationId": "createPayment",
        "description": "Create a hosted checkout link and a PayMV QR link for a customer payment.\n\n- Send the customer to `payment_url`, or display the image at `qr_url`.\n- Payments expire after 24 hours unless you set `expires_at`.\n- Confirm the payment is `paid` using the API or a verified webhook before fulfilling the order.",
        "requestBody": {
          "required": true,
          "description": "A JSON object. Express monetary amounts and limits as integer laari (100 laari = MVR 1.00).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              },
              "examples": {
                "required": {
                  "summary": "Required fields only",
                  "value": {
                    "reference": "ORDER-2026-001",
                    "amount": 2000
                  }
                },
                "optional": {
                  "summary": "With optional fields",
                  "value": {
                    "reference": "ORDER-2026-001",
                    "amount": 2000,
                    "title": "Order #001",
                    "description": "Online order",
                    "terminal_label": "COUNTER01",
                    "bill_number": "BILL-2026-001",
                    "return_url": "https://merchant.example/payment/complete",
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "metadata": {
                      "membership_id": "MEM-123",
                      "billing_period": "2026-10"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created payment, or the original payment returned for a matching retry. Hosted payments include payment_url for checkout and qr_url for the PayMV QR link. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                },
                "examples": {
                  "required": {
                    "summary": "Required fields only",
                    "value": {
                      "data": {
                        "id": "a1234567890b4cde8f0123456789abcd",
                        "reference": "ORDER-2026-001",
                        "amount": 2000,
                        "fee": null,
                        "net_amount": null,
                        "title": null,
                        "description": null,
                        "terminal_label": null,
                        "bill_number": null,
                        "status": "pending",
                        "payment_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                        "payment_reference": null,
                        "qr_url": "https://api.fahipay.mv/merchants/public/qr/YOUR_PAYMENT_ID",
                        "return_url": null,
                        "created_at": "2026-01-01T12:00:00+05:00",
                        "paid_at": null,
                        "expires_at": "2026-01-02T12:00:00+05:00",
                        "metadata": {}
                      }
                    }
                  },
                  "optional": {
                    "summary": "With optional fields",
                    "value": {
                      "data": {
                        "id": "a1234567890b4cde8f0123456789abcd",
                        "reference": "ORDER-2026-001",
                        "amount": 2000,
                        "fee": null,
                        "net_amount": null,
                        "title": "Order #001",
                        "description": "Online order",
                        "terminal_label": "COUNTER01",
                        "bill_number": "BILL-2026-001",
                        "status": "pending",
                        "payment_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                        "payment_reference": null,
                        "qr_url": "https://api.fahipay.mv/merchants/public/qr/YOUR_PAYMENT_ID",
                        "return_url": "https://merchant.example/payment/complete",
                        "created_at": "2026-01-01T12:00:00+05:00",
                        "paid_at": null,
                        "expires_at": "2026-12-31T23:59:59+05:00",
                        "metadata": {
                          "membership_id": "MEM-123",
                          "billing_period": "2026-10"
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": " An invalid Idempotency-Key returns invalid_idempotency_key."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict",
            "description": " Idempotency conflicts return idempotency_conflict or idempotency_in_progress."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "503": {
            "description": " An idempotency lookup race can return idempotency_unavailable. Retry with the same key and body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-required-scopes": [
          "payments:create"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payments:create"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ]
    },
    "/payments/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PaymentId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Get Payment",
        "operationId": "getPayment",
        "responses": {
          "200": {
            "description": "Payment details. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                },
                "example": {
                  "data": {
                    "id": "a1234567890b4cde8f0123456789abcd",
                    "reference": "ORDER-2026-001",
                    "amount": 2000,
                    "fee": null,
                    "net_amount": null,
                    "title": "Order #001",
                    "description": "Online order",
                    "terminal_label": "COUNTER01",
                    "bill_number": "BILL-2026-001",
                    "status": "pending",
                    "payment_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                    "payment_reference": null,
                    "qr_url": "https://api.fahipay.mv/merchants/public/qr/YOUR_PAYMENT_ID",
                    "return_url": "https://merchant.example/payment/complete",
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "paid_at": null,
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "metadata": {}
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "description": "Retrieve a payment and its current status. Hosted payments include checkout URLs; authorization payments omit them.\n\nA pending payment past its deadline is saved as `expired`, unless a payment attempt is still processing. Each expiry emits `payment.expired` once when webhooks are enabled.",
        "x-required-scopes": [
          "payments:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payments:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/payments/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PaymentId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Cancel Payment",
        "operationId": "cancelPayment",
        "description": "Cancel a pending payment before the customer pays. This does not refund a completed payment.\n\n- Success returns `cancelled` and emits `payment.cancelled` when webhooks are enabled. Its QR image is no longer available.\n- Repeating a successful cancellation returns HTTP 200 without another event.\n- To collect payment again, create a new payment with a new reference.",
        "responses": {
          "200": {
            "description": "The cancelled payment, including when it was already cancelled. Any returned checkout and QR URLs no longer accept payment.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                },
                "example": {
                  "data": {
                    "id": "a1234567890b4cde8f0123456789abcd",
                    "reference": "ORDER-2026-001",
                    "amount": 2000,
                    "fee": null,
                    "net_amount": null,
                    "title": "Order #001",
                    "description": "Online order",
                    "terminal_label": "COUNTER01",
                    "bill_number": "BILL-2026-001",
                    "status": "cancelled",
                    "payment_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                    "payment_reference": null,
                    "qr_url": "https://api.fahipay.mv/merchants/public/qr/YOUR_PAYMENT_ID",
                    "return_url": "https://merchant.example/payment/complete",
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "paid_at": null,
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "metadata": {}
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No payment exists with this ID in your merchant account and current mode.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Payment not found."
                  }
                }
              }
            }
          },
          "405": {
            "description": "Use POST for this endpoint.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              },
              "Allow": {
                "description": "The allowed HTTP method.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "POST"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method not allowed."
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/PaymentCancellationConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "payments:cancel"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payments:cancel"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/authorizations": {
      "get": {
        "tags": [
          "Authorizations"
        ],
        "summary": "List Authorizations",
        "operationId": "listAuthorizations",
        "description": "List your authorizations in the current live or sandbox mode, newest first. Filter by status and use `pagination.next_cursor` as `after` for the next page.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          },
          {
            "$ref": "#/components/parameters/AuthorizationStatus"
          }
        ],
        "responses": {
          "200": {
            "description": "Authorizations and pagination. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "b1234567890b4cde8f0123456789abcd",
                      "reference": "GYM-MEMBER-1042-2026",
                      "title": "Monthly gym membership",
                      "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
                      "type": "recurring",
                      "currency": "MVR",
                      "limits": {
                        "per_payment": 50000,
                        "period": {
                          "interval": "month",
                          "amount": 50000
                        },
                        "total": 600000,
                        "max_payments": 12
                      },
                      "usage": {
                        "payment_count": 0,
                        "total_paid": 0
                      },
                      "status": "pending",
                      "approval_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                      "approval_expires_at": "2026-01-02T12:00:00+05:00",
                      "expires_at": "2026-12-31T23:59:59+05:00",
                      "return_url": "https://merchant.example/authorizations/complete",
                      "approved_at": null,
                      "rejected_at": null,
                      "revoked_at": null,
                      "last_used_at": null,
                      "created_at": "2026-01-01T12:00:00+05:00",
                      "updated_at": "2026-01-01T12:00:00+05:00"
                    }
                  ],
                  "pagination": {
                    "limit": 20,
                    "has_more": false,
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "authorizations:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "authorizations:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      },
      "post": {
        "tags": [
          "Authorizations"
        ],
        "summary": "Create Authorization",
        "operationId": "createAuthorization",
        "description": "Ask a customer to approve future payments, such as gym membership fees, subscription renewals, or usage-based charges.\n\n- Send the customer to `approval_url`. The default approval window is 24 hours.\n- Once the authorization is `active`, your application submits each charge when it is due. FahiPay does not schedule charges automatically.\n- A gym charging MVR 500 monthly could set a per-payment limit of `50000` laari and a monthly period limit of `50000`.",
        "requestBody": {
          "required": true,
          "description": "A JSON object. Express monetary amounts and limits as integer laari (100 laari = MVR 1.00).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAuthorizationRequest"
              },
              "example": {
                "reference": "GYM-MEMBER-1042-2026",
                "title": "Monthly gym membership",
                "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
                "type": "recurring",
                "currency": "MVR",
                "limits": {
                  "per_payment": 50000,
                  "period": {
                    "interval": "month",
                    "amount": 50000
                  },
                  "total": 600000,
                  "max_payments": 12
                },
                "expires_at": "2026-12-31T23:59:59+05:00",
                "return_url": "https://merchant.example/authorizations/complete"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created authorization, or the original authorization returned for a repeated request. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationResponse"
                },
                "example": {
                  "data": {
                    "id": "b1234567890b4cde8f0123456789abcd",
                    "reference": "GYM-MEMBER-1042-2026",
                    "title": "Monthly gym membership",
                    "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
                    "type": "recurring",
                    "currency": "MVR",
                    "limits": {
                      "per_payment": 50000,
                      "period": {
                        "interval": "month",
                        "amount": 50000
                      },
                      "total": 600000,
                      "max_payments": 12
                    },
                    "usage": {
                      "payment_count": 0,
                      "total_paid": 0
                    },
                    "status": "pending",
                    "approval_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                    "approval_expires_at": "2026-01-02T12:00:00+05:00",
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "return_url": "https://merchant.example/authorizations/complete",
                    "approved_at": null,
                    "rejected_at": null,
                    "revoked_at": null,
                    "last_used_at": null,
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "updated_at": "2026-01-01T12:00:00+05:00"
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "authorizations:create"
        ],
        "security": [
          {
            "MerchantBearer": [
              "authorizations:create"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      },
      "parameters": [
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ]
    },
    "/authorizations/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AuthorizationId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Authorizations"
        ],
        "summary": "Get Authorization",
        "operationId": "getAuthorization",
        "responses": {
          "200": {
            "description": "Authorization details and lifetime usage. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationResponse"
                },
                "example": {
                  "data": {
                    "id": "b1234567890b4cde8f0123456789abcd",
                    "reference": "GYM-MEMBER-1042-2026",
                    "title": "Monthly gym membership",
                    "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
                    "type": "recurring",
                    "currency": "MVR",
                    "limits": {
                      "per_payment": 50000,
                      "period": {
                        "interval": "month",
                        "amount": 50000
                      },
                      "total": 600000,
                      "max_payments": 12
                    },
                    "usage": {
                      "payment_count": 0,
                      "total_paid": 0
                    },
                    "status": "pending",
                    "approval_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                    "approval_expires_at": "2026-01-02T12:00:00+05:00",
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "return_url": "https://merchant.example/authorizations/complete",
                    "approved_at": null,
                    "rejected_at": null,
                    "revoked_at": null,
                    "last_used_at": null,
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "updated_at": "2026-01-01T12:00:00+05:00"
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "description": "Retrieve an authorization, including its status, approved limits, total amount paid, and number of successful payments.",
        "x-required-scopes": [
          "authorizations:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "authorizations:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/authorizations/{id}/charge": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AuthorizationId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "post": {
        "tags": [
          "Authorizations"
        ],
        "summary": "Charge Authorization",
        "operationId": "chargeAuthorization",
        "description": "Collect a payment using an active authorization approved by the customer. Your application submits this request whenever a payment is due.\n\n- Each charge must fit within the authorization limits. Period limits reset at midnight in +05:00; weeks begin on Sunday.\n- Insufficient balance returns HTTP 402 (`insufficient_balance`); a wallet limit failure returns HTTP 422 (`wallet_limit_exceeded`). Neither creates a payment.\n- Reuse the original reference and amount when retrying. A matching successful payment is returned without charging again. See the `Idempotency-Key` rules below if you supply that header.",
        "requestBody": {
          "required": true,
          "description": "A JSON object. Express monetary amounts and limits as integer laari (100 laari = MVR 1.00).",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChargeAuthorizationRequest"
              },
              "example": {
                "reference": "GYM-MEMBER-1042-2026-01",
                "amount": 50000,
                "title": "Monthly gym membership",
                "description": "Gym membership for member 1042, January 2026",
                "bill_number": "GYM/2026/01-1042"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The successful payment, or the original payment returned for a matching retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                },
                "example": {
                  "data": {
                    "id": "c1234567890b4cde8f0123456789abcd",
                    "reference": "GYM-MEMBER-1042-2026-01",
                    "amount": 50000,
                    "fee": 1000,
                    "net_amount": 49000,
                    "title": "Monthly gym membership",
                    "description": "Gym membership for member 1042, January 2026",
                    "bill_number": "GYM/2026/01-1042",
                    "status": "paid",
                    "payment_reference": "FP202601010001",
                    "created_at": "2026-01-01T12:05:00+05:00",
                    "paid_at": "2026-01-01T12:05:00+05:00",
                    "metadata": {}
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest",
            "description": " An invalid Idempotency-Key returns invalid_idempotency_key."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The authorization was not found for the current merchant and mode.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "authorization_not_found",
                    "message": "Authorization not found."
                  }
                }
              }
            }
          },
          "409": {
            "description": "Possible codes: authorization_unapproved, authorization_inactive, authorization_expired, authorization_unavailable, currency_mismatch, per_payment_limit_exceeded, period_limit_exceeded, total_limit_exceeded, payment_count_limit_exceeded, payment_in_progress, reference_conflict. These remain HTTP 409. Idempotency conflicts return idempotency_conflict or idempotency_in_progress.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "authorization_expired",
                    "message": "Authorization has expired."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "402": {
            "description": "Insufficient customer balance. No new charge was taken by this request.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "insufficient_balance",
                    "message": "The customer has insufficient balance to complete this payment."
                  }
                }
              }
            }
          },
          "422": {
            "description": "The customer wallet-limit check rejected this charge. No new charge was taken by this request.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "wallet_limit_exceeded",
                    "message": "This payment exceeds the customer's wallet limits."
                  }
                }
              }
            }
          },
          "503": {
            "description": " An idempotency lookup race can return idempotency_unavailable. Retry with the same key and body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-required-scopes": [
          "authorizations:charge"
        ],
        "security": [
          {
            "MerchantBearer": [
              "authorizations:charge"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/authorizations/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AuthorizationId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "post": {
        "tags": [
          "Authorizations"
        ],
        "summary": "Cancel Authorization",
        "operationId": "cancelAuthorization",
        "description": "Cancel a pending authorization or revoke an active one to stop future charges.\n\n- A charge in progress returns HTTP 409; retry after it finishes.\n- Already cancelled or revoked authorizations return HTTP 200.\n- Previous payments are not reversed.",
        "responses": {
          "200": {
            "description": "Cancelled or revoked authorization. FahiPay URLs in response examples contain descriptive placeholders such as `YOUR_PUBLIC_LINK_ID` and `YOUR_PAYMENT_ID`; these are not working links. Use the exact payment, QR, and approval URLs returned by the API; do not construct these URLs from resource IDs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthorizationResponse"
                },
                "example": {
                  "data": {
                    "id": "b1234567890b4cde8f0123456789abcd",
                    "reference": "GYM-MEMBER-1042-2026",
                    "title": "Monthly gym membership",
                    "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
                    "type": "recurring",
                    "currency": "MVR",
                    "limits": {
                      "per_payment": 50000,
                      "period": {
                        "interval": "month",
                        "amount": 50000
                      },
                      "total": 600000,
                      "max_payments": 12
                    },
                    "usage": {
                      "payment_count": 0,
                      "total_paid": 0
                    },
                    "status": "cancelled",
                    "approval_url": "https://fahipay.mv/p/YOUR_PUBLIC_LINK_ID",
                    "approval_expires_at": "2026-01-02T12:00:00+05:00",
                    "expires_at": "2026-12-31T23:59:59+05:00",
                    "return_url": "https://merchant.example/authorizations/complete",
                    "approved_at": null,
                    "rejected_at": null,
                    "revoked_at": "2026-01-01T12:05:00+05:00",
                    "last_used_at": null,
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "updated_at": "2026-01-01T12:00:00+05:00"
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Retrieve the authorization. Retry after processing only if its state permits cancellation.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "authorization_not_cancellable",
                    "message": "Authorization cannot be cancelled in its current state or while being processed."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "authorizations:cancel"
        ],
        "security": [
          {
            "MerchantBearer": [
              "authorizations:cancel"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/authorizations/{id}/payments": {
      "parameters": [
        {
          "$ref": "#/components/parameters/AuthorizationId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Authorizations"
        ],
        "summary": "List Authorization Payments",
        "operationId": "listAuthorizationPayments",
        "description": "List payments made through this authorization, newest first.\n\n- Filter by `paid` or `refunded`, or omit the status to include both.\n- History remains available after the authorization is cancelled, revoked, or expired.\n- Keep filters unchanged when loading the next page. An authorization with no payments returns an empty `data` array.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          },
          {
            "$ref": "#/components/parameters/AuthorizationPaymentStatus"
          },
          {
            "$ref": "#/components/parameters/PaymentBillNumber"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedFrom"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Payments belonging to the requested authorization, with pagination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "c1234567890b4cde8f0123456789abcd",
                      "reference": "GYM-MEMBER-1042-2026-01",
                      "amount": 50000,
                      "fee": 1000,
                      "net_amount": 49000,
                      "title": "Monthly gym membership",
                      "description": "Gym membership for member 1042, January 2026",
                      "bill_number": "GYM/2026/01-1042",
                      "status": "paid",
                      "payment_reference": "FP202601010001",
                      "created_at": "2026-01-01T12:05:00+05:00",
                      "paid_at": "2026-01-01T12:05:00+05:00",
                      "metadata": {}
                    }
                  ],
                  "pagination": {
                    "limit": 20,
                    "has_more": false,
                    "next_cursor": null
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          }
        },
        "x-required-scopes": [
          "payments:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payments:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/public/qr/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PaymentId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Get PayMV QR",
        "operationId": "getPaymentQr",
        "description": "Display the PayMV QR image for a pending, unexpired hosted payment. This public endpoint returns PNG image bytes and does not require merchant credentials.\n\nUse the exact `qr_url` returned by payment creation. Unavailable payments and authorization payments return HTTP 404.",
        "security": [],
        "responses": {
          "200": {
            "description": "The PayMV QR image for the payment.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              },
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "The payment was not found or is no longer available for QR payment. Authorization-linked payments do not have a QR image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "not_found",
                    "message": "Payment QR is unavailable."
                  }
                }
              }
            },
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "405": {
            "description": "This endpoint accepts GET requests only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "Method not allowed."
                  }
                }
              }
            },
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string"
                },
                "example": "GET"
              },
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/payment-requests": {
      "parameters": [
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Payment Requests"
        ],
        "summary": "List Payment Requests",
        "operationId": "listPaymentRequests",
        "description": "List payment requests created by your merchant in the current live or sandbox mode, newest first. Combine the filters below and use `pagination.next_cursor` as `after` for the next page.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/After"
          },
          {
            "$ref": "#/components/parameters/PaymentRequestStatus"
          },
          {
            "$ref": "#/components/parameters/PaymentTerminalLabel"
          },
          {
            "$ref": "#/components/parameters/PaymentBillNumber"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedFrom"
          },
          {
            "$ref": "#/components/parameters/PaymentCreatedTo"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment requests and pagination.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestListResponse"
                },
                "example": {
                  "data": [
                    {
                      "id": "r1234567890b4cde8f0123456789abcd",
                      "reference": "SCHOOL-ACTIVITY-2026-001",
                      "amount": 75000,
                      "fee": null,
                      "net_amount": null,
                      "title": "School activity fee",
                      "description": "Activity fee for January 2026",
                      "terminal_label": "COUNTER01",
                      "bill_number": "ACT-2026-001",
                      "status": "pending",
                      "payment_id": null,
                      "payment_reference": null,
                      "customer": {
                        "type": "vpa",
                        "value": "test@fp"
                      },
                      "return_url": "https://merchant.example/payment-requests/complete",
                      "created_at": "2026-01-01T12:00:00+05:00",
                      "paid_at": null,
                      "expires_at": "2026-01-02T12:00:00+05:00",
                      "cancelled_at": null,
                      "rejected_at": null,
                      "metadata": {}
                    }
                  ],
                  "pagination": {
                    "limit": 20,
                    "has_more": false,
                    "next_cursor": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": [
          "payment_requests:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payment_requests:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      },
      "post": {
        "tags": [
          "Payment Requests"
        ],
        "summary": "Create Payment Request",
        "operationId": "createPaymentRequest",
        "description": "Request a payment from a customer using their mobile number, email address, or FahiPay VPA. Customer approval creates a paid payment.\n\n- Requests expire after 24 hours unless you set `expires_at`.\n- In sandbox mode, use VPA `test@fp` (or `test@fahipay`) or email `test@fahipay.mv`. Other recipients, including mobile numbers, return `sandbox_recipient_not_allowed`.",
        "requestBody": {
          "required": true,
          "description": "Payment request details. Amount is integer laari and currency is always MVR.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequestRequest"
              },
              "example": {
                "reference": "SCHOOL-ACTIVITY-2026-001",
                "amount": 75000,
                "customer": {
                  "type": "vpa",
                  "value": "test@fp"
                },
                "title": "School activity fee",
                "description": "Activity fee for January 2026",
                "terminal_label": "COUNTER01",
                "bill_number": "ACT-2026-001",
                "return_url": "https://merchant.example/payment-requests/complete"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created payment request, or the original request returned for a matching retry.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                },
                "example": {
                  "data": {
                    "id": "r1234567890b4cde8f0123456789abcd",
                    "reference": "SCHOOL-ACTIVITY-2026-001",
                    "amount": 75000,
                    "fee": null,
                    "net_amount": null,
                    "title": "School activity fee",
                    "description": "Activity fee for January 2026",
                    "terminal_label": "COUNTER01",
                    "bill_number": "ACT-2026-001",
                    "status": "pending",
                    "payment_id": null,
                    "payment_reference": null,
                    "customer": {
                      "type": "vpa",
                      "value": "test@fp"
                    },
                    "return_url": "https://merchant.example/payment-requests/complete",
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "paid_at": null,
                    "expires_at": "2026-01-02T12:00:00+05:00",
                    "cancelled_at": null,
                    "rejected_at": null,
                    "metadata": {}
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": [
          "payment_requests:create"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payment_requests:create"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/payment-requests/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PaymentRequestId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "get": {
        "tags": [
          "Payment Requests"
        ],
        "summary": "Get Payment Request",
        "operationId": "getPaymentRequest",
        "description": "Retrieve a payment request, including its customer identifier, current status, and payment reference after payment.",
        "responses": {
          "200": {
            "description": "Payment request details.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                },
                "example": {
                  "data": {
                    "id": "r1234567890b4cde8f0123456789abcd",
                    "reference": "SCHOOL-ACTIVITY-2026-001",
                    "amount": 75000,
                    "fee": null,
                    "net_amount": null,
                    "title": "School activity fee",
                    "description": "Activity fee for January 2026",
                    "terminal_label": "COUNTER01",
                    "bill_number": "ACT-2026-001",
                    "status": "pending",
                    "payment_id": null,
                    "payment_reference": null,
                    "customer": {
                      "type": "vpa",
                      "value": "test@fp"
                    },
                    "return_url": "https://merchant.example/payment-requests/complete",
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "paid_at": null,
                    "expires_at": "2026-01-02T12:00:00+05:00",
                    "cancelled_at": null,
                    "rejected_at": null,
                    "metadata": {}
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "description": "No payment request was found for this merchant and mode.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "payment_request_not_found",
                    "message": "Payment request not found."
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": [
          "payment_requests:read"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payment_requests:read"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    },
    "/payment-requests/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/PaymentRequestId"
        },
        {
          "$ref": "#/components/parameters/FahiPayVersion"
        }
      ],
      "post": {
        "tags": [
          "Payment Requests"
        ],
        "summary": "Cancel Payment Request",
        "operationId": "cancelPaymentRequest",
        "description": "Cancel a pending payment request before it is paid. Already-cancelled requests return the cancelled request.\n\nPaid, rejected, expired, refunded, or currently processing requests cannot be cancelled.",
        "responses": {
          "200": {
            "description": "Cancelled payment request.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                },
                "example": {
                  "data": {
                    "id": "r1234567890b4cde8f0123456789abcd",
                    "reference": "SCHOOL-ACTIVITY-2026-001",
                    "amount": 75000,
                    "fee": null,
                    "net_amount": null,
                    "title": "School activity fee",
                    "description": "Activity fee for January 2026",
                    "terminal_label": "COUNTER01",
                    "bill_number": "ACT-2026-001",
                    "status": "cancelled",
                    "payment_id": null,
                    "payment_reference": null,
                    "customer": {
                      "type": "vpa",
                      "value": "test@fp"
                    },
                    "return_url": "https://merchant.example/payment-requests/complete",
                    "created_at": "2026-01-01T12:00:00+05:00",
                    "paid_at": null,
                    "expires_at": "2026-01-02T12:00:00+05:00",
                    "cancelled_at": "2026-01-01T12:30:00+05:00",
                    "rejected_at": null,
                    "metadata": {}
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "description": "No payment request was found for this merchant and mode.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "payment_request_not_found",
                    "message": "Payment request not found."
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "Retrieve the request; it may have expired, changed state or started processing.",
            "headers": {
              "FahiPay-Version": {
                "$ref": "#/components/headers/FahiPayVersion"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "payment_request_not_cancellable",
                    "message": "Only unexpired, pending payment requests without a processing lock can be cancelled."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-required-scopes": [
          "payment_requests:cancel"
        ],
        "security": [
          {
            "MerchantBearer": [
              "payment_requests:cancel"
            ]
          },
          {
            "MerchantApiKey": []
          },
          {
            "MerchantBasic": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "MerchantBearer": {
        "type": "oauth2",
        "description": "Send an OAuth access token as Authorization: Bearer YOUR_ACCESS_TOKEN over HTTPS. Each authenticated endpoint requires its documented scope. Tokens with no recognized scopes have no authenticated endpoint access. Bearer takes precedence over X-API-Key and HTTP Basic; failed Bearer authentication does not fall back. The token identifies the merchant through its associated merchant ID, and requests use that merchant account's current mode. Keep access tokens on your server. Obtain access tokens using the Client Credentials flow at https://api.fahipay.mv/merchants/oauth/token.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://api.fahipay.mv/merchants/oauth/token",
            "scopes": {
              "payments:create": "Create hosted payments.",
              "payments:read": "List and retrieve payments, including authorization payment history.",
              "payments:cancel": "Cancel pending payments.",
              "authorizations:create": "Request customer approval for authorizations.",
              "authorizations:read": "List and retrieve authorizations.",
              "authorizations:charge": "Charge approved authorizations.",
              "authorizations:cancel": "Cancel pending authorizations or revoke active authorizations.",
              "payment_requests:create": "Create payment requests.",
              "payment_requests:read": "List and retrieve payment requests.",
              "payment_requests:cancel": "Cancel pending payment requests."
            }
          }
        }
      },
      "MerchantBasic": {
        "type": "http",
        "scheme": "basic",
        "description": "Use your merchant_id as the Basic username and case-sensitive secret_key as the password over HTTPS. Basic authentication grants all Merchant API scopes. Bearer and X-API-Key take precedence if supplied. Send one method per request. An active merchant account is required. Get your merchant credentials from https://fahipay.mv/merchants/portal/."
      },
      "MerchantApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Send your case-sensitive secret_key as X-API-Key over HTTPS, without a merchant ID or prefix. API keys grant all Merchant API scopes. Bearer takes precedence over X-API-Key; X-API-Key takes precedence over Basic. The selected method must be valid; authentication does not fall back on failure. An active merchant account is required. Get your merchant credentials from https://fahipay.mv/merchants/portal/."
      }
    },
    "parameters": {
      "PaymentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ResourceId"
        },
        "description": "The payment ID returned when you create or list payments."
      },
      "AuthorizationId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/ResourceId"
        },
        "description": "The authorization ID returned when you create or list authorizations."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "description": "Maximum number of results per page, from 1 to 100. Defaults to 20. Values outside this range are adjusted to the nearest boundary. Supply a single integer value; arrays and non-integer values return HTTP 400."
      },
      "After": {
        "name": "after",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Pass pagination.next_cursor from the previous response to retrieve the next page. An unknown cursor starts at the first page. Supply a single string; arrays return HTTP 400."
      },
      "PaymentStatus": {
        "name": "status",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "paid",
            "rejected",
            "expired",
            "cancelled",
            "refunded"
          ]
        },
        "description": "Return payments with this status. Pending includes payments with no expiry, a future expiry, or a reserved payment attempt still processing. Expired includes payments stored as expired and overdue pending payments without a processing reservation. Only returned rows are updated; filtering by pending does not update excluded overdue payments. Unsupported string values are ignored. Arrays return HTTP 400."
      },
      "AuthorizationStatus": {
        "name": "status",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "active",
            "rejected",
            "revoked",
            "expired",
            "cancelled",
            "suspended",
            "consumed"
          ]
        },
        "description": "Return authorizations with this status. Unsupported string values are ignored. Arrays return HTTP 400."
      },
      "FahiPayVersion": {
        "name": "FahiPay-Version",
        "in": "header",
        "required": false,
        "description": "Optional Merchant API version. Send 2027-07-01 to select this release. Omit the header to use the latest version. Unsupported or empty values return HTTP 400 with error code unsupported_api_version.",
        "schema": {
          "type": "string",
          "enum": [
            "2027-07-01"
          ]
        }
      },
      "PaymentTerminalLabel": {
        "name": "terminal_label",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 10
        },
        "description": "Filter payments by an exact, case-sensitive terminal label. Maximum 10 characters after trimming. Omit or leave blank to include all terminal labels. Unlabelled payments do not match a nonblank label.",
        "example": "COUNTER01"
      },
      "PaymentCreatedFrom": {
        "name": "created_from",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?)?$"
        },
        "description": "Include payments created at or after this time (inclusive). Accepts a date or timestamp, for example 2026-01-01 or 2026-01-01T00:00:00+05:00. Dates mean midnight, and values without an offset use +05:00. Use URL encoding for timestamps containing +. Blank, invalid, or out-of-range values return HTTP 400. When both bounds are supplied, created_from must be earlier than created_to. Omit either bound for an open-ended range. Applies to created_at, not paid_at.",
        "example": "2026-01-10T08:00:00+05:00"
      },
      "PaymentCreatedTo": {
        "name": "created_to",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?)?$"
        },
        "description": "Include payments created before this time (exclusive). Accepts a date or timestamp, for example 2026-01-01 or 2026-01-01T00:00:00+05:00. Dates mean midnight, and values without an offset use +05:00. Use URL encoding for timestamps containing +. Blank, invalid, or out-of-range values return HTTP 400. When both bounds are supplied, created_from must be earlier than created_to. Omit either bound for an open-ended range. Applies to created_at, not paid_at.",
        "example": "2026-01-10T16:00:00+05:00"
      },
      "AuthorizationPaymentStatus": {
        "name": "status",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "paid",
            "refunded"
          ]
        },
        "description": "Filter authorization payments by status: paid for completed charges, or refunded for refunded charges. Omit to include both. This is the payment status, not the authorization status. Supply a single string; arrays return HTTP 400."
      },
      "PaymentBillNumber": {
        "name": "bill_number",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 25,
          "pattern": "^[A-Za-z0-9 _./-]*$"
        },
        "description": "Filter payments by an exact, case-sensitive bill or invoice number. Maximum 25 characters after trimming. Leading zeroes are significant. Omit or leave blank to include all bill numbers. Payments without a bill number do not match a nonblank value. May be combined with status, terminal_label, creation-time bounds, and pagination. Allowed characters: letters A-Z and a-z, digits 0-9, spaces, hyphens (-), underscores (_), dots (.), and slashes (/). Unsupported characters or values longer than 25 characters return HTTP 400.",
        "example": "BILL-2026-001"
      },
      "PaymentRequestId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9-]{20,40}$"
        },
        "description": "The payment request ID returned when you create or list payment requests."
      },
      "PaymentRequestStatus": {
        "name": "status",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "pending",
            "paid",
            "rejected",
            "expired",
            "cancelled",
            "refunded"
          ]
        },
        "description": "Filter payment requests by status."
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional on POST /payments and POST /authorizations/{id}/charge only. Use a new key for each new operation and reuse it for retries. Accepts 1–255 printable ASCII characters without spaces; keys are case-sensitive and are not trimmed. Keys are scoped by merchant, live/sandbox mode, and operation. Request matching includes the authorization ID (for charges), effective API version, and parsed JSON body. JSON object key order is ignored; values, types, and field presence must remain unchanged. Completed responses are replayed with their original HTTP status and JSON body for 24 hours after completion, including saved validation and payment errors. A replay is a snapshot; retrieve the payment to check its current status. Different request data returns 409 idempotency_conflict. An unfinished request returns 409 idempotency_in_progress and may require support to reconcile an uncertain outcome. Do not switch keys to bypass an unfinished request. Completed keys can be reused after expiry; processing keys do not expire automatically. Existing reference uniqueness rules always apply. Without this header, existing reference-based retry behavior remains unchanged.\n\nAfter a confirmed failure such as insufficient_balance (402) or wallet_limit_exceeded (422), resolve the cause before trying again. If you supplied Idempotency-Key, reusing that key replays the saved failure for 24 hours. For a new attempt after a confirmed failure, use a new key while retaining the original payment reference, amount, and authorization ID. Never change keys for an unknown outcome or idempotency_in_progress; reconcile those first.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[!-~]+$"
        },
        "example": "ORDER-2026-001"
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input (invalid_request), malformed JSON (invalid_json), an oversized request (request_too_large), or an unresolved recipient (customer_not_found). Validation errors may include error.field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "invalidRequest": {
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "amount must be a positive integer in laari (MVR)."
                  }
                }
              },
              "unsupportedVersion": {
                "value": {
                  "error": {
                    "code": "unsupported_api_version",
                    "message": "Unsupported FahiPay-Version. Use 2027-07-01 or omit the header to use the latest version."
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "amount must be a positive integer in laari (MVR).",
                "field": "amount"
              }
            }
          }
        },
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        }
      },
      "Unauthorized": {
        "description": "Authentication failed. Check the selected credential and confirm that the merchant account is active. Bearer tokens must be valid and unexpired. API key and Basic authentication use the merchant secret_key; Basic also requires merchant_id. Authentication precedence is Bearer, X-API-Key, then Basic; a failed selected method does not fall back.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "Invalid merchant credentials or inactive merchant."
              }
            }
          }
        },
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            },
            "example": "Basic realm=\"Merchant API\"",
            "description": "Bearer challenge when Bearer authentication was selected; otherwise Basic."
          },
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        }
      },
      "NotFound": {
        "description": "The requested resource or endpoint is unavailable. Check merchant ownership and live/sandbox mode. Access restrictions outside the API router may instead return HTML.",
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "Resource or endpoint not found."
              }
            }
          },
          "text/html": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "Conflict": {
        "description": "A business-state, authorization-limit, reference or processing conflict. Inspect error.code; different 409 codes require different recovery steps. See the error-code guide.",
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "reference_conflict",
                "message": "Payment reference already exists with a different amount."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Wait before trying again.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests."
              }
            }
          }
        },
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        }
      },
      "InternalError": {
        "description": "The payment outcome may be unknown. Look up the payment or retry with its original reference and amount. If the outcome remains unclear, contact support before starting a new payment.",
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "An unexpected error occurred."
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "This endpoint does not support the requested HTTP method. Check the Allow response header for supported methods.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "method_not_allowed",
                "message": "Method not allowed."
              }
            }
          }
        },
        "headers": {
          "Allow": {
            "schema": {
              "type": "string"
            },
            "example": "GET, POST"
          },
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        }
      },
      "PaymentCancellationConflict": {
        "description": "Only pending payments can be cancelled. Repeating an already successful cancellation returns HTTP 200. Cancellation does not reverse paid payments.",
        "headers": {
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "payment_not_cancellable",
                "message": "Only pending payments can be cancelled."
              }
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "Authentication succeeded, but the credential lacks the scope required by this endpoint. No endpoint operation is performed. The message identifies the required scope; use a token granted that permission before retrying.",
        "headers": {
          "WWW-Authenticate": {
            "description": "Included for Bearer requests. Identifies insufficient_scope and the required endpoint scope.",
            "schema": {
              "type": "string"
            },
            "example": "Bearer realm=\"Merchant API\", error=\"insufficient_scope\", scope=\"payments:read\""
          },
          "FahiPay-Version": {
            "$ref": "#/components/headers/FahiPayVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "insufficient_scope",
                "message": "Required scope: payments:read."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "CreatePaymentRequest": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "Create Payment",
        "type": "object",
        "required": [
          "reference",
          "amount"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "pattern": "\\S",
            "description": "Your payment reference. Must be unique across all payments for your merchant, including hosted payments and authorization charges. Reuse the same reference and amount only when retrying the same payment. Leading and trailing whitespace is removed. A reference already used by an authorization payment returns HTTP 409. References reserved by a payment request also return HTTP 409. Maximum 100 characters after trimming.",
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "A positive integer amount in laari. Payments are always in MVR: 100 laari = MVR 1.00. For example, use 2000 for MVR 20.00. Send a JSON integer; omit currency.",
            "maximum": 9999999999
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment title. Leading and trailing whitespace is removed. Omit or use null for an empty title.",
            "default": ""
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment description. Leading and trailing whitespace is removed. Omit or use null for an empty description.",
            "default": ""
          },
          "terminal_label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10,
            "description": "Optional label identifying the checkout counter or terminal where this payment originated. Maximum 10 characters after trimming leading and trailing whitespace. Omit, use null, or send a blank string for no label. Labels can be reused across payments. A matching creation retry retains the original label.",
            "examples": [
              "COUNTER01",
              null
            ]
          },
          "bill_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional bill or invoice number associated with this hosted payment. Maximum 25 characters after trimming. Send a string to preserve leading zeroes; longer values return HTTP 400. Leading and trailing whitespace is removed. Omit, use null, or send a blank string for no bill number. Bill numbers may be reused; reference must still be unique for each new payment. A matching creation retry retains the original bill number. Allowed characters: letters A-Z and a-z, digits 0-9, spaces, hyphens (-), underscores (_), dots (.), and slashes (/).",
            "examples": [
              "BILL-2026-001",
              null
            ],
            "maxLength": 25,
            "pattern": "^[A-Za-z0-9 _./-]*$"
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional absolute HTTP or HTTPS URL for the customer to return to your website after checkout. Embedded usernames or passwords are not allowed. Maximum 2048 bytes after trimming leading and trailing whitespace. Omit, use null, or send a blank string for no return URL. Invalid URLs return HTTP 400 with error.field return_url. A return URL visit is not proof of payment; verify payment status using the API or a verified webhook.",
            "maxLength": 2048
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?)?$",
            "description": "Optional payment expiry. Omit this field or use null to expire the payment 24 hours after creation. To choose a different expiry, provide a future RFC 3339 timestamp, such as 2026-12-31T23:59:59+05:00. YYYY-MM-DD and YYYY-MM-DD HH:mm:ss are also accepted; a missing timezone offset means +05:00. Expiry is returned in +05:00 at second precision. A matching creation retry retains the original expiry, even if that time has passed.",
            "examples": [
              "2026-12-31T23:59:59+05:00",
              null
            ]
          },
          "metadata": {
            "$ref": "#/components/schemas/PaymentMetadata"
          }
        },
        "description": "Details for a hosted payment link. Specify amount as integer laari; the currency is always MVR and must not be supplied. Use the same reference and amount when retrying.",
        "examples": [
          {
            "reference": "ORDER-2026-001",
            "amount": 2000,
            "title": "Order #001",
            "description": "Online order",
            "terminal_label": "COUNTER01",
            "bill_number": "BILL-2026-001",
            "return_url": "https://merchant.example/payment/complete",
            "expires_at": "2026-12-31T23:59:59+05:00",
            "metadata": {
              "membership_id": "MEM-123",
              "billing_period": "2026-10"
            }
          }
        ],
        "not": {
          "required": [
            "currency"
          ]
        }
      },
      "ChargeAuthorizationRequest": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "Charge Authorization",
        "type": "object",
        "required": [
          "reference",
          "amount"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "pattern": "\\S",
            "description": "Your payment reference. Must be unique across all payments for your merchant, including hosted payments and authorization charges. Reuse the same reference and amount only when retrying the same payment. Leading and trailing whitespace is removed. Maximum 100 characters after trimming.",
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "A positive integer amount in laari. Payments are always in MVR: 100 laari = MVR 1.00. For example, use 2000 for MVR 20.00. Send a JSON integer; omit currency.",
            "maximum": 9999999999
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment title. Leading and trailing whitespace is removed. Omit or use null for an empty title.",
            "default": ""
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment description. Leading and trailing whitespace is removed. Omit or use null for an empty description.",
            "default": ""
          },
          "bill_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional bill or invoice number associated with this authorization charge. Maximum 25 characters after trimming. Send a string to preserve leading zeroes; longer values return HTTP 400. Leading and trailing whitespace is removed. Omit, use null, or send a blank string for no bill number. Bill numbers may be reused; reference must still be unique for each new payment. A matching creation retry retains the original bill number. Allowed characters: letters A-Z and a-z, digits 0-9, spaces, hyphens (-), underscores (_), dots (.), and slashes (/).",
            "examples": [
              "GYM/2026/01-1042",
              null
            ],
            "maxLength": 25,
            "pattern": "^[A-Za-z0-9 _./-]*$"
          }
        },
        "description": "Details for a payment using the authorization identified in the request path. Specify amount as integer laari; currency is always MVR and must not be supplied. Keep the same reference and amount when retrying. Reusing the reference with a different amount returns HTTP 409. Optionally include bill_number to associate the charge with a bill or invoice. A matching retry retains the original bill number.",
        "examples": [
          {
            "reference": "GYM-MEMBER-1042-2026-01",
            "amount": 50000,
            "title": "Monthly gym membership",
            "description": "Gym membership for member 1042, January 2026",
            "bill_number": "GYM/2026/01-1042"
          }
        ],
        "not": {
          "required": [
            "currency"
          ]
        }
      },
      "CreateAuthorizationRequest": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "Create Authorization",
        "type": "object",
        "required": [
          "reference",
          "title"
        ],
        "description": "Details of the customer approval requested for subscriptions, memberships, instalment plans, usage-based billing, repeat purchases, or a one-time payment. Choose the authorization type and spending limits to match your billing arrangement. Monetary limits are integer laari. A repeated request with an existing reference in your current mode returns the original authorization without updating its details.",
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "pattern": "\\S",
            "description": "Your reference for this authorization. Use a new reference for each authorization and keep the original reference when retrying a creation request. Leading and trailing whitespace is removed. Maximum 128 characters after trimming.",
            "maxLength": 128
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "pattern": "\\S",
            "description": "A title describing the customer approval you are requesting. Must contain at least one non-whitespace character. Leading and trailing whitespace is removed. Maximum 255 characters after trimming.",
            "maxLength": 255
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional description of the authorization. Leading and trailing whitespace is removed. Omit or use null if no description is needed. Maximum 500 characters after trimming.",
            "default": null,
            "maxLength": 500
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "one_time",
              "recurring",
              "unscheduled",
              null
            ],
            "default": "unscheduled",
            "description": "The payment arrangement approved by the customer:\n\n- `one_time`: one successful payment, such as a single order.\n- `recurring`: scheduled repeat payments, such as subscriptions, memberships, or instalments.\n- `unscheduled`: payments at irregular intervals, such as usage-based billing or repeat purchases.\n\nYour application submits each payment using the charge endpoint and controls when recurring charges are made. Defaults to unscheduled when omitted or null."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "MVR",
              null
            ],
            "default": "MVR",
            "description": "Always MVR. This field may be omitted; no other currency is supported."
          },
          "limits": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "per_payment": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1,
                "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit.",
                "maximum": 999999999999999999
              },
              "period": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "interval": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "day",
                      "week",
                      "month",
                      "year",
                      null
                    ]
                  },
                  "amount": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "format": "int64",
                    "minimum": 1,
                    "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit.",
                    "maximum": 999999999999999999
                  }
                },
                "description": "When amount is a positive integer, interval is required. Boundaries use +05:00: day at midnight, week on Sunday, month on the first, and year on January 1.",
                "allOf": [
                  {
                    "if": {
                      "type": "object",
                      "required": [
                        "amount"
                      ],
                      "properties": {
                        "amount": {
                          "type": "integer"
                        }
                      }
                    },
                    "then": {
                      "required": [
                        "interval"
                      ],
                      "properties": {
                        "interval": {
                          "type": "string",
                          "enum": [
                            "day",
                            "week",
                            "month",
                            "year"
                          ]
                        }
                      }
                    }
                  }
                ]
              },
              "total": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1,
                "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit.",
                "maximum": 999999999999999999
              },
              "max_payments": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1,
                "description": "Maximum number of successful payments. Omit or use null for no count limit. A one_time authorization always allows one payment; if supplied, this value must be 1.",
                "maximum": 4294967295
              }
            }
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?)?$",
            "description": "Optional expiry for the payment or authorization. For a new request, provide a future RFC 3339 timestamp, such as 2026-12-31T23:59:59+05:00. YYYY-MM-DD and YYYY-MM-DD HH:mm:ss are also accepted; a missing timezone offset means +05:00. Expiry is returned in +05:00 at second precision. Omit this field or use null for no expiry. A matching creation retry retains the original expiry, even if that time has passed.",
            "examples": [
              "2026-12-31T23:59:59+05:00",
              null
            ]
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2048,
            "description": "Optional URL for the customer to return to after approving or rejecting the authorization. Omit, use null, or send a blank string if you do not need a return URL. HTTP and HTTPS URLs are accepted. A matching creation retry retains the original return_url.",
            "examples": [
              "https://merchant.example/authorizations/complete",
              null
            ]
          }
        },
        "allOf": [
          {
            "if": {
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "const": "one_time"
                }
              }
            },
            "then": {
              "properties": {
                "limits": {
                  "properties": {
                    "max_payments": {
                      "enum": [
                        1,
                        null
                      ]
                    }
                  }
                }
              }
            }
          }
        ],
        "examples": [
          {
            "reference": "GYM-MEMBER-1042-2026",
            "title": "Monthly gym membership",
            "description": "Up to 12 monthly gym membership payments of MVR 500.00.",
            "type": "recurring",
            "currency": "MVR",
            "limits": {
              "per_payment": 50000,
              "period": {
                "interval": "month",
                "amount": 50000
              },
              "total": 600000,
              "max_payments": 12
            },
            "expires_at": "2026-12-31T23:59:59+05:00",
            "return_url": "https://merchant.example/authorizations/complete"
          }
        ]
      },
      "ResourceId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9-]{20,40}$",
        "description": "A resource ID returned by FahiPay. Use it unchanged in subsequent requests.",
        "examples": [
          "a1234567890b4cde8f0123456789abcd"
        ]
      },
      "Payment": {
        "type": "object",
        "description": "Payment amount, fee, and net_amount are flat integer values in laari; fee and net_amount are null until paid. Net amount equals amount minus fee. Currency is always MVR, so currency and unit fields are omitted. Payments linked to an authorization omit terminal_label, payment_url, qr_url, return_url, and expires_at. Payments without an authorization include these fields. All payments include payment_reference. All payments include paid_at, which records the original payment completion time or is null when unpaid or unavailable.",
        "required": [
          "id",
          "reference",
          "amount",
          "fee",
          "net_amount",
          "title",
          "description",
          "bill_number",
          "status",
          "payment_reference",
          "created_at",
          "paid_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9-]{20,40}$",
            "description": "The payment ID. Use it to retrieve this payment.",
            "examples": [
              "a1234567890b4cde8f0123456789abcd"
            ]
          },
          "reference": {
            "type": "string",
            "description": "Your reference provided when the payment was requested."
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "description": "Payment amount in laari. The currency is always MVR: 100 laari = MVR 1.00. For example, 2000 means MVR 20.00.",
            "examples": [
              2000
            ]
          },
          "fee": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0,
            "description": "The payment fee as a non-negative integer in laari when status is paid; null until payment is completed. For example, 40 means MVR 0.40. A zero fee is returned as 0.",
            "examples": [
              40,
              0,
              null
            ]
          },
          "net_amount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Payment proceeds after its fee, expressed as integer laari. Equals amount minus fee when status is paid; null otherwise. For example, amount 2000 and fee 40 give net_amount 1960 (MVR 19.60). Excludes later refunds or adjustments and does not indicate payout or settlement status.",
            "examples": [
              1960,
              0,
              null
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "terminal_label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10,
            "description": "The terminal label supplied when creating the hosted payment, or null when no label was supplied. Omitted for payments linked to an authorization, including charge responses, payment history, and webhooks.",
            "examples": [
              "COUNTER01",
              null
            ]
          },
          "bill_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "The bill number supplied when creating a hosted payment or charging an authorization, or null when no bill number was supplied. Allowed characters: letters A-Z and a-z, digits 0-9, spaces, hyphens (-), underscores (_), dots (.), and slashes (/).",
            "examples": [
              "BILL-2026-001",
              null
            ],
            "maxLength": 25,
            "pattern": "^[A-Za-z0-9 _./-]*$",
            "minLength": 1
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "rejected",
              "expired",
              "cancelled",
              "refunded"
            ],
            "description": "The payment status. Listing pending payments whose expiry time has been reached records and returns their status as expired. Other statuses are unchanged."
          },
          "payment_url": {
            "type": "string",
            "format": "uri",
            "description": "Hosted checkout URL to share with the customer. Omitted for payments linked to an authorization. Use the exact URL returned by the API; do not construct it from the resource ID. Customer-facing links use https://fahipay.mv/p/{uuid}. Use the returned URL as-is; its UUID may differ from the API resource ID."
          },
          "payment_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "The FahiPay transaction reference for the payment, or null when unavailable. Included for both hosted payments and authorization payments."
          },
          "qr_url": {
            "type": "string",
            "format": "uri",
            "description": "Public PayMV QR image link for this payment. No merchant credentials are required. Returns a PNG while the payment is pending and unexpired; otherwise returns HTTP 404. Omitted for payments linked to an authorization. Use the exact URL returned by the API; do not construct it from the resource ID."
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your return URL, or null if none was provided. Omitted for payments linked to an authorization."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The time the payment completed, as an RFC 3339 timestamp with a timezone offset. Null until status is paid or if the original payment time is unavailable. Matching retries retain the original payment time.",
            "examples": [
              "2026-01-01T12:05:00+05:00",
              null
            ]
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Hosted payment expiry timestamp with timezone offset. Defaults to 24 hours after creation unless a custom expiry is provided. Matching retries retain the original deadline. Null when no expiry is recorded. Omitted for payments linked to an authorization."
          },
          "metadata": {
            "$ref": "#/components/schemas/PaymentMetadata"
          }
        }
      },
      "Authorization": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "title",
          "description",
          "type",
          "currency",
          "limits",
          "usage",
          "status",
          "approval_url",
          "return_url",
          "approval_expires_at",
          "expires_at",
          "approved_at",
          "rejected_at",
          "revoked_at",
          "last_used_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9-]{20,40}$",
            "description": "The authorization ID. Use it to retrieve, charge, cancel, or list payments for this authorization.",
            "examples": [
              "a1234567890b4cde8f0123456789abcd"
            ]
          },
          "reference": {
            "type": "string",
            "description": "Your reference provided when the authorization was requested."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "one_time",
              "recurring",
              "unscheduled"
            ],
            "description": "The payment arrangement approved by the customer:\n\n- `one_time`: one successful payment, such as a single order.\n- `recurring`: scheduled repeat payments, such as subscriptions, memberships, or instalments.\n- `unscheduled`: payments at irregular intervals, such as usage-based billing or repeat purchases.\n\nYour application submits each payment using the charge endpoint and controls when recurring charges are made."
          },
          "currency": {
            "type": "string",
            "enum": [
              "MVR"
            ]
          },
          "limits": {
            "type": "object",
            "required": [
              "per_payment",
              "period",
              "total",
              "max_payments"
            ],
            "properties": {
              "per_payment": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1,
                "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit."
              },
              "period": {
                "type": "object",
                "required": [
                  "interval",
                  "amount"
                ],
                "properties": {
                  "interval": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "day",
                      "week",
                      "month",
                      "year",
                      null
                    ]
                  },
                  "amount": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "format": "int64",
                    "minimum": 1,
                    "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit."
                  }
                }
              },
              "total": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1,
                "description": "Positive integer in laari (100 = MVR 1.00), or null for no limit."
              },
              "max_payments": {
                "type": [
                  "integer",
                  "null"
                ],
                "format": "int64",
                "minimum": 1
              }
            }
          },
          "usage": {
            "type": "object",
            "required": [
              "payment_count",
              "total_paid"
            ],
            "properties": {
              "payment_count": {
                "type": "integer",
                "format": "int64",
                "minimum": 0
              },
              "total_paid": {
                "type": "integer",
                "format": "int64",
                "minimum": 0,
                "description": "Lifetime successful spending in laari."
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "rejected",
              "revoked",
              "expired",
              "cancelled",
              "suspended",
              "consumed"
            ]
          },
          "approval_url": {
            "type": "string",
            "format": "uri",
            "description": "Direct the customer to this URL to review and approve the authorization. Use the exact URL returned by the API; do not construct it from the resource ID. Customer-facing links use https://fahipay.mv/p/{uuid}. Use the returned URL as-is; its UUID may differ from the API resource ID."
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The return URL supplied when the authorization was created, or null when none was supplied. Use webhook events or GET /authorizations/{id} to confirm the final status; a return URL visit alone is not confirmation."
          },
          "approval_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Deadline for the customer to approve this authorization, including the timezone offset. New authorizations allow 24 hours from creation. Existing authorizations and matching creation retries retain their original approval deadlines. Null when unavailable."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time after which this authorization no longer permits new charges, including the timezone offset. Null when no expiry is set."
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "rejected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp with timezone offset; null when unavailable."
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": [
          "limit",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cursor for the next page when has_more is true; otherwise null. Pass it as after without changing the other filters. Concurrent status changes can produce a short or empty payment page; continue while has_more is true."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Use this value for application logic; ignore new codes you do not recognize and fall back to the HTTP status."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. Do not parse this text to identify the error."
              },
              "field": {
                "type": "string",
                "description": "Optional request field associated with a validation error, such as amount or customer.value."
              }
            }
          }
        }
      },
      "PaymentResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Payment"
          }
        }
      },
      "AuthorizationResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Authorization"
          }
        }
      },
      "PaymentListResponse": {
        "type": "object",
        "required": [
          "data",
          "pagination"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Payment"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "AuthorizationListResponse": {
        "type": "object",
        "required": [
          "data",
          "pagination"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Authorization"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "PaymentRequestCustomer": {
        "type": "object",
        "required": [
          "type",
          "value"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "mobile",
              "email",
              "vpa"
            ],
            "description": "Recipient identifier type: mobile, email, or FahiPay VPA."
          },
          "value": {
            "type": "string",
            "description": "Recipient mobile number, email address, or FahiPay VPA. VPA accepts name, name@fp, or name@fahipay and resolves by the VPA name."
          }
        }
      },
      "CreatePaymentRequestRequest": {
        "title": "Create Payment Request",
        "type": "object",
        "required": [
          "reference",
          "amount",
          "customer"
        ],
        "description": "Create a payment request for a customer by mobile, email, or FahiPay VPA. Fields align with hosted payment creation so customer approval can create a normal paid payment.",
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "pattern": "\\S",
            "description": "Your reference, unique across merchant payments and payment requests in the current mode. Maximum 100 characters after trimming. A retry must keep the same reference, amount, customer, title, description, terminal_label, bill_number, and return_url. If expires_at is supplied on the retry, it must match the original deadline. Changed details return HTTP 409 with reference_conflict.",
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "A positive integer amount in laari. Payments are always in MVR: 100 laari = MVR 1.00. For example, use 2000 for MVR 20.00. Send a JSON integer; omit currency.",
            "maximum": 9999999999
          },
          "customer": {
            "$ref": "#/components/schemas/PaymentRequestCustomer"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment title. Leading and trailing whitespace is removed. Omit or use null for no title. Maximum 255 characters after trimming.",
            "default": null,
            "maxLength": 255
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional payment description. Leading and trailing whitespace is removed. Omit or use null for no description. Maximum 500 characters after trimming.",
            "default": null,
            "maxLength": 500
          },
          "terminal_label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10,
            "description": "Optional label identifying the checkout counter or terminal where this payment originated. Maximum 10 characters after trimming leading and trailing whitespace. Omit, use null, or send a blank string for no label. Labels can be reused across payments. A retry must retain the original label.",
            "examples": [
              "COUNTER01",
              null
            ]
          },
          "bill_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional bill or invoice number associated with this payment request. Maximum 25 characters after trimming. Send a string to preserve leading zeroes; longer values return HTTP 400. Leading and trailing whitespace is removed. Omit, use null, or send a blank string for no bill number. Bill numbers may be reused; reference must still be unique for each new payment. A retry must retain the original bill number. Allowed characters: letters A-Z and a-z, digits 0-9, spaces, hyphens (-), underscores (_), dots (.), and slashes (/).",
            "examples": [
              "BILL-2026-001",
              null
            ],
            "maxLength": 25,
            "pattern": "^[A-Za-z0-9 _./-]*$"
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional URL for the customer to return to your website after checkout. Leading and trailing whitespace is removed. Maximum 2048 characters after trimming. HTTP and HTTPS URLs are accepted.",
            "maxLength": 2048
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}(?:[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?)?$",
            "description": "Optional payment expiry. Omit this field or use null to expire the payment 24 hours after creation. To choose a different expiry, provide a future RFC 3339 timestamp, such as 2026-12-31T23:59:59+05:00. YYYY-MM-DD and YYYY-MM-DD HH:mm:ss are also accepted; a missing timezone offset means +05:00. Expiry is returned in +05:00 at second precision. Retries keep the original expiry. If supplied on a retry, expires_at must match the original deadline.",
            "examples": [
              "2026-12-31T23:59:59+05:00",
              null
            ]
          }
        },
        "not": {
          "required": [
            "currency"
          ]
        },
        "examples": [
          {
            "reference": "SCHOOL-ACTIVITY-2026-001",
            "amount": 75000,
            "customer": {
              "type": "vpa",
              "value": "test@fp"
            },
            "title": "School activity fee",
            "description": "Activity fee for January 2026",
            "terminal_label": "COUNTER01",
            "bill_number": "ACT-2026-001",
            "return_url": "https://merchant.example/payment-requests/complete"
          }
        ]
      },
      "PaymentRequest": {
        "title": "Payment Request",
        "type": "object",
        "required": [
          "id",
          "reference",
          "amount",
          "fee",
          "net_amount",
          "title",
          "description",
          "terminal_label",
          "bill_number",
          "status",
          "payment_id",
          "payment_reference",
          "customer",
          "return_url",
          "created_at",
          "paid_at",
          "expires_at",
          "cancelled_at",
          "rejected_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9-]{20,40}$"
          },
          "reference": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "fee": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "net_amount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "terminal_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "bill_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "rejected",
              "expired",
              "cancelled",
              "refunded"
            ]
          },
          "payment_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Normal payment ID created when the request is paid."
          },
          "payment_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "FahiPay payment reference after payment."
          },
          "customer": {
            "$ref": "#/components/schemas/PaymentRequestCustomer"
          },
          "return_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "rejected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "metadata": {}
        },
        "examples": [
          {
            "id": "r1234567890b4cde8f0123456789abcd",
            "reference": "SCHOOL-ACTIVITY-2026-001",
            "amount": 75000,
            "fee": null,
            "net_amount": null,
            "title": "School activity fee",
            "description": "Activity fee for January 2026",
            "terminal_label": "COUNTER01",
            "bill_number": "ACT-2026-001",
            "status": "pending",
            "payment_id": null,
            "payment_reference": null,
            "customer": {
              "type": "vpa",
              "value": "test@fp"
            },
            "return_url": "https://merchant.example/payment-requests/complete",
            "created_at": "2026-01-01T12:00:00+05:00",
            "paid_at": null,
            "expires_at": "2026-01-02T12:00:00+05:00",
            "cancelled_at": null,
            "rejected_at": null,
            "metadata": {}
          }
        ]
      },
      "PaymentRequestResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PaymentRequest"
          }
        }
      },
      "PaymentRequestListResponse": {
        "type": "object",
        "required": [
          "data",
          "pagination"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentRequest"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        }
      },
      "PaymentMetadata": {
        "type": "object",
        "maxProperties": 50,
        "propertyNames": {
          "minLength": 1,
          "maxLength": 40
        },
        "additionalProperties": {
          "type": "string",
          "maxLength": 500
        },
        "description": "Optional merchant-supplied data for this hosted payment. Send a JSON object with at most 50 entries, non-empty keys of at most 40 characters, and string values of at most 500 characters. Values are preserved without trimming. Nested objects, arrays, numbers, booleans and null are not accepted. Omit or send {} for no metadata. Metadata is returned in authenticated payment responses and payment webhooks, but is not shown on public checkout. Do not include secrets or sensitive payment information. A matching reference-and-amount retry returns the original metadata without updating it.",
        "examples": [
          {
            "membership_id": "MEM-123",
            "billing_period": "2026-10"
          },
          {}
        ]
      }
    },
    "headers": {
      "FahiPayVersion": {
        "description": "The Merchant API version used for this response. For an unsupported version error, this identifies the latest supported version.",
        "schema": {
          "type": "string"
        },
        "example": "2027-07-01"
      }
    }
  },
  "x-error-codes": [
    {
      "group": "Request and access",
      "status": 400,
      "code": "invalid_request",
      "cause": "A request field is missing, invalid or unsupported.",
      "action": "Correct error.field when supplied. Send amounts as positive integer laari.",
      "retry": "Correct the request"
    },
    {
      "group": "Request and access",
      "status": 400,
      "code": "invalid_json",
      "cause": "The request body is not valid JSON.",
      "action": "Send one JSON object with Content-Type: application/json.",
      "retry": "Correct the request"
    },
    {
      "group": "Request and access",
      "status": 400,
      "code": "request_too_large",
      "cause": "The request body exceeds the accepted size.",
      "action": "Reduce the request body and remove unnecessary content.",
      "retry": "Correct the request"
    },
    {
      "group": "Request and access",
      "status": 400,
      "code": "unsupported_api_version",
      "cause": "FahiPay-Version is empty or unsupported.",
      "action": "Use 2027-07-01, or omit the header to select the latest version.",
      "retry": "Correct the header"
    },
    {
      "group": "Request and access",
      "status": 400,
      "code": "customer_not_found",
      "cause": "The payment-request recipient could not be resolved.",
      "action": "Check the mobile number, email address or FahiPay VPA with the customer.",
      "retry": "Correct the recipient"
    },
    {
      "group": "Request and access",
      "status": 401,
      "code": "unauthorized",
      "cause": "Credentials are invalid, expired, missing, or the merchant is inactive.",
      "action": "Check the credential and merchant access. Obtain a new token if yours expired.",
      "retry": "Fix access first"
    },
    {
      "group": "Request and access",
      "status": 403,
      "code": "insufficient_scope",
      "cause": "The credential does not grant the required operation.",
      "action": "Use a token granted the scope named in the message.",
      "retry": "Fix permissions first"
    },
    {
      "group": "Request and access",
      "status": 404,
      "code": "not_found",
      "cause": "The resource, route or public QR is unavailable.",
      "action": "Check the URL, resource ID, merchant account and live or sandbox mode.",
      "retry": "Check the resource"
    },
    {
      "group": "Request and access",
      "status": 404,
      "code": "authorization_not_found",
      "cause": "The charge authorization was not found for this merchant and mode.",
      "action": "Use the authorization ID returned by the API and check the account mode.",
      "retry": "Check the resource"
    },
    {
      "group": "Request and access",
      "status": 404,
      "code": "payment_request_not_found",
      "cause": "The payment request was not found for this merchant and mode.",
      "action": "Check the request ID and the credentials used to retrieve or cancel it.",
      "retry": "Check the resource"
    },
    {
      "group": "Request and access",
      "status": 405,
      "code": "method_not_allowed",
      "cause": "This route does not accept the HTTP method used.",
      "action": "Use the documented method; check Allow when it is returned.",
      "retry": "Correct the method"
    },
    {
      "group": "Payments and customer wallet",
      "status": 402,
      "code": "insufficient_balance",
      "cause": "The pre-charge check found insufficient customer balance.",
      "action": "Ask the customer to add funds. Retry the same charge only after the balance changes.",
      "retry": "After customer action"
    },
    {
      "group": "Payments and customer wallet",
      "status": 422,
      "code": "wallet_limit_exceeded",
      "cause": "The customer wallet-limit check rejected the charge.",
      "action": "The customer may need to wait for a limit reset or change their wallet tier. Do not repeatedly retry an unchanged limit.",
      "retry": "After the limit changes"
    },
    {
      "group": "Payments and customer wallet",
      "status": 409,
      "code": "reference_conflict",
      "cause": "The reference already belongs to a different payment, amount or request.",
      "action": "Check your original request. For a retry, retain its reference and amount. Use a new reference only for a genuinely new payment.",
      "retry": "Resolve the conflict"
    },
    {
      "group": "Payments and customer wallet",
      "status": 409,
      "code": "payment_not_cancellable",
      "cause": "The payment is no longer pending.",
      "action": "Retrieve the payment. Cancellation cannot reverse a paid payment.",
      "retry": "Do not retry unchanged"
    },
    {
      "group": "Payments and customer wallet",
      "status": 409,
      "code": "payment_request_not_cancellable",
      "cause": "The request is no longer pending, has expired, or is being processed.",
      "action": "Retrieve it first. Retry cancellation after processing only if it remains pending and unexpired.",
      "retry": "Check the current state"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_unapproved",
      "cause": "No customer has approved this authorization.",
      "action": "Wait for confirmed approval before submitting a new charge.",
      "retry": "After approval"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_inactive",
      "cause": "The authorization is not active.",
      "action": "Retrieve it. A revoked or cancelled authorization needs a new customer-approved authorization.",
      "retry": "Check the current state"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_expired",
      "cause": "The authorization expiry has passed.",
      "action": "Create a new authorization and obtain customer approval.",
      "retry": "Use a new authorization"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_not_pending",
      "cause": "The authorization is no longer awaiting a decision.",
      "action": "Retrieve its current state before attempting another approval or rejection.",
      "retry": "Check the current state"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_unavailable",
      "cause": "The authorization changed or is unavailable for this operation.",
      "action": "Retrieve it to check its state, expiry and any operation in progress.",
      "retry": "Check the current state"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "authorization_not_cancellable",
      "cause": "The authorization cannot be cancelled in its current state or while processing.",
      "action": "Retrieve it. If a charge is in progress, wait for its outcome before retrying cancellation.",
      "retry": "Check the current state"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "per_payment_limit_exceeded",
      "cause": "The amount exceeds the approved per-payment limit.",
      "action": "Correct an incorrect amount or obtain approval for appropriate terms. Do not split charges to bypass the approved cap.",
      "retry": "Change approved terms"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "period_limit_exceeded",
      "cause": "The charge exceeds the remaining approved period allowance.",
      "action": "Wait for the period to reset or obtain a new approval. Periods use +05:00; weeks begin on Sunday.",
      "retry": "After reset or approval"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "total_limit_exceeded",
      "cause": "The charge exceeds the remaining lifetime authorization allowance.",
      "action": "Obtain a new authorization if further payments are needed.",
      "retry": "Use a new authorization"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "payment_count_limit_exceeded",
      "cause": "The approved number of payments has been reached.",
      "action": "Obtain a new authorization for further payments.",
      "retry": "Use a new authorization"
    },
    {
      "group": "Authorization state and limits",
      "status": 409,
      "code": "currency_mismatch",
      "cause": "The stored authorization currency does not match MVR.",
      "action": "Check the authorization and contact support if the mismatch persists. Merchant charges use MVR.",
      "retry": "Resolve before retrying"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 409,
      "code": "payment_in_progress",
      "cause": "An operation already holds the authorization processing lock.",
      "action": "Wait briefly and retry with the original authorization, reference and amount. If it persists, contact support.",
      "retry": "Wait, then retry"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 409,
      "code": "payment_conflict",
      "cause": "A payment operation conflicts with the current resource state.",
      "action": "Retrieve the payment and resolve its state before trying again.",
      "retry": "Check the current state"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 409,
      "code": "conflict",
      "cause": "The operation conflicts with the current resource state.",
      "action": "Retrieve the resource. If the cause remains unclear, contact support with the resource ID and request time.",
      "retry": "Check the current state"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 429,
      "code": "rate_limited",
      "cause": "Requests are being restricted.",
      "action": "Reduce request frequency and retry with increasing delays. Honor Retry-After if present; it is not guaranteed.",
      "retry": "Back off, then retry"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 500,
      "code": "internal_error",
      "cause": "An unexpected server error occurred; the payment outcome may be unknown.",
      "action": "Look up the payment or retry using the original reference and amount. Never create a new reference merely because a request timed out or returned 500.",
      "retry": "Reconcile, then retry"
    },
    {
      "group": "Temporary and unexpected errors",
      "status": 501,
      "code": "not_implemented",
      "cause": "This operation is not available.",
      "action": "Use a supported operation or contact support.",
      "retry": "Do not retry unchanged"
    }
  ]
}
