{
  "openapi": "3.0.3",
  "info": {
    "title": "Telspay Stakeholder API",
    "version": "1.0.0",
    "description": "Release 2026.9.0. Server-to-server HMAC API. A security scheme declaration alone cannot produce signatures: follow docs.html and the included PHP client. Requests cannot directly withdraw, transfer, approve loans or post deposits."
  },
  "servers": [
    {
      "url": "https://www.telspay.com/api/v1/sandbox",
      "description": "Proposed public domain; replace with your configured deployment. Synthetic data only."
    },
    {
      "url": "https://www.telspay.com/api/v1/live",
      "description": "Approved live SACCO access."
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/products": {
      "get": {
        "summary": "List approved SACCO loan products",
        "operationId": "get_products",
        "description": "Returns products assigned to the client\u2019s SACCO. Interpret configured_rate and interest_method using SACCO policy, not as an assumed APR. Required permission: `products:read`.",
        "tags": [
          "Products"
        ],
        "x-required-scope": "products:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LoanProduct"
                      }
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fixed-products": {
      "get": {
        "summary": "List available fixed-deposit products",
        "operationId": "get_fixed_products",
        "description": "Rates are in basis points: 700 means 7.00% a year. Tax applies only to gross interest. The accepted booking terms remain fixed. Required permission: `products:read`.",
        "tags": [
          "Products"
        ],
        "x-required-scope": "products:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FixedProduct"
                      }
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}": {
      "get": {
        "summary": "Read authorised membership details",
        "operationId": "get_members_member_id",
        "description": "Requires unexpired member consent, an active member and the same SACCO as the client. Required permission: `members:read`.",
        "tags": [
          "Member data"
        ],
        "x-required-scope": "members:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Member"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}/balances": {
      "get": {
        "summary": "Read authorised member balances",
        "operationId": "get_members_member_id_balances",
        "description": "Savings is the signed legacy ledger total. Available excludes old active fixed holds and unposted positive entries; new fixed placements and group reservations are already debited. Final spending checks also enforce minimum balance and pending withdrawals. Required permission: `balances:read`.",
        "tags": [
          "Member data"
        ],
        "x-required-scope": "balances:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Balances"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}/statement": {
      "get": {
        "summary": "Read savings statement page",
        "operationId": "get_members_member_id_statement",
        "description": "Returns the signed savings ledger. Includes legacy records as recorded; it is not a bank settlement confirmation. Shares are not a savings transaction. Required permission: `statements:read`.",
        "tags": [
          "Member data"
        ],
        "x-required-scope": "statements:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Exclusive savings transaction ID cursor; results are oldest first."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Transaction"
                      }
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "next_after": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}/groups": {
      "get": {
        "summary": "List authorised group memberships",
        "operationId": "get_members_member_id_groups",
        "description": "Returns up to 100 active memberships. Does not expose other group members\u2019 personal accounts. Required permission: `groups:read`.",
        "tags": [
          "Member data"
        ],
        "x-required-scope": "groups:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Group"
                      }
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/members/{member_id}/fixed-deposits": {
      "get": {
        "summary": "List authorised fixed deposits",
        "operationId": "get_members_member_id_fixed_deposits",
        "description": "Returns up to 100 latest placements belonging to this individual member. Required permission: `fixed:read`.",
        "tags": [
          "Member data"
        ],
        "x-required-scope": "fixed:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/MemberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FixedDeposit"
                      }
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/loan-intents": {
      "post": {
        "summary": "Request a member-reviewed loan application",
        "operationId": "post_loan_intents",
        "description": "The member continues in the existing PIN, KYC, affordability, guarantor and SACCO-review process. This endpoint never disburses or approves a loan. Required permission: `loans:request`.",
        "tags": [
          "Requests"
        ],
        "x-required-scope": "loans:request",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/Idempotency"
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted for member review; no funds posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IntentAccepted"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoanIntent"
              }
            }
          }
        }
      }
    },
    "/fixed-deposit-intents": {
      "post": {
        "summary": "Request a member-reviewed fixed deposit",
        "operationId": "post_fixed_deposit_intents",
        "description": "The member must accept a fresh quote and enter their PIN before funds are reserved from their own savings. Required permission: `fixed:request`.",
        "tags": [
          "Requests"
        ],
        "x-required-scope": "fixed:request",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/Idempotency"
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted for member review; no funds posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IntentAccepted"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FixedIntent"
              }
            }
          }
        }
      }
    },
    "/deposit-notices": {
      "post": {
        "summary": "Notify a member of a claimed payment",
        "operationId": "post_deposit_notices",
        "description": "A notice is untrusted payment information. Member acknowledgement and administrator reconciliation do not automatically credit funds; post only through the existing verified payment workflow. Required permission: `deposits:notify`.",
        "tags": [
          "Requests"
        ],
        "x-required-scope": "deposits:notify",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/Idempotency"
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted for member review; no funds posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/IntentAccepted"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DepositNotice"
              }
            }
          }
        }
      }
    },
    "/requests/{request_id}": {
      "get": {
        "summary": "Read your integration request status",
        "operationId": "get_requests_request_id",
        "description": "Only this client can read its request. Live reads still require the member\u2019s current consent for that request kind. The loan reference means submitted, not approved or disbursed. Required permission: `scope for the request kind`.",
        "tags": [
          "Requests"
        ],
        "x-required-scope": "scope for the request kind",
        "parameters": [
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RequestStatus"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "SANDBOX"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "request_id"
                  ]
                }
              }
            }
          },
          "default": {
            "description": "Error; see HTTP status and error.code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Key ID, not the secret. Also send the required signed timestamp and nonce headers."
      }
    },
    "parameters": {
      "Timestamp": {
        "name": "X-API-Timestamp",
        "in": "header",
        "required": true,
        "description": "Current Unix seconds; maximum absolute clock skew is 300 seconds.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]{10}$"
        }
      },
      "Nonce": {
        "name": "X-API-Nonce",
        "in": "header",
        "required": true,
        "description": "Fresh random value for every HTTP attempt. Never reuse a nonce, even for a retry.",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 96,
          "pattern": "^[A-Za-z0-9_-]+$"
        }
      },
      "Signature": {
        "name": "X-API-Signature",
        "in": "header",
        "required": true,
        "description": "Lowercase hex HMAC-SHA256. See signing instructions in docs.html.",
        "schema": {
          "type": "string",
          "pattern": "^[a-f0-9]{64}$"
        }
      },
      "Idempotency": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Persist one unique key per logical mutation. Retrying uses the same key and payload, with a fresh nonce/signature.",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 96,
          "pattern": "^[A-Za-z0-9_-]+$"
        }
      },
      "MemberId": {
        "name": "member_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^mem_[a-f0-9]{32}$",
          "example": "mem_00000000000000000000000000000001"
        }
      },
      "RequestId": {
        "name": "request_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^req_[a-f0-9]{32}$"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message"
            ]
          },
          "request_id": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "request_id"
        ]
      },
      "LoanIntent": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "pattern": "^mem_[a-f0-9]{32}$",
            "example": "mem_00000000000000000000000000000001"
          },
          "product_id": {
            "type": "integer",
            "minimum": 1,
            "example": 1
          },
          "amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "months": {
            "type": "integer",
            "minimum": 1,
            "maximum": 600,
            "example": 6
          }
        },
        "required": [
          "member_id",
          "product_id",
          "amount",
          "months"
        ]
      },
      "FixedIntent": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "pattern": "^mem_[a-f0-9]{32}$",
            "example": "mem_00000000000000000000000000000001"
          },
          "product_id": {
            "type": "integer",
            "minimum": 1,
            "example": 1
          },
          "amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "matures_on": {
            "type": "string",
            "format": "date",
            "description": "Future UTC calendar date within the selected product term."
          }
        },
        "required": [
          "member_id",
          "product_id",
          "amount",
          "matures_on"
        ]
      },
      "DepositNotice": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "pattern": "^mem_[a-f0-9]{32}$",
            "example": "mem_00000000000000000000000000000001"
          },
          "amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "reference": {
            "type": "string",
            "minLength": 3,
            "maxLength": 100,
            "example": "BANK-REFERENCE-001"
          },
          "channel": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80,
            "example": "Bank transfer"
          }
        },
        "required": [
          "member_id",
          "amount",
          "reference",
          "channel"
        ]
      },
      "Member": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "pattern": "^mem_[a-f0-9]{32}$",
            "example": "mem_00000000000000000000000000000001"
          },
          "member_number": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "member_id",
          "member_number",
          "name",
          "status"
        ]
      },
      "Balances": {
        "type": "object",
        "properties": {
          "savings": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "legacy_fixed": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "available": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "shares": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "fixed_deposits": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "currency": {
            "type": "string",
            "enum": [
              "UGX"
            ]
          }
        },
        "required": [
          "currency",
          "savings",
          "available",
          "legacy_fixed",
          "shares",
          "fixed_deposits"
        ]
      },
      "LoanProduct": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "maximum_amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "maximum_months": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ]
          },
          "configured_rate": {
            "type": "string"
          },
          "interest_method": {
            "type": "string"
          }
        }
      },
      "FixedProduct": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "public_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "min_amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "max_amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "min_days": {
            "type": "integer"
          },
          "max_days": {
            "type": "integer"
          },
          "annual_rate_bps": {
            "type": "integer"
          },
          "day_basis": {
            "type": "integer"
          },
          "tax_bps": {
            "type": "integer"
          },
          "early_exit_allowed": {
            "type": "integer"
          },
          "early_fee_bps": {
            "type": "integer"
          }
        }
      },
      "Transaction": {
        "type": "object",
        "properties": {
          "transaction_id": {
            "type": "integer"
          },
          "transaction_timestamp": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ],
            "description": "Unix seconds, UTC."
          },
          "amount": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "reference": {
            "type": "string",
            "nullable": true
          },
          "payment_method": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Group": {
        "type": "object",
        "properties": {
          "group_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "role": {
            "type": "string"
          }
        }
      },
      "FixedDeposit": {
        "type": "object",
        "properties": {
          "deposit_id": {
            "type": "string"
          },
          "principal": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "started_on": {
            "type": "string",
            "format": "date"
          },
          "matures_on": {
            "type": "string",
            "format": "date"
          },
          "annual_rate_bps": {
            "type": "integer"
          },
          "day_basis": {
            "type": "integer"
          },
          "expected_gross": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "expected_tax": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation."
          },
          "status": {
            "type": "string"
          },
          "gross_paid": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation.",
            "nullable": true
          },
          "tax_paid": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation.",
            "nullable": true
          },
          "net_paid": {
            "type": "string",
            "pattern": "^-?(0|[1-9][0-9]{0,12})(\\.[0-9]{1,2})?$",
            "example": "100000.00",
            "description": "UGX decimal amount encoded as a string, at most two decimal places. Do not send floats or scientific notation.",
            "nullable": true
          },
          "settled_at": {
            "type": "string",
            "nullable": true,
            "description": "UTC YYYY-MM-DD HH:MM:SS, null before settlement."
          }
        }
      },
      "IntentAccepted": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string",
            "pattern": "^req_[a-f0-9]{32}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "AWAITING_MEMBER_REVIEW",
              "SIMULATED"
            ]
          },
          "review_url": {
            "type": "string",
            "format": "uri"
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "request_id",
          "status"
        ]
      },
      "RequestStatus": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string",
            "pattern": "^req_[a-f0-9]{32}$"
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "AWAITING_MEMBER_REVIEW",
              "SIMULATED",
              "EXPIRED",
              "REJECTED",
              "COMPLETED",
              "SUBMITTED_TO_SACCO",
              "MEMBER_ACKNOWLEDGED",
              "REFERRED_FOR_RECONCILIATION"
            ]
          },
          "result_ref": {
            "type": "string",
            "nullable": true
          },
          "expires_at": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "request_id",
          "kind",
          "status",
          "created_at"
        ]
      }
    }
  }
}
