Hosted Checkout
Server-to-server API for hosted checkout. Amounts are integers in minor units (paisa for NPR, cents for USD). amount is tax-inclusive. JSON fields are camelCase. Machine-readable: /openapi.json, /llms.txt.
How it works?
- Customer is ready to pay on your site.
- Your backend creates an order via
POST /api/ordersand receives acheckoutUrl. - You redirect the customer to that checkout URL.
- They pick a gateway and pay on the hosted page.
- They return to your success/failure URL with
?id=&status=(UX only;idis our order UUID). - Your backend calls
POST /api/orders/{id}/verify(or handles a signed webhook, then still verifies). - Fulfill only when
fulfilled: true.
Getting Started
No SDK required - any backend that can make HTTPS POST requests works. You need a merchant account and an API key from Awecode.
Merchant configuration
Awecode sets these on your merchant (not via this API). API keys and the webhook signing secret are minted on the admin merchant page (key shown once as ps_…; secret is rotatable).
| Field | Required | Notes |
|---|---|---|
| Success / failure redirect URLs | Fallbacks when an order omits per-order redirects. If every create sends successRedirectUrl / failureRedirectUrl, these are unused for that order - any absolute URL works as placeholder. | |
| Success / failure webhook URLs | no | payment_success → success URL; payment_failed → failure URL; receipt_synced → success webhook URL (not the redirect). Same URL for both is fine; branch on event. |
| Voucher enabled + apply URL | no | Hosted checkout voucher field. When on, voucherApplyUrl is required. Same HMAC secret as webhooks. |
| Allow cash tender via API | no | Unlocks method: "cash" on create. Off → those calls 403. |
API Endpoints
| Environment | Base URL |
|---|---|
| Production | https://pay.awecode.com |
| Staging | https://ps-stg.awecode.workers.dev |
Staging API keys and gateway sandbox credentials are issued by Awecode on the staging admin merchant (same form as production).
| Method | Path | Purpose |
|---|---|---|
POST | /api/orders | Create checkout order |
GET | /api/orders/{id} | Read order status |
POST | /api/orders/{id}/verify | Verify before fulfillment |
POST | /api/orders/{id}/cancel | Cancel open order |
POST | /api/orders/{id}/cash | Cash tender on an Open order |
POST | /api/orders/{id}/return | Cash sales return (credit note) |
POST | /api/orders/{id}/exchange | CN + INV + optional net settle |
Create order
Every payment starts as a server-side create. On success you get a checkoutUrl - redirect the customer there.
POST /api/ordersSample request
{
"amount": 150000,
"tax": 0,
"shipping": 0,
"currency": "NPR",
"orderId": "1000123",
"orderNumber": "1000123",
"reference": "1000123",
"message": "Order #1000123",
"products": [
{
"name": "Seto Dharti",
"quantity": 1,
"amount": 80000,
"description": "Ebook"
}
],
"data": { "yourOrderPk": 12345 },
"tags": ["vip", "color:red"]
}JSON payload details
| Field | Type | Required | Notes |
|---|---|---|---|
amount | integer | yes | Minor units. Tax- and shipping-inclusive. |
currency | string | no | ISO 4217. Default NPR. |
tax / shipping | integer | no | Default 0. Must satisfy tax + shipping <= amount. |
orderId | string | no | Your order id. Echoed on verify / webhooks (browser return uses our id). |
reference | string | no | Unique per merchant. If omitted: {orderNumber||orderId||"order"}::{ourUuid}. Reuse → 409. |
message | string | no | Shown on checkout. |
products | array | no* | Line items on checkout. When Awecount sync is on: required, each line needs code (see Awecount / IRD). |
discount | integer | no | Order-level commercial discount (tax-inclusive). Payable amount is already net. Awecount header Amount. |
customer | object | no | Buyer billing for IRD (name, email, phone). Not in data. |
remarks | string | no | Awecount voucher remarks (not checkout message). Not in data. |
data | object | no | Pass-through. Echoed on verify / webhooks. Not shown on checkout. |
method | "cash" | no | Omit for hosted checkout. "cash" = already collected; order Success + optional IRD (merchant must have cash enabled). |
tags | string[] | no | Dashboard labels. Immutable after create. |
successRedirectUrl / failureRedirectUrl | uri | no | Per-order override of merchant defaults. If set, merchant-level redirects are unused for that order. |
expiresAt | datetime | no | Optional. Must be in the future. Null = no expiry. Past → checkout refuses; status may move to Expired. |
Success response
{
"id": "019f8c2a-7b1e-7000-8000-abcdef012345",
"reference": "1000123",
"checkoutUrl": "https://pay.awecode.com/xY9kQm2..."
} Redirect the customer to checkoutUrl. Store id for verify / cancel.
Get order
GET /api/orders/{id} Returns current status, latest transaction pointers, and your data / tags. Also includes refunds and refundedTotal from order_refunds (the sale row is never mutated on return). For fulfillment, call verify - do not fulfill from get alone.
Cancel order
POST /api/orders/{id}/cancelOnly while Open. Already paid orders cannot be canceled (409).
Sales return (credit note)
Cash return against a Success parent sale (partial or full). Stores a row in order_refunds — no new order/transaction — and posts an Awecount credit note when sync is on. Parent sale stays Success.
POST /api/orders/{id}/return{id} is the original payment-service order id (the paid sale).
{
"method": "cash",
"amount": 28000,
"tax": 3212,
"discount": 0,
"reference": "019f-adjustment-uuid",
"remarks": "Return Veg Biryani",
"products": [
{
"name": "Veg Biryani",
"quantity": 1,
"amount": 28000,
"code": "FOOD-biryani-01",
"category": { "name": "Food" },
"tax": { "rate": 13 }
}
]
}| Field | Required | Notes |
|---|---|---|
method | yes | Must be "cash" (cashEnabled required). |
amount / products | yes | Returned lines only. Product codes required when Awecount sync is on. |
reference | yes | Idempotency key (unique per merchant). Same ref + different payload → 409. |
Response is the refund face + receipt (credit-note bill when synced), fulfilled: true. Cancel does not substitute for return.
Order exchange (CN + INV + net settle)
Fanpark-style paid-order amend: optional cash return (CN) + optional cash sale (INV at full add amount) + optional net digital/cash settle. Settle never posts a third Awecount sales voucher.
POST /api/orders/{id}/exchange{
"reference": "019f-adjustment-uuid",
"remarks": "Fanpark order #48291365 adjustment",
"return": {
"amount": 28000,
"tax": 3212,
"products": [
{ "name": "Veg Biryani", "quantity": 1, "amount": 28000, "code": "FOOD-biryani-01" }
]
},
"sale": {
"amount": 30000,
"tax": 3442,
"products": [
{ "name": "Samosa (2 pc)", "quantity": 3, "amount": 30000, "code": "FOOD-samosa-01" }
]
},
"settle": {
"method": "digital",
"successRedirectUrl": "https://pos.example/adjust?paid=1",
"failureRedirectUrl": "https://pos.example/adjust?status=Failed"
}
}settleAmount = max(0, sale.amount − return.amount) (server-computed). Example above → QR for Rs 20 while IRD still gets CN 280 + INV 300.
| Leg | Awecount (sync on) | Money |
|---|---|---|
| Return | credit-note/ | Till handback (cash) |
| Sale | sales-voucher/ (cash) | Fiscal INV only — not the QR amount |
| Settle | none (exchange_settle) | Gateway/till collects net only |
{
"id": "019f-exchange-uuid",
"reference": "019f-adjustment-uuid",
"parentOrderId": "019f-parent-sale-uuid",
"status": "Open",
"settleAmount": 2000,
"settleOrderId": "019f-settle-uuid",
"checkoutUrl": "https://pay.awecode.com/…",
"fulfilled": false,
"return": { "id": "019f-refund-uuid", "amount": 28000, "receipt": { "billNo": "CN-…" } },
"sale": { "id": "019f-sale-uuid", "amount": 30000, "receipt": { "billNo": "INV-…" } }
}fulfilled: truewhen settleAmount is 0, settle was cash, or settle order isSuccess.- Digital settle: poll/verify
POST /api/orders/{settleOrderId}/verifylike any digital order;payment_successwebhook fires on settle Success. - Same
referenceresumes incomplete/Failed legs; different payload →409. Works with Awecount sync off (local bookkeeping + settle only).
Browser return
After payment, the customer is redirected to the order's success/failure URL (per-order override, else merchant default). Params are appended with URLSearchParams (existing ?… becomes &…, never a second ?):
?id={our order UUID}&status={Success|Failed|Canceled|Expired} Use id for POST /api/orders/{id}/verify. Order statuses: Open, Success, Failed, Canceled, Expired (American spelling).
Verify payment
Required before fulfillment. Re-checks the gateway when needed and returns whether you may fulfill.
POST /api/orders/{id}/verifyPaid response
{
"id": "019f8c2a-7b1e-7000-8000-abcdef012345",
"orderId": "1000123",
"reference": "1000123",
"amount": 150000,
"currency": "NPR",
"status": "Success",
"verified": true,
"fulfilled": true,
"verificationResult": "Success",
"transactionId": "019f8c2b-1111-7000-8000-abcdef012345",
"gateway": "esewa",
"gatewayReference": "000ABC"
}Webhooks
Optional. Ask Awecode to set your success / failure webhook URLs and signing secret. Same URL for both is fine; branch on event. Still call verify before fulfillment.
payment_success→ success webhook URLpayment_failed→ failure webhook URLreceipt_synced→ success webhook URL (not the redirect), when Awecount sync is on
Headers
Content-Type: application/json
X-Event-ID: <uuid, stable across retries>
X-Webhook-Signature: sha256=<hex HMAC of raw body> HMAC key = your webhook signing secret as UTF-8 (do not base64-decode). Respond 2xx. Use X-Event-ID as an idempotency key across retries.
Retries
Up to 6 attempts. Backoff after failure: 1m, 5m, 15m, 1h, 4h, then Exhausted.
Body
{
"event": "payment_success",
"id": "019f8c2a-7b1e-7000-8000-abcdef012345",
"orderId": "1000123",
"transactionId": "019f8c2b-1111-7000-8000-abcdef012345",
"amount": 150000,
"currency": "NPR",
"status": "Success",
"gateway": "esewa",
"gatewayReference": "000ABC",
"data": { "yourOrderPk": 12345 },
"tags": ["vip", "color:red"]
}id is our order UUID (use for verify). orderId is your create-time id if set, otherwise our UUID. amount is minor units.
Vouchers
Optional. Ask Awecode to enable vouchers and set your voucherApplyUrl. Hosted checkout then shows a voucher field. You own the codes - we call your URL on apply / remove.
Only one voucher at a time. To change code, remove first, then apply again. Stacking discounts is not supported.
Apply request (us → you)
POST {your voucherApplyUrl}
Content-Type: application/json
X-Webhook-Signature: sha256=<hex HMAC of raw body>{
"action": "apply",
"orderId": "019f8c2a-7b1e-7000-8000-abcdef012345",
"merchantOrderId": "1000123",
"reference": "1000123",
"checkoutToken": "xY9kQm2...",
"code": "SAVE20",
"amount": 150000,
"originalAmount": 150000,
"currency": "NPR",
"tax": 0,
"shipping": 0,
"data": { "yourOrderPk": 12345 }
}You can ignore fields you do not need. Typical use:
code- voucher to applymerchantOrderId/data- find the order in your systemoriginalAmount- create-time total (baseline for % off / min-order rules)amount- current payable (same asoriginalAmounton apply, since only one voucher is allowed)
orderId, reference, checkoutToken, tax, shipping, currency are context if you want them.
Success response (you → us)
{
"ok": true,
"amount": 120000,
"code": "SAVE20"
} Return the new payable in amount (minor units). It must be <= originalAmount. We store that as the order payable and keep the create-time total so remove can restore it.
Failure response
{
"ok": false,
"error": "invalid_code",
"detail": "Optional short message shown to the customer"
} Common error values: invalid_code, expired, already_used, not_applicable.
Remove
Same URL with "action": "remove". Reply { "ok": true }. We restore the create-time amount locally.
100% off
If apply returns amount: 0, checkout completes without a gateway: order becomes Success, we send a payment_success webhook (gateway: "voucher"), and the customer is redirected to your success URL. Still call verify and deliver only when fulfilled: true.
Awecount / IRD
Optional. When Awecode enables Awecount sync for your merchant, a successful payment creates an Awecount sales voucher (IRD receipt). You never call Awecount yourself — send the fields below on POST /api/orders; we POST the partner sales-voucher after the order is Success.
Payment modes (Awecount)
Prefer optional gatewayModes ids when company mode names differ. Otherwise we send payment_mode_obj: { name } (exact spelling below). Seed only creates Cash.
| PS gateway | Fallback Awecount name |
|---|---|
cash, voucher | Cash |
esewa | eSewa |
khalti | Khalti |
fonepay_qr, fonepay_bank | Fonepay |
connectips | ConnectIPS |
paypal | PayPal |
stripe | Stripe |
Sample create (cash + IRD)
Payable amount is already net of discounts (here Rs 550). Gross lines 300+500 − line 50 − order 200 → amount: 55000.
POST /api/orders{
"amount": 55000,
"tax": 7200,
"currency": "NPR",
"orderId": "10001234",
"orderNumber": "10001234",
"message": "Table 4",
"discount": 20000,
"remarks": "Order #10001234",
"customer": {
"name": "Ram Bahadur",
"email": "ram@example.com",
"phone": "9800000000"
},
"products": [
{
"name": "Veg Biryani",
"quantity": 1,
"amount": 30000,
"discount": 5000,
"code": "FOOD-biryani-01",
"category": { "name": "Food" },
"tax": { "rate": 13 },
"description": "NPR 300 each"
},
{
"name": "General ticket",
"quantity": 1,
"amount": 50000,
"code": "TIX-general-01",
"category": { "name": "Tickets" },
"tax": { "rate": 13 },
"description": "Entry"
}
],
"data": { "yourOrderPk": 12345 },
"tags": ["fanpark"],
"method": "cash"
}Order-level fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | yes | Payable NPR paisa (tax-inclusive), already net of discounts. |
tax | integer | no | Default 0. > 0 → VAT scheme id; else taxless. |
currency | string | no | Default NPR. Prefer NPR when sync is on. |
orderId / orderNumber | string | no | Your ids; echoed on verify / webhooks. Browser return uses our id. |
message | string | no | Checkout display only (not Awecount remarks). |
discount | integer | no | Order-level discount (tax-inclusive paisa). Awecount header Amount is VAT-peeled when tax > 0. Default 0. |
remarks | string | no | Awecount voucher remarks (max 500). Separate from message. |
products | array | yes* | Required when sync is on. Non-empty. See product fields. |
customer | object | no | Buyer billing for IRD. See customer fields. |
data | object | no | Pass-through only (your PKs). Echoed on verify / webhooks. |
tags | string[] | no | Dashboard labels. Not sent to Awecount. |
method | "cash" | no | "cash" → Success + await receipt (checkoutUrl: null). Omit for digital. Needs cashEnabled. |
Customer object
| Field | Type | Required | Description |
|---|---|---|---|
customer.name | string | no | → customer_name. Max 120. Omit → "Customer". |
customer.email | string | no | → Awecount email. Max 254. |
customer.phone | string | no | → Awecount contact_no. Max 32. |
customer.taxId | string | no | Nepal PAN / VAT (9 digits). → Partner party_obj.tax_identification_number for VAT-claimable bills. |
Product object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Checkout title; Awecount item name on create. |
quantity | integer | yes | Positive. Voucher row quantity. |
amount | integer | yes | Line gross total (paisa, tax-inclusive), not unit price. Awecount row rate scales by (order.amount − order.tax) / order.amount when tax > 0, else amount / quantity / 100. |
code | string | yes* | Stable item code (1–64). Required when sync on. Lookup by code; create if missing. Same SKU → same code forever (e.g. FOOD-<id>). |
category | object | no | { "name": "Food", "hsCode"? } → Partner category_obj + category create (optional hs_code). Also sets item HS when present. Omit → uncategorized. Do not send legacy categoryId integers. |
tax | object | no | { "rate": 13 } → Partner row tax_scheme_obj.rate. Omit → no row tax scheme. |
description | string | no | Checkout subtitle; voucher row description (else name). |
discount | integer | no | Line discount (tax-inclusive paisa), ≤ line amount. Awecount row Amount is VAT-peeled when tax > 0. |
Cash create response
With method: "cash" and sync enabled, create returns the verify-shaped face immediately (no checkoutUrl). When Awecount succeeds in time, receipt.status is synced and invoice is filled.
{
"id": "019f8c2a-7b1e-7000-8000-abcdef012345",
"orderId": "10001234",
"orderNumber": "10001234",
"reference": "10001234",
"amount": 55000,
"currency": "NPR",
"status": "Success",
"data": { "yourOrderPk": 12345 },
"tags": ["fanpark", "tender:cash"],
"customer": {
"name": "Ram Bahadur",
"email": "ram@example.com",
"phone": "9800000000"
},
"verified": true,
"fulfilled": true,
"verificationResult": "Success",
"transactionId": "019f8c2b-1111-7000-8000-abcdef012345",
"gateway": "cash",
"gatewayReference": null,
"checkoutUrl": null,
"receipt": {
"billNo": "SV-1042",
"status": "synced",
"syncedAt": "2026-10-14T06:00:00.000Z",
"invoice": {
"id": 1042,
"billNo": "SV-1042",
"fiscalYear": "2082/83",
"date": "2026-10-14",
"customerName": "Ram Bahadur",
"rows": [
{ "id": 1, "description": "NPR 300 each" },
{ "id": 2, "description": "Entry" }
]
}
}
}Response / receipt fields
| Field | Description |
|---|---|
fulfilled | Fulfill only when true (same rule as verify). |
gateway | cash for cash tender; otherwise the digital gateway used. |
checkoutUrl | null on cash create; otherwise redirect the customer there. |
customer | Echo of what you sent on create (name / email / phone). |
receipt | Present when sync is enabled. null / omitted if not claimed yet. |
receipt.billNo | Awecount voucher number (e.g. SV-1042) once synced. |
receipt.status | pending | synced | failed. |
receipt.syncedAt | ISO timestamp when status became synced. |
receipt.invoice | Print subset from Awecount: id, billNo, fiscalYear, date, customerName, lines (name / hsCode / qty / rate), legacy rows with flattened hs_code. Stored after sync; later verify does not re-POST. |
Digital flow
- Create without
method→ getcheckoutUrl. - Customer pays → you
POST /api/orders/{id}/verify(or handle webhook, then still verify). - After Success we await Awecount sync before the checkout redirect (and before your
POST …/verifyreturns). You can also wait forreceipt_synced.
receipt_synced webhook
Delivered to your success webhook URL after the voucher is created (in addition to payment_success). Same HMAC headers as other webhooks.
{
"event": "receipt_synced",
"orderId": "10001234",
"transactionId": "019f8c2b-1111-7000-8000-abcdef012345",
"amount": 55000,
"currency": "NPR",
"status": "Success",
"gateway": "cash",
"gatewayReference": null,
"data": { "yourOrderPk": 12345 },
"tags": ["fanpark", "tender:cash"],
"receipt": {
"billNo": "SV-1042",
"status": "synced",
"syncedAt": "2026-10-14T06:00:00.000Z",
"invoice": {
"id": 1042,
"billNo": "SV-1042",
"fiscalYear": "2082/83",
"date": "2026-10-14",
"customerName": "Ram Bahadur",
"rows": []
}
}
}Create rejects (sync on)
- Empty
productsor any line missingcode→400. data.remarkspresent →400(use top-levelremarks).data.kindpresent →400(reserved for exchange legs; PS sets it).method: "cash"but cash not enabled →403.
Credit note & exchange
Beyond create sales vouchers:
- Sales return → Partner
credit-note/linked to the parent INV. - Order exchange → CN + INV at full line amounts; settle checkout never posts a third voucher (
exchange_settle). Sale leg still posts INV (exchange_saleis an internal stamp only).
Repo detail: docs/integrations/awecount.md and docs/order-exchange.md.
Errors
{
"statusCode": 409,
"statusMessage": "Order reference already exists for this merchant",
"message": "Order reference already exists for this merchant"
}Quick curl
BASE=https://pay.awecode.com
KEY=ps_...
curl -sS -X POST "$BASE/api/orders" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"amount":150000,"currency":"NPR","orderId":"1000123"}'
curl -sS "$BASE/api/orders/$ORDER_ID" -H "Authorization: Bearer $KEY"
curl -sS -X POST "$BASE/api/orders/$ORDER_ID/verify" -H "Authorization: Bearer $KEY"Security
Payment integration bugs become money bugs. Treat fulfillment as a security boundary: only your backend, only after server-to-server verify, only when fulfilled: true.
Checklist
Tick each item as you implement. Saved in this browser only.