RupayDeveloper docsOpenAPI specification

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_url and cancel_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}/subscriptions
  • GET /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

FieldRequiredContract
modeYesMust be payment
client_reference_idRecommendedMerchant payment/order ID, maximum 200 characters
coupon_codeNoActive merchant coupon code, 3-32 characters
currencyNonpr by default; supported values are npr and usd
line_itemsYes1-50 items
line_items[].descriptionYes1-200 characters
line_items[].quantityNoInteger 1-99; defaults to 1
line_items[].amount_unitYesPositive integer minor-unit price
customer.emailNoValid email, maximum 254 characters
customer.phoneNo7-30 characters
success_urlYesHTTPS; live origin must be registered
cancel_urlYesHTTPS; live origin must be registered
expires_atNoUnix seconds, 30 minutes to 24 hours ahead; defaults to 24 hours
metadataNoUp 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

statusMeaning
created, pending_paymentOpen and payable until expiration
paidA provider-confirmed payment was recorded
fulfilledRupay-side downstream fulfillment completed, if used
failedSession ended unsuccessfully
expiredPayment window elapsed or merchant expired it
cancelledSession 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.