{
  "components": {
    "schemas": {
      "ApiErrorBody": {
        "properties": {
          "code": {
            "type": "string"
          },
          "details": {
            "items": {
              "$ref": "#/components/schemas/ApiErrorDetail"
            },
            "type": "array"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message",
          "details"
        ],
        "type": "object"
      },
      "ApiErrorDetail": {
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "type": "object"
      },
      "ApiErrorResponse": {
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiErrorBody"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "CheckoutSessionCreatedResponse": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "expiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "url",
          "amount",
          "currency",
          "description",
          "externalId",
          "expiresAt"
        ],
        "type": "object"
      },
      "CheckoutSessionView": {
        "properties": {
          "amountMinorUnits": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "failureReasonCode": {
            "type": [
              "null",
              "string"
            ]
          },
          "merchantDisplayName": {
            "type": "string"
          },
          "returnUrl": {
            "type": [
              "null",
              "string"
            ]
          },
          "status": {
            "type": "string"
          },
          "transactionStatus": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "required": [
          "status",
          "merchantDisplayName",
          "amountMinorUnits",
          "currency",
          "description",
          "returnUrl",
          "transactionStatus",
          "failureReasonCode"
        ],
        "type": "object"
      },
      "CollectionResponse": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "failureReasonCode": {
            "type": [
              "null",
              "string"
            ]
          },
          "fee": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "id": {
            "type": "string"
          },
          "msisdn": {
            "type": "string"
          },
          "net": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status",
          "amount",
          "currency",
          "fee",
          "net",
          "msisdn",
          "description",
          "externalId",
          "failureReasonCode",
          "createdAt"
        ],
        "type": "object"
      },
      "CreateCheckoutSessionRequest": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "returnUrl": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "required": [
          "amount",
          "currency",
          "description"
        ],
        "type": "object"
      },
      "CreateCollectionRequest": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "msisdn": {
            "type": "string"
          }
        },
        "required": [
          "amount",
          "currency",
          "msisdn",
          "description"
        ],
        "type": "object"
      },
      "CreatePayoutRequest": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "msisdn": {
            "type": "string"
          }
        },
        "required": [
          "amount",
          "currency",
          "msisdn",
          "description"
        ],
        "type": "object"
      },
      "CreateRefundRequest": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "collectionId": {
            "type": "string"
          },
          "description": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "required": [
          "collectionId"
        ],
        "type": "object"
      },
      "PayCheckoutSessionAcceptedResponse": {
        "properties": {
          "status": {
            "type": "string"
          }
        },
        "required": [
          "status"
        ],
        "type": "object"
      },
      "PayCheckoutSessionRequest": {
        "properties": {
          "msisdn": {
            "type": "string"
          }
        },
        "required": [
          "msisdn"
        ],
        "type": "object"
      },
      "PayoutResponse": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "externalId": {
            "type": [
              "null",
              "string"
            ]
          },
          "failureReasonCode": {
            "type": [
              "null",
              "string"
            ]
          },
          "fee": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "id": {
            "type": "string"
          },
          "msisdn": {
            "type": "string"
          },
          "net": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status",
          "amount",
          "currency",
          "fee",
          "net",
          "msisdn",
          "description",
          "externalId",
          "failureReasonCode",
          "createdAt"
        ],
        "type": "object"
      },
      "RefundResponse": {
        "properties": {
          "amount": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ]
          },
          "collectionId": {
            "type": "string"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "failureReasonCode": {
            "type": [
              "null",
              "string"
            ]
          },
          "fee": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "id": {
            "type": "string"
          },
          "msisdn": {
            "type": "string"
          },
          "net": {
            "format": "int64",
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ]
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status",
          "collectionId",
          "amount",
          "currency",
          "fee",
          "net",
          "msisdn",
          "description",
          "failureReasonCode",
          "createdAt"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "description": "Your secret key as a bearer token: `Authorization: Bearer sk_test_…`. Secret keys are server-side only and must never reach a browser. The three `/v1/checkout/{token}` routes are the deliberate exception — they are called BY the hosted checkout page and are authorised by the session token in the path, not by a key.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Accept and send Mobile Money payments in Central Africa.\n\nAuthenticate with a secret key as a bearer token. Every mutating request carries a client-generated `Idempotency-Key` UUID, and duplicates replay the original response rather than charging twice.\n\nTransaction status is authoritative by polling — webhooks are a convenience, never the only way to learn an outcome.",
    "title": "MoniWave API",
    "version": "v1"
  },
  "openapi": "3.1.1",
  "paths": {
    "/v1/checkout-sessions": {
      "post": {
        "description": "Creates a payment page and returns its URL. Use this when you do NOT have the\r\ncustomer's number — a payment link in a chat, a \"Pay now\" button, an invoice email.\r\nIf you already know the number, call `POST /v1/collections` and skip checkout.\r\n\r\nThe amount is fixed here, by you. The returned token authorises paying this one\r\nbill and nothing else, so a leaked link risks someone paying a bill that was\r\nalready owed — not minting a new charge.\r\n\r\nYou learn the outcome the same way as any other payment: the webhook for the\r\nresulting collection, or by polling it. The page is not your notification channel.\r\n\r\nRequires an `Idempotency-Key` header.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCheckoutSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionCreatedResponse"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Bad Request"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Conflict"
          }
        },
        "summary": "Create a checkout session",
        "tags": [
          "Hosted checkout"
        ]
      }
    },
    "/v1/checkout/{token}": {
      "get": {
        "description": "Called by the payment page to render itself, and polled to watch the outcome.\r\nAuthorised by the token in the path — no key, which is why this route is\r\nanonymous.\r\n\r\nReturns only what the page must display: merchant display name, amount, currency,\r\ndescription, return URL and status. Never the merchant id, never anything about\r\nother transactions.\r\n\r\nAn unknown, malformed or expired token all answer 404 identically. This URL is\r\nguessable by anyone on the internet, so it must not reveal whether a session ever\r\nexisted.",
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionView"
                }
              }
            },
            "description": "OK"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "security": [],
        "summary": "Read a checkout session",
        "tags": [
          "Hosted checkout"
        ]
      }
    },
    "/v1/checkout/{token}/pay": {
      "post": {
        "description": "Submits the customer's number and starts the charge. Anonymous — the token in the\r\npath is the entire authorisation.\r\n\r\nReturns **202 Accepted**, not 200: the operator has prompted the customer and they\r\nhave not approved yet. Poll `GET /v1/checkout/{token}` for the outcome.\r\n\r\nOne session yields at most one successful payment. Concurrent attempts are\r\nserialised, so a double-tap cannot produce two charges — but a FAILED attempt may\r\nbe retried on the same link, which is what lets a customer who mistyped their\r\nnumber try again.\r\n\r\nNo `Idempotency-Key`: a browser has no secret and no stable identity, so the\r\nsession itself is the idempotency scope.",
        "parameters": [
          {
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayCheckoutSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayCheckoutSessionAcceptedResponse"
                }
              }
            },
            "description": "Accepted"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Bad Request"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "security": [],
        "summary": "Pay a checkout session",
        "tags": [
          "Hosted checkout"
        ]
      }
    },
    "/v1/collections": {
      "post": {
        "description": "Charges a customer's mobile wallet. Returns immediately with status `accepted` — the customer still has to approve on their phone, so this is not a completed payment. Poll the collection or wait for the webhook.\n\nRequires an `Idempotency-Key` header (a UUID you generate). Retrying with the same key replays the original response instead of charging twice.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCollectionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CollectionResponse"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Bad Request"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Conflict"
          }
        },
        "summary": "Create a collection",
        "tags": [
          "Collections"
        ]
      }
    },
    "/v1/collections/{id}": {
      "get": {
        "description": "The authoritative status. Webhooks are a convenience; this is the source of truth, and a client that only listens for callbacks will eventually miss one.\n\nAnother merchant's collection returns 404, identical to an id that does not exist — a 403 would confirm it exists.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CollectionResponse"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "summary": "Retrieve a collection",
        "tags": [
          "Collections"
        ]
      }
    },
    "/v1/payouts": {
      "post": {
        "description": "Sends money to a mobile wallet. Two different balances can refuse it: your own\nspendable balance, checked here (`payout_exceeds_balance` — the amount plus its fee\nmust fit); and our operator float, checked at submission, which FAILS the payout\nwith `insufficient_float` rather than rejecting the call.\n\nA payout you requested seconds ago already holds its amount, even though your\nposted balance has not moved — the ledger only posts on completion.\n\nRequires an `Idempotency-Key` header.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePayoutRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Bad Request"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Conflict"
          }
        },
        "summary": "Create a payout",
        "tags": [
          "Payouts"
        ]
      }
    },
    "/v1/payouts/{id}": {
      "get": {
        "description": "`completed` means the recipient has the money. Another merchant's payout returns 404, identical to an id that does not exist.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "summary": "Retrieve a payout",
        "tags": [
          "Payouts"
        ]
      }
    },
    "/v1/ping": {
      "get": {
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [],
        "tags": [
          "MoniWave.Api.Public"
        ]
      }
    },
    "/v1/refunds": {
      "post": {
        "description": "A refund is its own transaction with its own `ref_` id — never a state change on\nthe original collection, which stays `completed` forever.\n\nOnly `completed` collections are refundable. Omit `amount` to refund everything\nstill refundable; partial refunds are allowed and accumulate, and a refund still in\nflight holds its amount, so `refund_exceeds_refundable` can appear even when the\narithmetic looks fine.\n\nAn unknown collection, or one belonging to another merchant, returns 404.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRefundRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundResponse"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Bad Request"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Conflict"
          }
        },
        "summary": "Refund a collection",
        "tags": [
          "Refunds"
        ]
      }
    },
    "/v1/refunds/{id}": {
      "get": {
        "description": "Another merchant's refund returns 404, identical to a missing id.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundResponse"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "summary": "Retrieve a refund",
        "tags": [
          "Refunds"
        ]
      }
    }
  },
  "security": [
    {
      "ApiKey": []
    }
  ],
  "servers": [
    {
      "description": "Sandbox (preview) — deterministic simulator, sk_test_ keys",
      "url": "https://api-dev.moni-wave.com"
    },
    {
      "description": "Sandbox at general availability — not yet serving",
      "url": "https://api-sandbox.moni-wave.com"
    },
    {
      "description": "Local development",
      "url": "http://localhost:6003"
    }
  ],
  "tags": [
    {
      "name": "Hosted checkout"
    },
    {
      "name": "MoniWave.Api.Public"
    },
    {
      "name": "Collections"
    },
    {
      "name": "Payouts"
    },
    {
      "name": "Refunds"
    }
  ]
}
