API reference
The routes a partner server calls on the identity provider. The same information is available as an OpenAPI 3.1 file, which you can load into Postman, Insomnia or any code generator.
This page is generated from that file, so the two always agree.
Servers#
| Address | Use |
|---|---|
https://superapp.ideapreneurnepal.com |
Hosted service |
https://superapp.ideapreneurnepal.com/sandbox |
Sandbox: the same verify and keys routes with fake users |
Credentials for your app are issued when it is registered. To test before that, use the sandbox (client id sandbox_app, secret sandbox_secret).
POST /api/launch/verify#
Verify a launch token
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | The launch token your page received from Shainee. |
nonce |
string | yes | The nonce your server issued for this page load. Pattern ^[A-Za-z0-9_-]{16,128}$. |
Example:
{
"token": "eyJhbGciOiJFZERTQSIsImtp...Cjw40TDnu8Dg",
"nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0"
}
Responses#
| Status | Meaning |
|---|---|
| 200 | The token is valid. name is present only if the user approved the profile scope. |
| 400 | The request or the token was refused. See the error codes below. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 429 | More than 600 calls a minute from one address. |
200 response:
{
"sub": "6wVFC8ZW-uir4EjfnDOwK95teeinCnN1NrjjLgXvNgQ",
"scopes": [
"profile"
],
"name": "Demo Player"
}
400 response, replayed: The token was already used
{
"error": "replayed"
}
400 response, nonce_mismatch: The nonce is not the one the token was issued for
{
"error": "nonce_mismatch"
}
400 response, invalid_token: Not a valid token
{
"error": "invalid_token"
}
400 response, invalid_request: A field is missing or malformed
{
"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 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
GET /.well-known/jwks.json#
Get the public signing keys
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.
Authentication: none.
Responses#
| Status | Meaning |
|---|---|
| 200 | The key set. Tokens are signed with EdDSA (Ed25519). |
200 response:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "oGSRh-ZVy5_mJPgS",
"x": "sMZWbe0Cf1cFc3PEcU_oJk-Gl9PenmDSj1l-JvNTmwE"
}
]
}
POST /sandbox/api/launch/token#
Sandbox only: get a launch token
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.
Authentication: none.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
nonce |
string | yes | The nonce your server issued. Pattern ^[A-Za-z0-9_-]{16,128}$. |
client_id |
string | no | Which test app the token is for. Default sandbox_app. One of: sandbox_app, sandbox_suspended. |
user |
string | no | Which fake user signs in. Default sita. One of: sita, bikash, asha. |
scenario |
string | no | What kind of token to make. Default ok. One of: ok, expired, revoked, wrong_audience, invalid_token. |
Example:
{
"nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0",
"user": "sita",
"scenario": "ok"
}
Responses#
| Status | Meaning |
|---|---|
| 200 | A token for the fake user. |
| 400 | The nonce is malformed, or the user, client or scenario is not one of the allowed values. |
200 response:
{
"token": "eyJhbGciOiJFZERTQSIs...",
"expires_in": 60,
"user": "Sita Gurung",
"scenario": "ok"
}
400 response:
{
"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."
]
}
}
POST /api/partner/vendors#
Register or update a vendor
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
ref |
string | yes | Your own id for this vendor. Registering the same ref again updates it. Pattern ^[A-Za-z0-9_.-]{1,64}$. |
name |
string | yes | Shown to the customer on the payment sheet. At most 150 characters. |
wallet_mobile |
string | yes | The vendor's CityWallet number, where released money is paid: 10 digits starting with 96, 97 or 98 (a +977 prefix is accepted). |
Example:
{
"ref": "ilam-tea",
"name": "Ilam Tea House",
"wallet_mobile": "9800000101"
}
Responses#
| Status | Meaning |
|---|---|
| 200 | The vendor is registered. |
| 400 | The vendor was refused. See the payment error codes below. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 429 | More than 600 calls a minute from one address. |
| 502 | CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later. |
200 response:
{
"ref": "ilam-tea",
"name": "Ilam Tea House",
"wallet_mobile": "980*****01",
"wallet_name": "Wallet 0101"
}
400 response, invalid_mobile: Not a Nepali mobile number
{
"error": "invalid_mobile",
"detail": "Enter a 10-digit Nepali mobile number."
}
400 response, wallet_not_found: No CityWallet account for this number
{
"error": "wallet_not_found",
"detail": "No CityWallet account for this mobile number."
}
400 response, invalid_request: A field is missing or malformed
{
"error": "invalid_request",
"detail": "This field is required.",
"fields": {
"order_ref": [
"This field is required."
]
}
}
401 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
502 response, provider_unreachable: CityWallet could not be reached
{
"error": "provider_unreachable",
"detail": "CityWallet could not be reached."
}
502 response, provider_not_configured: Payments are switched off on the server
{
"error": "provider_not_configured",
"detail": "Payments are not available right now."
}
POST /api/partner/payments#
Create a payment
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Parameters#
| Name | In | Required | Description |
|---|---|---|---|
Idempotency-Key |
header | yes | Unique per order. Retries with the same key are safe. |
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
sub |
string | yes | The customer's id in your app, from the verify call. Only this customer can pay. |
amount |
string | yes | The total in rupees as a string with two decimals, more than zero and at most the per-payment limit. |
currency |
string | no | Optional; always NPR. One of: NPR. |
description |
string | yes | Shown on the payment sheet and in the customer's history. At most 200 characters. |
order_ref |
string | yes | Your order id. At most 64 characters. |
splits |
array of object | yes | One entry per vendor, 1 to 20 entries. The amounts must add up to amount. |
idempotency_key |
string | no | Only if you cannot send the Idempotency-Key header: the same value, in the body. Pattern ^[A-Za-z0-9_.:-]{8,64}$. |
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#
| Status | Meaning |
|---|---|
| 200 | A payment with this Idempotency-Key already exists; it is returned unchanged. |
| 201 | The payment was created and is waiting for the customer. |
| 400 | The payment was refused. See the payment error codes below. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 409 | This Idempotency-Key was already used for a different amount or order. |
| 429 | More than 600 calls a minute from one address. |
200 response:
{
"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 response:
{
"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 response, invalid_splits: The splits do not add up
{
"error": "invalid_splits",
"detail": "The split amounts must add up to amount."
}
400 response, unknown_vendor: A vendor is not registered
{
"error": "unknown_vendor",
"detail": "Register these vendors first: ghost."
}
400 response, invalid_request: A field is missing or malformed
{
"error": "invalid_request",
"detail": "This field is required.",
"fields": {
"order_ref": [
"This field is required."
]
}
}
401 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
409 response:
{
"error": "idempotency_conflict",
"detail": "This idempotency key was used for a different payment."
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
GET /api/partner/payments/{payment_id}#
Check a payment
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Parameters#
| Name | In | Required | Description |
|---|---|---|---|
payment_id |
path | yes | The payment id from the create call. |
Responses#
| Status | Meaning |
|---|---|
| 200 | The payment. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 404 | No payment with this id belongs to your app. |
| 429 | More than 600 calls a minute from one address. |
200 response:
{
"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 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
404 response:
{
"error": "not_found",
"detail": "Payment not found."
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
POST /api/partner/payments/{payment_id}/release#
Release held money to the vendors
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Parameters#
| Name | In | Required | Description |
|---|---|---|---|
payment_id |
path | yes | The payment id from the create call. |
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string | no | Optional: act only on this vendor's split. Leave it out to act on every held split. |
Example:
{
"vendor": "ilam-tea"
}
Responses#
| Status | Meaning |
|---|---|
| 200 | Done. The payment with its new escrow state. |
| 400 | A field is malformed. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 404 | No such payment, or it has no split for that vendor. |
| 409 | There is nothing to move, or the payment belongs to the other mode (test or live). |
| 429 | More than 600 calls a minute from one address. |
| 502 | CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later. |
200 response:
{
"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 response, invalid_request: A field is missing or malformed
{
"error": "invalid_request",
"detail": "This field is required.",
"fields": {
"order_ref": [
"This field is required."
]
}
}
401 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
404 response, not_found: No payment with this id belongs to your app
{
"error": "not_found",
"detail": "Payment not found."
}
404 response, unknown_vendor: The payment has no split for this vendor
{
"error": "unknown_vendor",
"detail": "This payment has no split for that vendor."
}
409 response, not_paid: The customer has not paid
{
"error": "not_paid",
"detail": "Only a paid payment has money in escrow."
}
409 response, nothing_held: Already paid out or refunded
{
"error": "nothing_held",
"detail": "Nothing is held in escrow for this payment."
}
409 response, no_vendor_wallet: Test mode: the vendor has no CityWallet, so its money stays held
{
"error": "no_vendor_wallet",
"detail": "Ilam Tea House has no CityWallet yet, so its money stays held with Shainee."
}
409 response, mode_mismatch: The payment was made in the other mode
{
"error": "mode_mismatch",
"detail": "This payment was made in test mode and cannot be handled in live mode."
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
502 response, provider_unreachable: CityWallet could not be reached
{
"error": "provider_unreachable",
"detail": "CityWallet could not be reached."
}
POST /api/partner/payments/{payment_id}/refund#
Refund held money to the customer (not available yet)
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.
Authentication: HTTP Basic: your client_id is the user name and your client_secret is the password.
Parameters#
| Name | In | Required | Description |
|---|---|---|---|
payment_id |
path | yes | The payment id from the create call. |
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
vendor |
string | no | Optional: act only on this vendor's split. Leave it out to act on every held split. |
Example:
{}
Responses#
| Status | Meaning |
|---|---|
| 200 | Done (once refunds are available). The payment with its new escrow state. |
| 400 | A field is malformed. |
| 401 | Wrong client id or client secret. |
| 403 | Your app has been suspended. |
| 404 | No such payment, or it has no split for that vendor. |
| 409 | There is nothing to move, refunds are not available yet, or the payment belongs to the other mode (test or live). |
| 429 | More than 600 calls a minute from one address. |
| 502 | CityWallet refused the call, could not be reached, or payments are switched off on the server. Nothing changed; try again later. |
200 response:
{
"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 response, invalid_request: A field is missing or malformed
{
"error": "invalid_request",
"detail": "This field is required.",
"fields": {
"order_ref": [
"This field is required."
]
}
}
401 response:
{
"error": "invalid_client"
}
403 response:
{
"error": "app_suspended"
}
404 response, not_found: No payment with this id belongs to your app
{
"error": "not_found",
"detail": "Payment not found."
}
404 response, unknown_vendor: The payment has no split for this vendor
{
"error": "unknown_vendor",
"detail": "This payment has no split for that vendor."
}
409 response, not_paid: The customer has not paid
{
"error": "not_paid",
"detail": "Only a paid payment has money in escrow."
}
409 response, nothing_held: Already paid out or refunded
{
"error": "nothing_held",
"detail": "Nothing is held in escrow for this payment."
}
409 response, mode_mismatch: The payment was made in the other mode
{
"error": "mode_mismatch",
"detail": "This payment was made in test mode and cannot be handled in live mode."
}
409 response, refund_unavailable: Refunds are not available yet
{
"error": "refund_unavailable",
"detail": "Refunds are not available yet: CityPay does not tell us the paying wallet."
}
429 response:
{
"error": "rate_limited",
"detail": "Request was throttled. Expected available in 30 seconds.",
"retry_after_seconds": 31
}
502 response, provider_unreachable: CityWallet could not be reached
{
"error": "provider_unreachable",
"detail": "CityWallet could not be reached."
}
Error codes#
| Code | Status | Meaning |
|---|---|---|
expired |
400 | The token is older than 60 seconds. |
wrong_audience |
400 | The token was issued for a different app. |
invalid_token |
400 | Bad signature or a malformed token. |
nonce_mismatch |
400 | The nonce is not the one the token was issued for. The token stays valid for the right nonce. |
replayed |
400 | The token was already used. |
revoked |
400 | The user revoked access, was deactivated, or the app is no longer allowed. |
invalid_request |
400 | A field is missing or malformed; fields says which. |
invalid_json |
400 | The request body is not valid JSON. |
invalid_client |
401 | Wrong client id or client secret. |
app_suspended |
403 | Your app has been suspended. |
rate_limited |
429 | Too many calls. retry_after_seconds says how long to wait. |
Payment error codes#
The payment routes can also answer with these. Every one comes with a readable detail.
| Code | Status | Meaning |
|---|---|---|
invalid_mobile |
400 | The wallet number is not a 10-digit Nepali mobile number. |
wallet_not_found |
400 | CityWallet has no account for this number. |
provider_rejected |
502 | CityPay or CityWallet refused the call. Nothing changed; held money stays held. |
provider_unreachable |
502 | CityPay or CityWallet could not be reached. Nothing changed; try again later. |
idempotency_conflict |
409 | The Idempotency-Key was already used for a different amount or order. |
invalid_amount |
400 | The amount must be more than zero. |
amount_too_large |
400 | The amount is above the per-payment limit. |
invalid_splits |
400 | The splits are empty, repeat a vendor, or do not add up to the amount. |
unknown_vendor |
400 or 404 | A vendor is not registered, or was registered in the other mode (create: register it again), or the payment has no split for it (release, refund). |
not_found |
404 | No payment with this id belongs to your app. |
not_paid |
409 | The customer has not paid, so nothing is held. |
nothing_held |
409 | Every split was already paid out or refunded. |
no_vendor_wallet |
409 | Test mode only: the vendor has no CityWallet, so its money stays held with Shainee. |
mode_mismatch |
409 | The payment was made in test mode and the platform is now live, or the other way round. It cannot be released or refunded in this mode. |
provider_not_configured |
502 | Payments are switched off on the server for now. Nothing changed; try later. |
refund_unavailable |
409 | Refunds are not available yet: CityPay Checkout does not say which wallet paid. The money stays held. |
Routes you implement yourself#
Your own server provides GET /api/config, POST /api/nonce, POST /api/session and your own protected routes. They are specified in the Server-side contract, not here, because they live on your server.