Shainee logoShainee for Developers v1.0

Taking payments

Customers pay you inside Shainee with Pay with Shainee. You never handle wallet details, card numbers or passwords: your server asks for a payment, Shainee shows its own payment sheet, the customer pays on CityPay Checkout (CityPay's own payment page), and your server checks the result. Sellers are paid out through CityWallet. Both are licensed Nepali payment services.

Every payment is held in escrow. The customer's money goes to Shainee first and waits there, split by seller, until you tell us to release it to the sellers (for example when the order is delivered) or refund it to the customer. Customers trust a new shop more when they know nobody is paid until they get what they ordered.

Your server asks for a payment, Shainee takes it, your server checks it Ten messages between the Shainee API, the Shainee app, your page and your server, in order. In step 6 the customer pays on CityPay's page. Shainee APIShainee appYour pageYour server 1 checkout2 POST /api/partner/payments with your client secret3 payment id4 payment id5 pay + payment id6 sheet, CityPay page7 payment_result8 check my order9 GET /api/partner/payments/{id}: paid, held in escrow10 later: release to sellers
A payment. Steps 2, 3, 9 and 10 are server to server; step 7 only tells your page the sheet closed. A webhook can also tell your server, instead of step 8.
  1. The customer checks out on your page. Your server saves the order.
  2. Your server creates a payment for it on Shainee, with the amount, the customer's sub and each seller's share.
  3. Shainee answers with a payment id.
  4. Your server hands the id to your page.
  5. Your page asks Shainee to take the payment.
  6. Shainee shows its own payment sheet with the amount and sellers from step 2, never from your page. When the customer taps Pay, Shainee opens CityPay's payment page, where they log in to CityPay and pay. Afterwards Shainee asks CityPay itself whether the payment went through.
  7. Shainee tells your page the sheet closed, and (if you have a webhook) tells your server.
  8. Your page asks your server to check the order.
  9. Your server asks Shainee what really happened. Only this answer decides whether the order is paid.
  10. Later, the money is released to the sellers, by your server or by Shainee staff.

Before your first payment: register your sellers#

A payment is split between sellers (we call them vendors). Register each one once, with the CityWallet number their money is paid out to. Registering again with the same ref updates the vendor; it is safe to do on every start of your server.

POST /api/partner/vendors with your client credentials (HTTP Basic, the same as the verify call):

{"ref": "ilam-tea", "name": "Ilam Tea House", "wallet_mobile": "9800000101"}

ref is your own id for the seller. Shainee checks the wallet exists before saying yes:

{"ref": "ilam-tea", "name": "Ilam Tea House", "wallet_mobile": "980*****01", "wallet_name": "Wallet 0101"}

A shop that sells its own goods registers a single vendor: itself.

1. Create the payment (your server)#

POST /api/partner/payments with your client credentials and an Idempotency-Key header:

{
  "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"}
  ]
}
Field Rule
sub The customer, as your verify call told you. Only this customer can pay it
amount Rupees as a string with two decimals. More than zero, at most the per-payment limit
description What the customer sees on the payment sheet and in their history
order_ref Your order id, so you can match the payment later
splits One entry per vendor; the amounts must add up to amount
Idempotency-Key header 8 to 64 letters, digits or . _ : -, unique per order. Use your order id

Work out the amounts on your server from your own prices. Never take a price from the page.

The answer is 201 with the new payment, or 200 with the same payment if you send the same Idempotency-Key again, so a retry after a timeout never charges twice. Reusing a key for a different amount answers 409 idempotency_conflict.

An unpaid payment expires after 30 minutes, unless the customer is still on CityPay's page or CityPay is still confirming it: a payment never expires while it may still be paid. Create a new one (with a new key) if the customer comes back later.

2. Ask Shainee to take it (your page)#

Your page sends one message and waits for one answer, over the same channel as sign-in (see the Browser-side contract):

Direction Message
Page to Shainee {"type": "pay", "payment_id": "pay_qmo8uHYBAMq-ShlCKzdALg"}
Shainee to page {"type": "payment_result", "payment_id": "pay_qmo8uHYBAMq-ShlCKzdALg", "status": "paid"}

status in the answer is paid, processing (CityPay is still confirming), cancelled (the customer closed the sheet) or failed. It only tells your page the sheet has closed. Never mark an order paid because of this message: anything running in a browser can be faked. Ask your server instead.

The customer can close the sheet and pay later. Until the payment expires, you can send the same pay message again, for example from a "Pay now" button on the order. If an attempt on CityPay's page fails, the customer can simply try again from the sheet.

3. Check the result (your server)#

GET /api/partner/payments/{payment_id} with your client credentials. Check that order_ref and sub are the ones you expect (and, on your production server, that livemode is true), then read status and escrow:

{
  "id": "pay_qmo8uHYBAMq-ShlCKzdALg",
  "status": "paid",
  "escrow": "held",
  "amount": "930.00",
  "order_ref": "HM-71FAEB1E",
  "livemode": true,
  "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"}
  ]
}

The full answer has a few more fields; they are listed in the API reference.

status Meaning What you do
created Waiting for the customer Show "Pay now"
awaiting_payment The customer is on CityPay's payment page Wait
processing CityPay is still confirming the payment Wait. Shainee keeps asking CityPay, and a webhook tells you how it ends
paid The money is with Shainee Fulfil the order. Read escrow
failed The last attempt at CityPay did not go through. No money moved Show "Pay now" again; the customer may retry
cancelled, expired No money moved Let the customer try again with a new payment
escrow Meaning
none Not paid yet, so nothing is held
held Every split is held
partially_held Some splits are paid out or refunded, others are still held
processing A payout is under way at CityWallet
released Every split was paid to its vendor
refunded Every split went back to the customer
settled Every split was paid out or refunded, some each way

4. Release the money (your server)#

When the customer has what they paid for, release the money:

POST /api/partner/payments/{payment_id}/release

The body is empty ({}) to release every held split, or {"vendor": "ilam-tea"} for one vendor, for example when only part of an order arrives. The answer is the payment with its new escrow value. A split can be paid out only once: a second call answers 409 nothing_held. Each vendor's share goes to their CityWallet.

If CityWallet does not accept a payout, the answer is an error and the money stays held, so you can try again. Shainee staff can also release a payment from their side, for example once delivery is confirmed or to settle a dispute.

Refunds are not available yet#

POST /api/partner/payments/{payment_id}/refund exists, but it answers 409 refund_unavailable for now and the money stays held: CityPay Checkout does not tell Shainee which wallet the customer paid from, so there is nowhere to send it back yet. If an order is cancelled after payment, do not release it, and contact the Shainee team, who will settle it with the customer.

What the customer sees#

Tell your customers on your own order pages too, in the same words: "Paid, held by Shainee until delivery".

Webhooks (optional)#

Instead of waiting for your page, your server can be told. Give the Shainee team an HTTPS address on your server; you get a webhook signing secret once, and from then on a POST for each of these events:

Event When
payment.paid The money is held in escrow
payment.processing CityPay is still confirming
payment.failed An attempt at CityPay did not go through
payment.cancelled, payment.expired The payment will not be paid
payment.released A vendor's share was paid out
payment.refunded A share went back to the customer

The body is the same payment object as GET /api/partner/payments/{payment_id}:

{
  "id": "evt_8f3k2m9q",
  "type": "payment.paid",
  "created_at": "2026-10-19T08:25:10+00:00",
  "livemode": true,
  "data": {"payment": {"id": "pay_qmo8uHYBAMq-ShlCKzdALg", "status": "paid", "escrow": "held", "order_ref": "HM-71FAEB1E"}}
}

(The real payment has every field.) Each request carries SuperPreneur-Event, SuperPreneur-Event-Id and a signature header:

SuperPreneur-Signature: t=<unix seconds>,v1=<hex>

To check it, compute HMAC-SHA256 with your webhook secret over the text <t>.<raw body> (the timestamp, a dot, and the body exactly as received) and compare it with v1 in constant time. Refuse requests whose t is more than 5 minutes old.

Answer any 2xx quickly. Anything else is retried after 1, 5, 30, 120, 360 and 1440 minutes, with the same event id, so ignore an id you have already handled. A webhook only says something changed: before you ship, confirm with GET /api/partner/payments/{payment_id} as in step 3.

Test mode and live mode#

The platform runs in one of two modes, and every payment records which one in livemode:

Mode livemode Money
Test false Nothing real moves. CityPay and CityWallet run with test accounts
Live true Real money through CityPay and CityWallet

Payments and vendors belong to the mode they were made in:

On your production server, never ship real goods for a payment with livemode: false. To show a "test mode" banner, your page can ask Shainee's GET /api/config (not your own route of the same name; no credentials needed):

{"mode": "test", "test_mode": true}

If payments are switched off on the server, the payment routes answer 502 provider_not_configured. Nothing changes; try again later.

Testing payments#

Payments are not in the hosted sandbox yet. Ask the Shainee team for an app on a server in test mode:

Test at least: a paid order, a cancelled sheet followed by "Pay now", a failed attempt followed by a successful one, a retry of the create call with the same Idempotency-Key, a release, a partial release, a second release of the same payment (refused), a refund (refused for now with refund_unavailable), and, if you use them, your webhook's signature check.

Rules#

  1. Create payments only on your server, with your client secret. The page never sees the secret.
  2. Prices and splits come from your database, never from the page.
  3. Only GET /api/partner/payments/{id} decides whether an order is paid.
  4. Use your order id as the Idempotency-Key, so retries are safe.
  5. Release only when the customer has what they paid for. If they will not get it, do not release; contact the Shainee team until refunds are available.