Katana Pay — Gateway Integration Kit

Hosted-checkout UPI pay-ins for any platform · Base URL https://katanapay.tech · API v1
What you'll do: your server creates a signed order → you redirect the customer to the Katana Pay hosted payment page → the customer pays by UPI → Katana POSTs a signed status callback to your server and redirects the customer back to your return_url. You verify every signature with the Key + Salt issued to your merchant account.

Contents

1. Credentials (Key + Salt) 2. Endpoints 3. Create an order 4. Sign the request 5. Redirect to the payment page 6. Receive the status callback 7. Status enquiry (and long-poll) 8. Status & response codes 9. Traceability — following one payment end to end 10. Sample code (PHP / Node / Python) 11. Test mode 12. Go-live checklist

1. Credentials (Key + Salt)

Every merchant account gets its own dedicated Key and Salt, issued from the Katana dashboard (Integration → Generate, or ask your Katana account manager). They are not shared between merchants, and every order and callback is bound to the pair that signed it.

Test and live pairs

PairKey starts withOrders it creates
Testmk_test_Test orders: they pay a sandbox UPI ID, never move real money and never appear in live totals, statements or settlement (§11).
LiveKT_live_Real payments. Live Keys issued before 26 Oct 2026 start mk_live_ and keep working.

The Key you sign with decides the mode; there is no mode field to send. Keys issued before test mode (mk_ followed by hex) are live and keep working unchanged.

FieldMeaning
KeyPublic-ish identifier (mk_test_… / KT_live_…) sent with every request. Identifies the merchant and the mode.
SaltSecret. Shown only once at issue. Used to sign your requests and to verify our callbacks. Never expose it in client-side code.
SchemeSigning algorithm for your account: HMAC_SHA256. Every new pair is issued with it; pairs issued earlier with the legacy SHA-512 format (§4) keep working until regenerated.
Keep each Salt safe. If lost, regenerate that pair — which invalidates only that pair (the other mode is untouched), so update your server immediately. One active Key per mode.

2. Endpoints

PurposeMethodURL
Create order (server-to-server)POSThttps://katanapay.tech/api/v1/katana-pay/order
Hosted payment page (browser)GEThttps://katanapay.tech/pay/{order_id}
Status enquiryGEThttps://katanapay.tech/api/pay-status/{order_id}
Customer's UTR for a P2P order (§7)POSThttps://katanapay.tech/api/v1/p2p/utr
Your Key + Salt is the only credential on these endpoints. Treat Salt disclosure as a full compromise of your integration: regenerate immediately and reconcile every order created in the interim.

Pay-in flows: P2P and Intent

Katana takes pay-ins on two flows, and your account is set up for P2P, Intent or Both:

FlowHow the customer paysOrder APIStatus
P2PA UPI link or QR that pays the receiving UPI ID directly. Confirmed by the bank credit.POST /api/v1/p2p/orderGET /api/v1/p2p/order/{id}
IntentThe payment is issued and confirmed by the payment gateway.POST /api/v1/intent/orderGET /api/v1/intent/order/{id}

All three order APIs take the same request, the same signature and return the same response. The general /api/v1/katana-pay/order uses the flow selected for your account, so an existing integration keeps working unchanged. Call a flow's own API when you want that flow by name. The response says which flow the order took in "flow" (P2P or INTENT).

A flow your account is not set up for is refused with HTTP 409 and a code: FLOW_NOT_ENABLED (your account is on the other flow), FLOW_NOT_SELECTED (no flow has been selected yet) or FLOW_NOT_READY (the flow is selected but its setup is not complete). No order is created.

The flow status APIs accept the order id or the flow reference (P2P-000000123 / INT-000000123) and add flow and that reference to the usual status fields.

3. Create an order

POST /api/v1/katana-pay/order with a JSON body (form-encoded is also accepted). Idempotent on txnid — re-sending the same txnid returns the existing order rather than creating a second one, so a retry after a network timeout is always safe. A txnid is unique per mode: the same reference sent with a test Key and a live Key creates two separate orders.

Request fields

FieldReqDescription
keyyesYour Key.
txnidyesYour unique order reference (≤ 60 chars). This is your handle on the payment forever — see §9.
amountyesAmount in rupees, e.g. "100" or "100.50".
hashyesSignature over the order (see §4).
productinfonoOrder description (part of the signature).
firstnamenoCustomer name (part of the legacy SHA-512 signature).
emailnoCustomer email (part of the signature).
phonenoCustomer phone.
return_urlnoBrowser redirect target after payment. Overrides your saved default.
notify_urlnoServer-to-server status-callback target. Overrides your saved webhook URL.
currencynoDefaults to INR.
modenoQR or INTENT.
return_url and notify_url must be http(s) URLs. Other schemes are rejected.

Response (HTTP 201 new / 200 reused)

{
  "verified": true,
  "merchant": "UK-108",                               // the account that holds the order
  "signed_by": "UK-108",                              // the account whose Key signed it
  "livemode": true,                                   // false = created with a test Key
  "order": { "id": "8d20c0b6-…", "order_id": "ORDER-1001", "amount": 100, "status": "PENDING", … },
  "checkout": "H2H",                                  // H2H: UPI link + QR below; REDIRECT: send to pay_url
  "pay_url": "https://katanapay.tech/pay/8d20c0b6-…",   // redirect the customer here
  "deeplinks": { "upi": "upi://pay?…", … },
  "upi_intent": "upi://pay?…",
  "qr_payload": "upi://pay?…"
}
Several accounts? With the banker switch on (MID switch page in your portal), you can sign with any of your accounts’ Keys and Katana picks the account that takes each order. merchant then says which account took it; signed_by is the Key you signed with. The callback still comes to your usual address, signed with the Salt you signed the order with, and retrying with the same Key and txnid finds the same order.

Take pay_url and send the browser there. If you are building your own payment screen instead, use qr_payload (render as a QR) or upi_intent (an app deeplink).

checkout says whether you can: H2H (host-to-host) means the UPI link and QR are in the answer; REDIRECT means they are null and the customer pays on the hosted page. It depends on the payment account behind your channel; ask Katana for host-to-host if your integration needs it. A test order always carries the sandbox UPI link, and adds live_checkout: what your live orders will get (H2H, REDIRECT, or null while no payment account is set up).

Accounts paid by app buttons (pay_apps)

On some P2P accounts a payment can only be made from the Paytm or PhonePe app, through a link made for that order. Their live orders are still H2H, but carry pay_apps instead of a UPI link: upi_intent, qr_payload and deeplinks.upi are null.

{
  …,
  "checkout": "H2H",
  "pay_url": "https://katanapay.tech/pay/8d20c0b6-…",
  "upi_intent": null, "qr_payload": null,
  "deeplinks": { "upi": null, "paytm": "paytmmp://…", "phonepe": "phonepe://…" },
  "pay_apps": [
    { "app": "PAYTM",   "name": "Paytm",   "link": "paytmmp://…", "android_link": "intent://…;package=net.one97.paytm;end" },
    { "app": "PHONEPE", "name": "PhonePe", "link": "phonepe://…", "android_link": "intent://…;package=com.phonepe.app;end" }
  ],
  "gateway_url": "https://katanapay.tech/pay/8d20c0b6-…/go"   // when present: the processor's own payment page
}

If the answer also has gateway_url, you may send the customer there instead of showing the buttons (a top-level redirect in the same tab): it opens the processor's own payment page for this order. Use one or the other for an order, not both.

To take the payment on your own checkout page:

  1. Show one button per entry in pay_apps, in that order, and nothing else: no QR, no "any UPI app", no Google Pay, no UPI ID to copy. A payment made any other way reaches the account without the order's reference and is not confirmed.
  2. On tap, navigate the page itself (not a new tab or popup) to android_link on Android and to link on iOS. Use each link exactly as given; do not rebuild it from its parts.
  3. The customer must be on the phone that has the app. On a computer, send them to pay_url (or show it as a QR to open on the phone).
  4. The customer has 15 minutes. Wait for the callback, or ask the status API, as for any order. Returning to your page from the app proves nothing.
Test orders always carry the test UPI link and no pay_apps; live_checkout is H2H for these accounts. Make your page handle both answers: pay_apps when present, else qr_payload / upi_intent.

Merchants paid on a gateway's payment page

Some merchants collect through a gateway that takes the payment on its own secure page. Their orders have no UPI link: checkout is REDIRECT, deeplinks, upi_intent and qr_payload are null, and one field is added:

{
  …,
  "checkout": "REDIRECT",
  "pay_url": "https://katanapay.tech/pay/8d20c0b6-…",   // still the page to send the customer to
  "deeplinks": null, "upi_intent": null, "qr_payload": null,
  "gateway_url": "https://katanapay.tech/pay/8d20c0b6-…/go"  // straight to the gateway's page, if you'd rather skip ours
}

Send the customer to pay_url as usual — it shows the order and hands over to the gateway — or straight to gateway_url. The result reaches you the same way (callback, status enquiry). The gateway's own limits apply, e.g. a minimum amount per payment; an order outside them is refused with 502 and the gateway's reason.

ErrorMeaning
401 invalid keyUnknown Key.
401 signature mismatchHash didn't match — check your signing string.
403Account blocked or suspended (code MERCHANT_BLOCKED / MERCHANT_SUSPENDED), or set up for payouts only (code PAYIN_NOT_ENABLED).
400 invalid amountAmount missing / not positive.
409 TXNID_IN_USEBanker switch only: the account the order was offered to already has this txnid from another Key, and no other account could take it. Use a new txnid.
409This account can't take live payments yet (no payment gateway or receiving UPI ID is set up). Contact your Katana account manager.
502The payment gateway refused the order — the message gives its reason. No order was created; fix and retry with the same txnid. With code GATEWAY_CREDENTIALS the account's payment credentials need updating on Katana's side: send us the reference in the message.
422The order is outside your account's limits. No order was created. The body carries code, field, limit and actual — see below.
429 RATE_LIMITEDToo many orders in one second. Send it again after the Retry-After header (1 second).

Limits. A live order outside your account's limits is refused with 422:

{ "error": "amount is above the maximum of ₹5,000", "code": "AMOUNT_ABOVE_MAX", "field": "amount", "limit": 5000, "actual": 5000.01 }
codeMeaning
AMOUNT_BELOW_MINThe amount is under the minimum for one order: your account's own, or the minimum of the payment account behind it (some take ₹500 or ₹1,000 and more per payment). limit is the minimum that applies.
AMOUNT_ABOVE_MAXThe amount is over your account's maximum for one order.
UPI_LIMIT_EXCEEDEDThe amount is over what one UPI payment can carry (₹1,00,000 unless your account has a higher maximum).
DAILY_LIMIT_EXCEEDEDToday's orders plus this one would pass your daily limit; actual is the total it would reach. The day is the calendar day in India, and an order that fails or expires gives its amount back.

Repeating a txnid you already created is never limited: you get the original order back.

Request id. Every response carries an X-Request-Id header. Send your own (8 to 64 letters, digits, ., _, : or -) and it is echoed and kept with the order; otherwise one is generated. Quote it when you contact support.

4. Sign the request

Compute hash with the scheme configured on your account (shown next to your Key in the dashboard), then send it as the hash field. You never send the scheme name — Katana already knows which one your account uses.

HMAC_SHA256 (recommended)

message = txnid + "|" + amount + "|" + productinfo + "|" + email
hash    = HMAC_SHA256( key = KEY + SALT , message )      // lowercase hex

Legacy SHA-512 format

Only for pairs issued before HMAC_SHA256 became the only scheme; a regenerated pair is always HMAC_SHA256. Used by accounts migrated from an older checkout. Pipe-joined, with five user-defined fields and five reserved blanks before the salt:

seq  = KEY|txnid|amount|productinfo|firstname|email|||||||||||SALT   // 5 udf + 5 reserved blanks
hash = SHA512(seq)                                       // lowercase hex

5. Redirect to the payment page

Send the customer's browser to the pay_url from the response. The hosted page shows a UPI QR plus app buttons and live status (or, for a merchant paid on a gateway's page, the order and a Pay button that opens it). When the payment is confirmed it automatically redirects back to your return_url with query params:

https://your-site.com/return?order_id=ORDER-1001&status=SUCCESS&rrn=123456789012
Always re-confirm server-side (callback or status enquiry) before fulfilling — the browser redirect is informational only and can be forged by anyone who can type a URL.

6. Receive the status callback

On every terminal status Katana POSTs a JSON body to your notify_url (or your saved webhook URL). Reply HTTP 200. Delivery is queued and retried with backoff until you do, and every attempt is recorded — so a callback is never silently dropped.

Katana also checks your callback URL now and then with a made-up order (ORDER_ID integration-check, header X-Katana-Check: 1). Answer it 200, or reject it as an unknown order (404 is fine); never act on it.

Callback body

{
  "PAY_ID": "…", "ORDER_ID": "ORDER-1001", "TXN_ID": "…",
  "AMOUNT": "100", "CURRENCY_CODE": "356",
  "STATUS": "Captured", "RESPONSE_CODE": "000",
  "RRN": "123456789012", "RESPONSE_DATE_TIME": "2026-07-01T…Z",
  "HASH": "716346F5…EC21"
}

One sample for each outcome

The body has the same fields whatever happened. These are the three statuses a callback can carry, and the second callback of an order that is paid late:

// Paid
{ "ORDER_ID": "ORDER-1001", "STATUS": "Captured", "RESPONSE_CODE": "000", "RRN": "123456789012", … }

// Failed: the payment was declined. RRN is an empty string.
{ "ORDER_ID": "ORDER-1002", "STATUS": "Failed",   "RESPONSE_CODE": "004", "RRN": "", … }

// Expired: no payment arrived in time. RRN is an empty string.
{ "ORDER_ID": "ORDER-1003", "STATUS": "Expired",  "RESPONSE_CODE": "003", "RRN": "", … }

// The same order, paid after it expired: a SECOND callback, some time later
{ "ORDER_ID": "ORDER-1003", "STATUS": "Captured", "RESPONSE_CODE": "000", "RRN": "123456789013", … }

In the portal, Orders finds any order by its order id, your txnid or the bank reference and shows every callback attempt with what your server answered; Resend sends one again, and Send test event sends a sample of each status to your callback URL.

Verify the HASH (with your Salt)

  1. Take every field except HASH.
  2. Sort the keys ascending; join as KEY=value with ~ between pairs.
  3. Append your SALT directly to the end of that string.
  4. SHA256 it → hex → UPPERCASE. It must equal HASH.

Paid = STATUS is Captured and RESPONSE_CODE is 000.

Test callbacks. A callback for a test order also carries "LIVEMODE": "false" (included in the HASH like any other field) and is signed with the test Salt. Live callbacks omit the field. Verify with the Salt of the matching mode, and never fulfil a real order from a LIVEMODE=false callback.
Treat callbacks as idempotent. The same ORDER_ID may arrive more than once (a retry). Key your handler on ORDER_ID + STATUS and make a repeat of the same status a no-op.
Expired and Failed can be followed by Captured. If the customer's payment goes through after the order expired, or after an earlier attempt failed, Katana honours it: the order becomes SUCCESS and a second callback with STATUS = "Captured" is sent. Let Captured overwrite an earlier Expired or Failed. Captured is final — nothing is ever sent after it.

7. Status enquiry (and long-poll)

At any time: GET /api/pay-status/{order_id}

{ "order_id": "ORDER-1001", "amount": 100, "currency_code": "INR",
  "status": "SUCCESS", "terminal": true, "rrn": "123456789012",
  "mode": "QR", "livemode": true, "return_url": "…" }

terminal: true means the order has reached a final state and will not change again.

Long-poll: ?wait=1

Add ?wait=1 and the request is held open until the order becomes terminal (up to ~25s), instead of returning the current state immediately. This lets your own page flip to "Payment received" within about half a second of the money landing, without polling in a tight loop.

GET https://katanapay.tech/api/pay-status/8d20c0b6-…?wait=1

Use plain enquiry (no wait) for reconciliation sweeps, and wait=1 only where a person is watching a screen. Either way, enquiry is the fallback if a callback was missed — it always reflects the truth.

Pay-in report

POST /api/v1/reports/payins returns your own orders for a date range, with totals.

FieldMeaning
keyYour Key. A test Key reports test orders, a live Key live ones.
from, toCalendar days in India, YYYY-MM-DD. At most 31 days.
formatjson (default) or csv.
hashSigned like an order, over the string from|to.

The JSON carries summary (orders, paid orders, paid amount, failed, expired, pending, success rate), by_day, by_flow and orders (your txnid, the order id, amount, status, flow, utr, created_at, paid_at). status is the same word your callback carries: Captured, Failed, Expired, or Pending. A report holds up to 10,000 orders; truncated: true means the range has more, and the totals still cover all of them.

Every report carries an X-Report-Hash header, the SHA-256 of its content (the JSON repeats it as report_hash). Keep it with the file: a copy that has been edited no longer matches.

Customer's UTR for a P2P order

On P2P, the customer pays the receiving UPI ID and Katana confirms the order when that payment reaches it, usually within seconds. When it doesn't (the customer paid late, or several orders of the same amount were open), your customer has a 12-digit UTR (UPI reference number) in their UPI app. Send it here and the order is confirmed by it:

POST https://katanapay.tech/api/v1/p2p/utr
{ "key": "KT_live_…", "txnid": "ORDER-1001", "utr": "629655704289", "hash": "…" }
FieldMeaning
keyYour Key: the one the order was created with.
txnid or order_idYour order reference (txnid), or Katana's order id from the order response. Send one, not both.
utrThe 12-digit UTR from the customer's UPI app (Google Pay: "UPI transaction ID"; PhonePe: "UTR"; Paytm: "UPI Ref No").
hashHMAC-SHA256(key + salt, "<txnid or order_id>|<utr>"), hex. Legacy pairs: sha512(key|<txnid or order_id>|<utr>|salt).

A UTR alone never confirms an order. It is checked against the payment Katana actually received: same UTR, same amount, on your account, made while the order existed. So a customer cannot use someone else's UTR or a made-up one.

HTTPresultMeaning
200CONFIRMEDThe payment with this UTR was found. The order is paid and your Captured callback is on its way (also when the order had already expired).
202WAITINGThe UTR is recorded but that payment hasn't reached Katana yet. The order is confirmed, and your callback sent, the moment it does. Nothing more to send.
4xxREFUSEDWith a code below. The order is unchanged.
HTTPcodeMeaning
400INVALID_REQUESTA field is missing or wrong, or both txnid and order_id were sent.
400INVALID_UTRNot a 12-digit UTR.
401SIGNATURE_MISMATCHThe hash is wrong; hint says what is signed.
403LIVE_MODE_NOT_ACTIVATEDA live Key before live mode is activated.
404ORDER_NOT_FOUNDNo order with this reference was created with this Key (in this mode).
409ORDER_FAILEDThe order has failed and can't be paid.
409UTR_ALREADY_USEDThis UTR has already confirmed another order.
409AMOUNT_MISMATCHThe payment with this UTR is for a different amount than the order.
409PAID_BEFORE_ORDERThe payment with this UTR was made before the order was created.
409PAID_TOO_LATEThe payment was made too long after the order was created; support will check it.
422UTR_NOT_APPLICABLEThe order is confirmed by its payment processor (Intent), so no UTR is needed.
429TOO_MANY_TRIESToo many UTRs sent for one order (6). Send the payment screenshot to support.

Every answer carries txnid, order_id, utr, order_status and livemode. With a test Key, any 12-digit UTR confirms the open test P2P order, so you can see the callback without money.

// Node.js
const sig = crypto.createHmac("sha256", KEY + SALT).update(`${txnid}|${utr}`).digest("hex");
const r = await fetch("https://katanapay.tech/api/v1/p2p/utr", { method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ key: KEY, txnid, utr, hash: sig }) });

8. Status & response codes

STATUSRESPONSE_CODETerminalMeaning
Captured000yesPaid — fulfil the order.
Pending005noAwaiting payment.
Failed004yes *Declined / failed.
Expired003yes *Payment request lapsed.

* Failed and Expired end the order unless the payment goes through afterwards; a Captured callback then follows (see §6). Captured never changes.

9. Traceability — following one payment end to end

Every pay-in carries four identifiers. Store all of them against your order; each answers a different question.

IdentifierWho creates itUse it to
txnid / ORDER_IDYou, on createYour own reference. Idempotency key on create, and the key you match callbacks on.
order.id (UUID)KatanaAddress the hosted page (/pay/{id}) and the status endpoint.
PAY_IDKatanaQuote to support — it identifies the payment inside Katana.
RRN / UTRThe UPI networkThe 12-digit bank reference. This is what a bank statement shows, so it is the identifier that settles disputes.

The lifecycle

create order            → status PENDING      (you hold txnid + order.id)
customer pays by UPI    → credit confirmed
                        → status SUCCESS, RRN filled in
                        → signed callback POSTed to your notify_url  (retried until 200)
                        → customer redirected to your return_url

A payment that never completes ends Expired; one that fails ends Failed. Both are terminal and both fire a callback, so your side never has to guess. If the money arrives afterwards all the same, the order moves to SUCCESS and a Captured callback follows.

Reconciling a day

Reconciliation rule of thumb: your system should be able to answer "what happened to ORDER-1001?" from your own database alone. That is what storing the four identifiers above buys you.

10. Sample code

PHP — create order

$key="mk_xxx"; $salt="your_salt";
$o = ["txnid"=>"ORDER-1001","amount"=>"100","productinfo"=>"Order 1001","email"=>"d@e.com"];
$msg = $o["txnid"]."|".$o["amount"]."|".$o["productinfo"]."|".$o["email"];
$o["hash"] = hash_hmac("sha256", $msg, $key.$salt);
$o["key"]=$key; $o["return_url"]="https://you.com/return"; $o["notify_url"]="https://you.com/callback";
$ch=curl_init("https://katanapay.tech/api/v1/katana-pay/order");
curl_setopt_array($ch,[CURLOPT_POST=>1,CURLOPT_RETURNTRANSFER=>1,
  CURLOPT_HTTPHEADER=>["Content-Type: application/json"],CURLOPT_POSTFIELDS=>json_encode($o)]);
$res=json_decode(curl_exec($ch),true);
header("Location: ".$res["pay_url"]);   // redirect customer

Node.js — create order

import crypto from "crypto";
const key="mk_xxx", salt="your_salt";
const o={ txnid:"ORDER-1001", amount:"100", productinfo:"Order 1001", email:"d@e.com" };
const msg=[o.txnid,o.amount,o.productinfo,o.email].join("|");
o.hash=crypto.createHmac("sha256",key+salt).update(msg).digest("hex");
const r=await fetch("https://katanapay.tech/api/v1/katana-pay/order",{method:"POST",
  headers:{"Content-Type":"application/json"},
  body:JSON.stringify({...o,key,return_url:"https://you.com/return",notify_url:"https://you.com/callback"})});
const data=await r.json();   // redirect customer to data.pay_url

Python — create order

import hmac,hashlib,requests
key,salt="mk_xxx","your_salt"
o={"txnid":"ORDER-1001","amount":"100","productinfo":"Order 1001","email":"d@e.com"}
msg="|".join([o["txnid"],o["amount"],o["productinfo"],o["email"]])
o["hash"]=hmac.new((key+salt).encode(),msg.encode(),hashlib.sha256).hexdigest()
o.update(key=key, return_url="https://you.com/return", notify_url="https://you.com/callback")
data=requests.post("https://katanapay.tech/api/v1/katana-pay/order",json=o).json()
# redirect customer to data["pay_url"]

Verify a callback (Node.js)

function verify(body, salt){
  const got=body.HASH; const f={...body}; delete f.HASH;
  const str=Object.keys(f).sort().map(k=>`${k}=${f[k]==null?"":f[k]}`).join("~")+salt;
  const exp=crypto.createHash("sha256").update(str,"utf8").digest("hex").toUpperCase();
  return exp===got;   // true → trust it; then check STATUS==="Captured"
}

Verify a callback (PHP)

function verify($body,$salt){
  $got=$body["HASH"]; unset($body["HASH"]); ksort($body);
  $parts=[]; foreach($body as $k=>$v){$parts[]="$k=".($v??"");}
  $str=implode("~",$parts).$salt;
  return strtoupper(hash("sha256",$str))===$got;
}

11. Test mode

Integrate with the test pair first. Test orders behave exactly like live ones (hosted page, callbacks, status enquiry) except that they pay a sandbox UPI ID and the hosted page shows a Test payment label with Simulate success and Simulate failure buttons. Tap one and the order settles that way; your server receives the same signed callback a real payment sends. From your own code, the paise of the amount also force the outcome, so every branch can be tested without a real payer:

Send amountOrder becomesUse it to test
100.99SUCCESS after ~8 sHappy path, callback verification, fulfilment
100.13FAILEDDecline handling
100.11EXPIREDExpiry handling
100.00stays PENDING, expires after 15 minLong-poll and your own re-check
These rules apply only to test orders. A live order changes state only on a real confirmed credit, and an amount ending in .99 behaves exactly like any other. Real bank credits never confirm a test order.

In the dashboard, the Test / Live switch in the header shows test orders; an orange banner stays on while you are viewing test data. Live totals, statements and settlement never include test orders.

12. Go-live checklist

Live mode must be activated first. Live keys and live orders are locked until Katana activates live mode for your account. In the dashboard, open Integration → Activate live mode: once every item is ticked (onboarding approved, settlement UPI ID saved, webhook URL set, one successful test payment), tap Request activation. Until it is approved a live Key is refused with 403 and "code": "LIVE_MODE_NOT_ACTIVATED".

Katana Pay · integration support: contact your Katana account manager.