Hosted Checkout API
This is the complete server-to-server contract for dynamically creating a Rupay-hosted payment page.
Prerequisites
Ask the Rupay operator for:
- the API base URL;
- a test API key beginning with
rp_test_; - the HTTPS origins allowed for
success_urlandcancel_url; - a webhook signing secret returned when the endpoint is registered.
Use separate test and live keys. Store all secrets in the merchant backend.
RUPAY_API_BASE_URL=https://your-rupay-host.example
RUPAY_API_KEY=rp_test_replace_me
RUPAY_WEBHOOK_SECRET=whsec_replace_me
All API responses are JSON and include Cache-Control: no-store.
1. Create a Checkout Session
POST /v1/checkout/sessions
For customers that exist in your application, first call POST /v1/customers
with a stable external_reference_id. Repeating that request updates and
returns the same merchant-owned Rupay customer. You can then pass its returned
id as customer_id when creating checkout sessions instead of sending an
inline customer object.
Customer billing data is available server-to-server at:
GET /v1/customers/{customerId}GET /v1/customers/{customerId}/subscriptionsGET /v1/customers/{customerId}/payments
These endpoints require the customers:read scope; customer attachment uses
customers:write. A paid subscription checkout is active until the end of
its snapshotted recurring interval and becomes expired after that date.
Headers:
Authorization: Bearer rp_test_...
Content-Type: application/json
Idempotency-Key: fundyatra-payment-fyp_01J...
The idempotency key is required, must contain 8-255 printable ASCII characters without spaces, and should identify one merchant payment attempt. Retrying the same body with the same key returns the original response. Reusing a key with a different body returns a conflict.
Request:
{
"mode": "payment",
"client_reference_id": "fyp_01JABCDEF",
"currency": "npr",
"line_items": [
{
"description": "Contribution to Community Recovery Campaign",
"quantity": 1,
"amount_unit": 250000
}
],
"customer": {
"email": "supporter@example.com",
"phone": "9800000000"
},
"success_url": "https://web-production-be68e.up.railway.app/payments/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://web-production-be68e.up.railway.app/payments/cancelled",
"metadata": {
"fundyatra_payment_id": "fyp_01JABCDEF",
"payment_kind": "campaign_contribution"
}
}
Successful response (201 Created):
{
"id": "cs_example",
"object": "checkout.session",
"client_reference_id": "fyp_01JABCDEF",
"livemode": false,
"mode": "payment",
"status": "pending_payment",
"payment_status": "unpaid",
"amount_total": 250000,
"amount_display": "NPR 2,500.00",
"currency": "npr",
"payment_method_types": ["card", "esewa"],
"payment_method_type": null,
"payment_provider": null,
"url": "https://your-rupay-host.example/checkout/cs_example",
"success_url": "https://web-production-be68e.up.railway.app/payments/success?session_id=cs_example",
"cancel_url": "https://web-production-be68e.up.railway.app/payments/cancelled",
"line_items": [
{
"description": "Contribution to Community Recovery Campaign",
"quantity": 1,
"amount_unit": 250000,
"amount_subtotal": 250000,
"amount_total": 250000
}
],
"metadata": {
"fundyatra_payment_id": "fyp_01JABCDEF",
"payment_kind": "campaign_contribution"
},
"expires_at": 1787263200,
"completed_at": null,
"created_at": 1787261400,
"updated_at": 1787261400
}
Send the browser to url. Do not call the internal payment-provider endpoints
from the merchant application.
Catalog-priced subscriptions
For fixed-period subscriptions, send product_id, optional variant_id,
customer_id, client_reference_id, and the two return URLs, with
mode: "payment". Omit line_items: Rupay resolves the price and duration from
its catalog. A variable product requires an active variant. This flow uses manual
renewal, not automatic collection. The response includes a subscription object
with status and period boundaries; pending sessions do not grant access.
See the Nepse Edge integration guide for the complete
request, configuration, webhook contract, and early-renewal limitation.
Dynamic-price request fields and limits
| Field | Required | Contract |
|---|---|---|
mode | Yes | Must be payment |
client_reference_id | Recommended | Merchant payment/order ID, maximum 200 characters |
coupon_code | No | Active merchant coupon code, 3-32 characters |
currency | No | npr by default; supported values are npr and usd |
line_items | Yes | 1-50 items |
line_items[].description | Yes | 1-200 characters |
line_items[].quantity | No | Integer 1-99; defaults to 1 |
line_items[].amount_unit | Yes | Positive integer minor-unit price |
customer.email | No | Valid email, maximum 254 characters |
customer.phone | No | 7-30 characters |
success_url | Yes | HTTPS; live origin must be registered |
cancel_url | Yes | HTTPS; live origin must be registered |
expires_at | No | Unix seconds, 30 minutes to 24 hours ahead; defaults to 24 hours |
metadata | No | Up to 50 string entries; keys are 1-40 characters and values are at most 500 characters |
The session response includes amount_subtotal, amount_discount, and the
discounted amount_total. Product-restricted coupons are available on hosted
product, catalog API, and product-backed payment-link checkouts. The session total cannot
exceed 999999999 minor units. Rupay rejects unknown request fields, unsafe
return URLs, fractional amounts, and overflows.
Metadata keys may contain only letters, numbers, _, ., and -, matching
[A-Za-z0-9_.-]+. Use metadata for non-sensitive identifiers, not supporter
messages or other private data.
{CHECKOUT_SESSION_ID} is replaced wherever it occurs in a return URL.
2. Retrieve the result
GET /v1/checkout/sessions/{sessionId} requires a Bearer API key. A merchant
can retrieve only its own sessions. An open session past expires_at is marked
expired before it is returned.
After payment, the important fields are:
{
"id": "cs_example",
"client_reference_id": "fyp_01JABCDEF",
"status": "paid",
"payment_status": "paid",
"amount_total": 250000,
"currency": "npr",
"payment_method_type": "esewa",
"payment_provider": "esewa"
}
The retrieval endpoint is useful for a status page, but the signed webhook is the durable fulfillment signal.
3. Expire an unused session
POST /v1/checkout/sessions/{sessionId}/expire requires a Bearer API key. It
prevents further payment attempts. Paid sessions cannot be expired.
Status model
status | Meaning |
|---|---|
created, pending_payment | Open and payable until expiration |
paid | A provider-confirmed payment was recorded |
fulfilled | Rupay-side downstream fulfillment completed, if used |
failed | Session ended unsuccessfully |
expired | Payment window elapsed or merchant expired it |
cancelled | Session was cancelled |
Only payment_status: "paid" means money was confirmed. A browser at the
success URL is not sufficient evidence.
Error envelope
{
"error": {
"type": "invalid_request_error",
"code": "redirect_origin_not_allowed",
"message": "success_url must use an origin registered for this merchant.",
"param": "success_url",
"request_id": "evt_example"
}
}
Treat 400 as a request bug, 401 as a missing/invalid key, 403 as a missing
key scope, 404 as absent or outside the merchant boundary, 409 as an
idempotency conflict/in-progress request, and 5xx as retryable with the same
idempotency key. Successful and client-error responses are replayed for that
key. A 5xx retains the operation identity but opens it for another attempt.
Rupay derives the same local Checkout Session ID and Stripe idempotency keys on
every attempt, so recovery reuses any customer, invoice, invoice items, or local
session already created before the failure. A processing attempt has a
15-minute lease; after that, the same key can safely reclaim an interrupted
operation.
Rupay does not rotate a Stripe sub-operation key after an explicit Stripe server error because Stripe can reconcile an indeterminate request later and a new key could create a duplicate, consistent with Stripe's server-error guidance. Such provider incidents remain pending for operator/webhook reconciliation even though retrying the Rupay request remains duplicate-safe.
Production checklist
- Use a live merchant/key and registered production redirect origins.
- Configure the card/eSewa provider credentials for the intended environment.
- Configure and verify provider webhooks before taking live payments.
- Leave Cybersource recovery disabled until its encrypted provider webhook can complete a session without a browser return; Rupay falls back to Stripe Cards while that cutover is incomplete.
- Register the merchant webhook and persist the returned secret securely.
- Schedule Rupay's private background worker for webhook retries and receipt emails.
- Complete the end-to-end cases in the Fundyatra guide.
The machine-readable contract is in OpenAPI.
Automatic checkout expiration
Checkout sessions default to a 24-hour lifetime. Set expires_at to a Unix
timestamp between 30 minutes and 24 hours from creation to use a shorter window.
The operations worker expires overdue created and pending_payment sessions
every minute, including existing overdue sessions, and emits
checkout.session.expired to subscribed merchant webhook endpoints. The worker
processes a bounded backlog per run; delivery failures use the existing retry queue.
Paid and fulfilled sessions are preserved. Provider payment attempts are retained,
and a verified late success can restore an expired session to paid.
Minimum payment totals
The final payable total must be at least USD 0.50 (50 minor units) or NPR 1.00
(100 minor units), including after discounts. Promotion codes that reduce the total
below that currency's minimum are rejected with HTTP 400 and code
checkout_amount_below_minimum; discounts are not silently capped. Coupon quotes
and checkout creation enforce the same rule before any Stripe resources are created.
Existing sessions below the minimum cannot start a new payment attempt.