{
  "openapi": "3.1.0",
  "info": {
    "title": "Shainee identity provider: partner API",
    "version": "1.0.0",
    "description": "The routes a partner server calls: verifying sign-in, and taking payments held in escrow. Everything else a partner needs is a route on the partner's own server, described in the guide."
  },
  "servers": [
    {
      "url": "https://superapp.ideapreneurnepal.com",
      "description": "Hosted service"
    },
    {
      "url": "https://superapp.ideapreneurnepal.com/sandbox",
      "description": "Sandbox: the same verify and keys routes with fake users"
    }
  ],
  "paths": {
    "/api/launch/verify": {
      "post": {
        "operationId": "verifyLaunchToken",
        "summary": "Verify a launch token",
        "description": "Proves which user opened your app. Call it from your server with your client credentials, passing the token the page received and the nonce your server issued for that page load. On success the token is used up: a second call with the same token answers `replayed`. A call that fails because of a wrong nonce or wrong credentials does not use the token up.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "example": {
                "token": "eyJhbGciOiJFZERTQSIsImtp...Cjw40TDnu8Dg",
                "nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The token is valid. `name` is present only if the user approved the `profile` scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResult"
                },
                "example": {
                  "sub": "6wVFC8ZW-uir4EjfnDOwK95teeinCnN1NrjjLgXvNgQ",
                  "scopes": [
                    "profile"
                  ],
                  "name": "Demo Player"
                }
              }
            }
          },
          "400": {
            "description": "The request or the token was refused. See the error codes below.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "replayed": {
                    "summary": "The token was already used",
                    "value": {
                      "error": "replayed"
                    }
                  },
                  "nonce_mismatch": {
                    "summary": "The nonce is not the one the token was issued for",
                    "value": {
                      "error": "nonce_mismatch"
                    }
                  },
                  "invalid_token": {
                    "summary": "Not a valid token",
                    "value": {
                      "error": "invalid_token"
                    }
                  },
                  "invalid_request": {
                    "summary": "A field is missing or malformed",
                    "value": {
                      "error": "invalid_request",
                      "detail": "nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only.",
                      "fields": {
                        "nonce": [
                          "nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only."
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/jwks.json": {
      "get": {
        "operationId": "getSigningKeys",
        "summary": "Get the public signing keys",
        "description": "Optional. The public keys that launch tokens are signed with, as a JSON Web Key Set. Use them only if you also want to check a token's signature yourself; the verify route is still required. Cache the keys and fetch again when you meet a `kid` you do not know.",
        "security": [],
        "responses": {
          "200": {
            "description": "The key set. Tokens are signed with EdDSA (Ed25519).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Jwks"
                },
                "example": {
                  "keys": [
                    {
                      "kty": "OKP",
                      "crv": "Ed25519",
                      "alg": "EdDSA",
                      "use": "sig",
                      "kid": "oGSRh-ZVy5_mJPgS",
                      "x": "sMZWbe0Cf1cFc3PEcU_oJk-Gl9PenmDSj1l-JvNTmwE"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sandbox/api/launch/token": {
      "post": {
        "operationId": "mintSandboxToken",
        "summary": "Sandbox only: get a launch token",
        "description": "In production the Shainee app delivers the launch token to your page. The sandbox has no app, so you ask for one here, for the nonce your server issued. Only the sandbox has this route. The token lasts 60 seconds and works once. Choose a `scenario` to get a token your server must refuse.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SandboxTokenRequest"
              },
              "example": {
                "nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0",
                "user": "sita",
                "scenario": "ok"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A token for the fake user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxToken"
                },
                "example": {
                  "token": "eyJhbGciOiJFZERTQSIs...",
                  "expires_in": 60,
                  "user": "Sita Gurung",
                  "scenario": "ok"
                }
              }
            }
          },
          "400": {
            "description": "The nonce is malformed, or the user, client or scenario is not one of the allowed values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "detail": "nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only.",
                  "fields": {
                    "nonce": [
                      "nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only."
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/partner/vendors": {
      "post": {
        "operationId": "registerVendor",
        "summary": "Register or update a vendor",
        "description": "A vendor is a seller whose share of a payment is held in escrow and later paid to their CityWallet. Register every vendor once before you create payments for them; registering the same `ref` again updates the name or wallet. Shainee checks with CityWallet that the wallet exists. Vendors belong to the mode (test or live) they were registered in: after the platform goes live, register every vendor again so its real wallet is checked.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VendorRequest"
              },
              "example": {
                "ref": "ilam-tea",
                "name": "Ilam Tea House",
                "wallet_mobile": "9800000101"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The vendor is registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vendor"
                },
                "example": {
                  "ref": "ilam-tea",
                  "name": "Ilam Tea House",
                  "wallet_mobile": "980*****01",
                  "wallet_name": "Wallet 0101"
                }
              }
            }
          },
          "400": {
            "description": "The vendor was refused. See the payment error codes below.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "invalid_mobile": {
                    "summary": "Not a Nepali mobile number",
                    "value": {
                      "error": "invalid_mobile",
                      "detail": "Enter a 10-digit Nepali mobile number."
                    }
                  },
                  "wallet_not_found": {
                    "summary": "No CityWallet account for this number",
                    "value": {
                      "error": "wallet_not_found",
                      "detail": "No CityWallet account for this mobile number."
                    }
                  },
                  "invalid_request": {
                    "summary": "A field is missing or malformed",
                    "value": {
                      "error": "invalid_request",
                      "detail": "This field is required.",
                      "fields": {
                        "order_ref": [
                          "This field is required."
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          },
          "502": {
            "description": "CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "provider_unreachable": {
                    "summary": "CityWallet could not be reached",
                    "value": {
                      "error": "provider_unreachable",
                      "detail": "CityWallet could not be reached."
                    }
                  },
                  "provider_not_configured": {
                    "summary": "Payments are switched off on the server",
                    "value": {
                      "error": "provider_not_configured",
                      "detail": "Payments are not available right now."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/partner/payments": {
      "post": {
        "operationId": "createPayment",
        "summary": "Create a payment",
        "description": "Asks Shainee to take a payment from one customer, split between your vendors. Send an `Idempotency-Key` header (8 to 64 letters, digits or `. _ : -`) unique per order, for example your order id: sending the same key again returns the same payment with status 200 instead of creating a second one. Then give the payment `id` to your page, which sends `{\"type\": \"pay\", \"payment_id\": ...}` to Shainee. An unpaid payment expires after 30 minutes.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{8,64}$"
            },
            "description": "Unique per order. Retries with the same key are safe."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentRequest"
              },
              "example": {
                "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                "amount": "930.00",
                "description": "Himal Mart order HM-71FAEB1E (2 items)",
                "order_ref": "HM-71FAEB1E",
                "splits": [
                  {
                    "vendor": "ilam-tea",
                    "amount": "650.00"
                  },
                  {
                    "vendor": "himal-salt",
                    "amount": "280.00"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A payment with this Idempotency-Key already exists; it is returned unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
                  "status": "created",
                  "escrow": "none",
                  "amount": "930.00",
                  "currency": "NPR",
                  "description": "Himal Mart order HM-71FAEB1E (2 items)",
                  "order_ref": "HM-71FAEB1E",
                  "app": {
                    "client_id": "app_qJLnjtVDAz23B4yj",
                    "name": "Himal Mart",
                    "icon": "🛍️"
                  },
                  "splits": [
                    {
                      "vendor": "ilam-tea",
                      "vendor_name": "Ilam Tea House",
                      "amount": "650.00",
                      "status": "pending"
                    },
                    {
                      "vendor": "himal-salt",
                      "vendor_name": "Himalayan Salt Co.",
                      "amount": "280.00",
                      "status": "pending"
                    }
                  ],
                  "created_at": "2026-10-19T08:23:32.672048+00:00",
                  "expires_at": "2026-10-19T08:53:32.672003+00:00",
                  "paid_at": null,
                  "livemode": true,
                  "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                  "idempotency_key": "order-HM-71FAEB1E"
                }
              }
            }
          },
          "201": {
            "description": "The payment was created and is waiting for the customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
                  "status": "created",
                  "escrow": "none",
                  "amount": "930.00",
                  "currency": "NPR",
                  "description": "Himal Mart order HM-71FAEB1E (2 items)",
                  "order_ref": "HM-71FAEB1E",
                  "app": {
                    "client_id": "app_qJLnjtVDAz23B4yj",
                    "name": "Himal Mart",
                    "icon": "🛍️"
                  },
                  "splits": [
                    {
                      "vendor": "ilam-tea",
                      "vendor_name": "Ilam Tea House",
                      "amount": "650.00",
                      "status": "pending"
                    },
                    {
                      "vendor": "himal-salt",
                      "vendor_name": "Himalayan Salt Co.",
                      "amount": "280.00",
                      "status": "pending"
                    }
                  ],
                  "created_at": "2026-10-19T08:23:32.672048+00:00",
                  "expires_at": "2026-10-19T08:53:32.672003+00:00",
                  "paid_at": null,
                  "livemode": true,
                  "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                  "idempotency_key": "order-HM-71FAEB1E"
                }
              }
            }
          },
          "400": {
            "description": "The payment was refused. See the payment error codes below.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "invalid_splits": {
                    "summary": "The splits do not add up",
                    "value": {
                      "error": "invalid_splits",
                      "detail": "The split amounts must add up to amount."
                    }
                  },
                  "unknown_vendor": {
                    "summary": "A vendor is not registered",
                    "value": {
                      "error": "unknown_vendor",
                      "detail": "Register these vendors first: ghost."
                    }
                  },
                  "invalid_request": {
                    "summary": "A field is missing or malformed",
                    "value": {
                      "error": "invalid_request",
                      "detail": "This field is required.",
                      "fields": {
                        "order_ref": [
                          "This field is required."
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "409": {
            "description": "This Idempotency-Key was already used for a different amount or order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "example": {
                  "error": "idempotency_conflict",
                  "detail": "This idempotency key was used for a different payment."
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          }
        }
      }
    },
    "/api/partner/payments/{payment_id}": {
      "get": {
        "operationId": "getPayment",
        "summary": "Check a payment",
        "description": "The real state of a payment. Call it after your page reports `payment_result` or a webhook arrives, and before you fulfil an order: never trust the browser for this. Check that `order_ref` and `sub` are the ones you expect, then read `status` and `escrow`.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "parameters": [
          {
            "name": "payment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The payment id from the create call."
          }
        ],
        "responses": {
          "200": {
            "description": "The payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
                  "status": "paid",
                  "escrow": "held",
                  "amount": "930.00",
                  "currency": "NPR",
                  "description": "Himal Mart order HM-71FAEB1E (2 items)",
                  "order_ref": "HM-71FAEB1E",
                  "app": {
                    "client_id": "app_qJLnjtVDAz23B4yj",
                    "name": "Himal Mart",
                    "icon": "🛍️"
                  },
                  "splits": [
                    {
                      "vendor": "ilam-tea",
                      "vendor_name": "Ilam Tea House",
                      "amount": "650.00",
                      "status": "held"
                    },
                    {
                      "vendor": "himal-salt",
                      "vendor_name": "Himalayan Salt Co.",
                      "amount": "280.00",
                      "status": "held"
                    }
                  ],
                  "created_at": "2026-10-19T08:23:32.672048+00:00",
                  "expires_at": "2026-10-19T08:53:32.672003+00:00",
                  "paid_at": "2026-10-19T08:25:10.681496+00:00",
                  "livemode": true,
                  "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                  "idempotency_key": "order-HM-71FAEB1E"
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "404": {
            "description": "No payment with this id belongs to your app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "example": {
                  "error": "not_found",
                  "detail": "Payment not found."
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          }
        }
      }
    },
    "/api/partner/payments/{payment_id}/release": {
      "post": {
        "operationId": "releasePayment",
        "summary": "Release held money to the vendors",
        "description": "Pays each held split to its vendor's CityWallet: do it when the customer has what they paid for. Send `{}` for every held split, or `{\"vendor\": \"ilam-tea\"}` for one. A split is paid out only once. If CityWallet refuses, the money stays held and you can call again.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "parameters": [
          {
            "name": "payment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The payment id from the create call."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EscrowRequest"
              },
              "example": {
                "vendor": "ilam-tea"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done. The payment with its new escrow state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
                  "status": "paid",
                  "escrow": "partially_held",
                  "amount": "930.00",
                  "currency": "NPR",
                  "description": "Himal Mart order HM-71FAEB1E (2 items)",
                  "order_ref": "HM-71FAEB1E",
                  "app": {
                    "client_id": "app_qJLnjtVDAz23B4yj",
                    "name": "Himal Mart",
                    "icon": "🛍️"
                  },
                  "splits": [
                    {
                      "vendor": "ilam-tea",
                      "vendor_name": "Ilam Tea House",
                      "amount": "650.00",
                      "status": "released"
                    },
                    {
                      "vendor": "himal-salt",
                      "vendor_name": "Himalayan Salt Co.",
                      "amount": "280.00",
                      "status": "held"
                    }
                  ],
                  "created_at": "2026-10-19T08:23:32.672048+00:00",
                  "expires_at": "2026-10-19T08:53:32.672003+00:00",
                  "paid_at": "2026-10-19T08:25:10.681496+00:00",
                  "livemode": true,
                  "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                  "idempotency_key": "order-HM-71FAEB1E"
                }
              }
            }
          },
          "400": {
            "description": "A field is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "A field is missing or malformed",
                    "value": {
                      "error": "invalid_request",
                      "detail": "This field is required.",
                      "fields": {
                        "order_ref": [
                          "This field is required."
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "404": {
            "description": "No such payment, or it has no split for that vendor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "not_found": {
                    "summary": "No payment with this id belongs to your app",
                    "value": {
                      "error": "not_found",
                      "detail": "Payment not found."
                    }
                  },
                  "unknown_vendor": {
                    "summary": "The payment has no split for this vendor",
                    "value": {
                      "error": "unknown_vendor",
                      "detail": "This payment has no split for that vendor."
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "There is nothing to move, or the payment belongs to the other mode (test or live).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "not_paid": {
                    "summary": "The customer has not paid",
                    "value": {
                      "error": "not_paid",
                      "detail": "Only a paid payment has money in escrow."
                    }
                  },
                  "nothing_held": {
                    "summary": "Already paid out or refunded",
                    "value": {
                      "error": "nothing_held",
                      "detail": "Nothing is held in escrow for this payment."
                    }
                  },
                  "no_vendor_wallet": {
                    "summary": "Test mode: the vendor has no CityWallet, so its money stays held",
                    "value": {
                      "error": "no_vendor_wallet",
                      "detail": "Ilam Tea House has no CityWallet yet, so its money stays held with Shainee."
                    }
                  },
                  "mode_mismatch": {
                    "summary": "The payment was made in the other mode",
                    "value": {
                      "error": "mode_mismatch",
                      "detail": "This payment was made in test mode and cannot be handled in live mode."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          },
          "502": {
            "description": "CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "provider_unreachable": {
                    "summary": "CityWallet could not be reached",
                    "value": {
                      "error": "provider_unreachable",
                      "detail": "CityWallet could not be reached."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/partner/payments/{payment_id}/refund": {
      "post": {
        "operationId": "refundPayment",
        "summary": "Refund held money to the customer (not available yet)",
        "description": "Will send each held split back to the customer. Not available yet: CityPay Checkout does not tell Shainee which wallet paid, so this route answers `409 refund_unavailable` and the money stays held. Ask the Shainee team to settle a cancelled order until refunds are available.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "parameters": [
          {
            "name": "payment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The payment id from the create call."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EscrowRequest"
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done (once refunds are available). The payment with its new escrow state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                },
                "example": {
                  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
                  "status": "paid",
                  "escrow": "refunded",
                  "amount": "930.00",
                  "currency": "NPR",
                  "description": "Himal Mart order HM-71FAEB1E (2 items)",
                  "order_ref": "HM-71FAEB1E",
                  "app": {
                    "client_id": "app_qJLnjtVDAz23B4yj",
                    "name": "Himal Mart",
                    "icon": "🛍️"
                  },
                  "splits": [
                    {
                      "vendor": "ilam-tea",
                      "vendor_name": "Ilam Tea House",
                      "amount": "650.00",
                      "status": "refunded"
                    },
                    {
                      "vendor": "himal-salt",
                      "vendor_name": "Himalayan Salt Co.",
                      "amount": "280.00",
                      "status": "refunded"
                    }
                  ],
                  "created_at": "2026-10-19T08:23:32.672048+00:00",
                  "expires_at": "2026-10-19T08:53:32.672003+00:00",
                  "paid_at": "2026-10-19T08:25:10.681496+00:00",
                  "livemode": true,
                  "sub": "yyYFeygPWSKpCJE7wJNKOP01Dto8F3V5Arwstmi-M20",
                  "idempotency_key": "order-HM-71FAEB1E"
                }
              }
            }
          },
          "400": {
            "description": "A field is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "A field is missing or malformed",
                    "value": {
                      "error": "invalid_request",
                      "detail": "This field is required.",
                      "fields": {
                        "order_ref": [
                          "This field is required."
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Wrong client id or client secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_client"
                }
              }
            }
          },
          "403": {
            "description": "Your app has been suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "app_suspended"
                }
              }
            }
          },
          "404": {
            "description": "No such payment, or it has no split for that vendor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "not_found": {
                    "summary": "No payment with this id belongs to your app",
                    "value": {
                      "error": "not_found",
                      "detail": "Payment not found."
                    }
                  },
                  "unknown_vendor": {
                    "summary": "The payment has no split for this vendor",
                    "value": {
                      "error": "unknown_vendor",
                      "detail": "This payment has no split for that vendor."
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "There is nothing to move, refunds are not available yet, or the payment belongs to the other mode (test or live).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "not_paid": {
                    "summary": "The customer has not paid",
                    "value": {
                      "error": "not_paid",
                      "detail": "Only a paid payment has money in escrow."
                    }
                  },
                  "nothing_held": {
                    "summary": "Already paid out or refunded",
                    "value": {
                      "error": "nothing_held",
                      "detail": "Nothing is held in escrow for this payment."
                    }
                  },
                  "mode_mismatch": {
                    "summary": "The payment was made in the other mode",
                    "value": {
                      "error": "mode_mismatch",
                      "detail": "This payment was made in test mode and cannot be handled in live mode."
                    }
                  },
                  "refund_unavailable": {
                    "summary": "Refunds are not available yet",
                    "value": {
                      "error": "refund_unavailable",
                      "detail": "Refunds are not available yet: CityPay does not tell us the paying wallet."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 calls a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "rate_limited",
                  "detail": "Request was throttled. Expected available in 30 seconds.",
                  "retry_after_seconds": 31
                }
              }
            }
          },
          "502": {
            "description": "CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentError"
                },
                "examples": {
                  "provider_unreachable": {
                    "summary": "CityWallet could not be reached",
                    "value": {
                      "error": "provider_unreachable",
                      "detail": "CityWallet could not be reached."
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "clientBasic": {
        "type": "http",
        "scheme": "basic",
        "description": "HTTP Basic: your `client_id` is the user name and your `client_secret` is the password."
      }
    },
    "schemas": {
      "VerifyRequest": {
        "type": "object",
        "required": [
          "token",
          "nonce"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The launch token your page received from Shainee."
          },
          "nonce": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{16,128}$",
            "description": "The nonce your server issued for this page load."
          }
        }
      },
      "VerifyResult": {
        "type": "object",
        "required": [
          "sub",
          "scopes"
        ],
        "properties": {
          "sub": {
            "type": "string",
            "description": "The user's private id in your app only. Use it as the key for the user's data."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the user approved. Today only `profile`."
          },
          "name": {
            "type": "string",
            "description": "The user's name. Present only when `profile` was approved."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A short machine-readable code.",
            "enum": [
              "expired",
              "wrong_audience",
              "invalid_token",
              "nonce_mismatch",
              "replayed",
              "revoked",
              "invalid_request",
              "invalid_json",
              "invalid_client",
              "app_suspended",
              "rate_limited"
            ]
          },
          "detail": {
            "type": "string",
            "description": "A readable sentence. Present on most errors."
          },
          "fields": {
            "type": "object",
            "description": "For `invalid_request`: field name to a list of messages."
          },
          "retry_after_seconds": {
            "type": "integer",
            "description": "For `rate_limited`: how long to wait."
          }
        }
      },
      "Jwks": {
        "type": "object",
        "required": [
          "keys"
        ],
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "kty",
                "crv",
                "alg",
                "use",
                "kid",
                "x"
              ],
              "properties": {
                "kty": {
                  "type": "string",
                  "description": "Key type: `OKP`."
                },
                "crv": {
                  "type": "string",
                  "description": "Curve: `Ed25519`."
                },
                "alg": {
                  "type": "string",
                  "description": "Algorithm: `EdDSA`."
                },
                "use": {
                  "type": "string",
                  "description": "Key use: `sig`."
                },
                "kid": {
                  "type": "string",
                  "description": "Key id, also in each token's header."
                },
                "x": {
                  "type": "string",
                  "description": "The public key, Base64URL."
                }
              }
            }
          }
        }
      },
      "SandboxTokenRequest": {
        "type": "object",
        "required": [
          "nonce"
        ],
        "properties": {
          "nonce": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{16,128}$",
            "description": "The nonce your server issued."
          },
          "client_id": {
            "type": "string",
            "enum": [
              "sandbox_app",
              "sandbox_suspended"
            ],
            "description": "Which test app the token is for. Default `sandbox_app`."
          },
          "user": {
            "type": "string",
            "enum": [
              "sita",
              "bikash",
              "asha"
            ],
            "description": "Which fake user signs in. Default `sita`."
          },
          "scenario": {
            "type": "string",
            "enum": [
              "ok",
              "expired",
              "revoked",
              "wrong_audience",
              "invalid_token"
            ],
            "description": "What kind of token to make. Default `ok`."
          }
        }
      },
      "SandboxToken": {
        "type": "object",
        "required": [
          "token",
          "expires_in",
          "user",
          "scenario"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The launch token."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds it stays valid."
          },
          "user": {
            "type": "string",
            "description": "The fake user's name."
          },
          "scenario": {
            "type": "string",
            "description": "The scenario used."
          }
        }
      },
      "PaymentError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A short machine-readable code. The general codes of the Error schema can also appear.",
            "enum": [
              "invalid_mobile",
              "wallet_not_found",
              "provider_rejected",
              "provider_unreachable",
              "idempotency_conflict",
              "invalid_amount",
              "amount_too_large",
              "invalid_splits",
              "unknown_vendor",
              "not_found",
              "not_paid",
              "nothing_held",
              "no_vendor_wallet",
              "mode_mismatch",
              "provider_not_configured",
              "refund_unavailable"
            ]
          },
          "detail": {
            "type": "string",
            "description": "A readable sentence."
          }
        }
      },
      "VendorRequest": {
        "type": "object",
        "required": [
          "ref",
          "name",
          "wallet_mobile"
        ],
        "properties": {
          "ref": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.-]{1,64}$",
            "description": "Your own id for this vendor. Registering the same ref again updates it."
          },
          "name": {
            "type": "string",
            "description": "Shown to the customer on the payment sheet. At most 150 characters."
          },
          "wallet_mobile": {
            "type": "string",
            "description": "The vendor's CityWallet number, where released money is paid: 10 digits starting with 96, 97 or 98 (a +977 prefix is accepted)."
          }
        }
      },
      "Vendor": {
        "type": "object",
        "properties": {
          "ref": {
            "type": "string",
            "description": "Your id for the vendor."
          },
          "name": {
            "type": "string",
            "description": "The vendor's name."
          },
          "wallet_mobile": {
            "type": "string",
            "description": "The wallet number, masked."
          },
          "wallet_name": {
            "type": "string",
            "description": "The account name CityWallet reported, masked."
          }
        }
      },
      "Split": {
        "type": "object",
        "required": [
          "vendor",
          "amount"
        ],
        "properties": {
          "vendor": {
            "type": "string",
            "description": "A registered vendor ref."
          },
          "amount": {
            "type": "string",
            "description": "This vendor's share in rupees, two decimals."
          }
        }
      },
      "PaymentRequest": {
        "type": "object",
        "required": [
          "sub",
          "amount",
          "description",
          "order_ref",
          "splits"
        ],
        "properties": {
          "sub": {
            "type": "string",
            "description": "The customer's id in your app, from the verify call. Only this customer can pay."
          },
          "amount": {
            "type": "string",
            "description": "The total in rupees as a string with two decimals, more than zero and at most the per-payment limit."
          },
          "currency": {
            "type": "string",
            "enum": [
              "NPR"
            ],
            "description": "Optional; always NPR."
          },
          "description": {
            "type": "string",
            "description": "Shown on the payment sheet and in the customer's history. At most 200 characters."
          },
          "order_ref": {
            "type": "string",
            "description": "Your order id. At most 64 characters."
          },
          "splits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Split"
            },
            "description": "One entry per vendor, 1 to 20 entries. The amounts must add up to amount."
          },
          "idempotency_key": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{8,64}$",
            "description": "Only if you cannot send the Idempotency-Key header: the same value, in the body."
          }
        }
      },
      "PaymentSplit": {
        "type": "object",
        "properties": {
          "vendor": {
            "type": "string",
            "description": "The vendor ref."
          },
          "vendor_name": {
            "type": "string",
            "description": "The vendor's name."
          },
          "amount": {
            "type": "string",
            "description": "The vendor's share."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "held",
              "releasing",
              "released",
              "refunding",
              "refunded",
              "void"
            ],
            "description": "Where this share is."
          }
        }
      },
      "Payment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The payment id your page sends in the pay message."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "awaiting_payment",
              "processing",
              "paid",
              "failed",
              "cancelled",
              "expired"
            ],
            "description": "Whether the customer has paid. `awaiting_payment`: on CityPay's payment page. `processing`: CityPay is still confirming."
          },
          "escrow": {
            "type": "string",
            "enum": [
              "none",
              "held",
              "partially_held",
              "processing",
              "released",
              "refunded",
              "settled"
            ],
            "description": "Where the paid money is."
          },
          "amount": {
            "type": "string",
            "description": "The total in rupees."
          },
          "currency": {
            "type": "string",
            "description": "Always NPR."
          },
          "description": {
            "type": "string",
            "description": "As you sent it."
          },
          "order_ref": {
            "type": "string",
            "description": "As you sent it."
          },
          "app": {
            "type": "object",
            "description": "Your app: client_id, name and icon."
          },
          "splits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentSplit"
            },
            "description": "Each vendor's share."
          },
          "created_at": {
            "type": "string",
            "description": "ISO 8601."
          },
          "expires_at": {
            "type": "string",
            "description": "An unpaid payment can no longer be paid after this time."
          },
          "paid_at": {
            "type": "string",
            "description": "ISO 8601, or null until paid."
          },
          "livemode": {
            "type": "boolean",
            "description": "true for a real payment, false for a test-mode payment where no real money moved. A payment can only be handled in the mode it was made in."
          },
          "sub": {
            "type": "string",
            "description": "The customer, as you sent it."
          },
          "idempotency_key": {
            "type": "string",
            "description": "As you sent it."
          }
        }
      },
      "EscrowRequest": {
        "type": "object",
        "properties": {
          "vendor": {
            "type": "string",
            "description": "Optional: act only on this vendor's split. Leave it out to act on every held split."
          }
        }
      }
    }
  }
}
