Shainee logoShainee for Developers v1.0

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.