RupayDeveloper docsOpenAPI specification

Refunds and disputes

Refund and chargeback inquiries are handled by Rupay support by email or phone. Merchants can view records in Refunds & disputes in the dashboard. They cannot create, change, or cancel refund/dispute records through the dashboard or API.

The support email defaults to support@rupay.dev. Configure RUPAY_SUPPORT_EMAIL and RUPAY_SUPPORT_PHONE to show the team's email and telephone links. Include the Rupay payment ID, amount, reason, and supporting evidence in the email or call. Do not send card numbers, security codes, or API keys.

Stripe Node SDK read compatibility

Rupay implements Stripe-shaped refund and dispute list and retrieval contracts, matching the existing pinned SDK API version 2026-02-25.clover. This is a read-only supported subset: provider refund execution, evidence submission, dispute closure, and other writes are handled by support. An issuer/provider opens a network chargeback; neither Stripe nor Rupay exposes a merchant chargeback creation API.

SDK methodEndpointPermission
stripe.refunds.list()GET /v1/refundsrefunds:read
stripe.refunds.retrieve(id)GET /v1/refunds/{id}refunds:read
stripe.disputes.list()GET /v1/disputesdisputes:read
stripe.disputes.retrieve(id)GET /v1/disputes/{id}disputes:read

Use a server-side Rupay API key. Bearer and Stripe SDK Basic authentication both work. Data stays isolated to the API key's merchant and staging/live environment. Newly issued full-access keys include both scopes; existing scoped keys need to be reissued with the permissions.

import Stripe from "stripe";

const origin = new URL(process.env.RUPAY_API_BASE_URL);
const stripe = new Stripe(process.env.RUPAY_API_KEY, {
  host: origin.hostname,
  port: origin.port ? Number(origin.port) : undefined,
  protocol: origin.protocol.replace(":", ""),
  apiVersion: "2026-02-25.clover",
});

const refunds = await stripe.refunds.list({ limit: 10 });
const refund = refunds.data[0]
  ? await stripe.refunds.retrieve(refunds.data[0].id)
  : null;
const disputes = await stripe.disputes.list({ limit: 10 });
const dispute = disputes.data[0]
  ? await stripe.disputes.retrieve(disputes.data[0].id)
  : null;

Lists return Stripe's { object: "list", url, data, has_more } envelope, newest first. Supported filters are charge, payment_intent, created (an integer timestamp or gt/gte/lt/lte ranges), limit (1–100, default 10), and either starting_after or ending_before. Use the returned charge/payment_intent values to filter later calls. Stripe SDK automatic pagination works for the supported list contracts. Expanded objects and other optional Stripe parameters are not supported and return validation errors.

Refunds use re_ IDs, object: "refund", integer minor-unit amount, lowercase currency, Unix created, standard refund reason, and pending/succeeded statuses. Support inquiries do not appear in this API until accepted for processing. A refund becomes succeeded only after support records a provider-confirmed outcome.

Disputes use du_ IDs, object: "dispute", standard reason/status fields, livemode, and the Stripe evidence/evidence-details structure. Only cases with a recorded provider reference are exposed. Support records provider-confirmed needs-response, under-review, and won/lost outcomes. Unknown evidence and deadlines are returned as null or empty values; Rupay does not claim a provider evidence submission occurred. Network withdrawals recorded as completed are treated as won.

Both objects contain charge and payment_intent identifiers. Non-Stripe rails use stable Rupay identifiers in Stripe's ch_/pi_ format; Stripe payments reuse a known provider PaymentIntent ID. These identifiers do not imply standalone Charge/PaymentIntent APIs are supported. Unknown balance transactions, transfer reversals, receipts, and unavailable provider information are null or empty. Internal support notes and actor identities are never exposed in SDK objects.

Authenticated calls to refunds.create, refunds.update, refunds.cancel, disputes.update, and disputes.close return HTTP 403 with instructions to contact support. There are no /v1/refund_requests or /v1/dispute_requests endpoints. Missing/foreign records return 404, missing permissions return 403, and invalid list filters return 400.

Support dashboard workflow

Only platform administrators configured through RUPAY_ADMIN_EMAILS can log and manage a support record. A submitted email/call inquiry can be recorded with the payment ID, amount, standard reason, and internal supporting details. Existing provider disputes additionally require the provider case reference at intake.

Refund workflow: submitted → under_review → approved → completed. Process the refund in the provider's own system before recording completion. Enter its provider reference and the cumulative provider-confirmed refunded amount across all refunds for that payment. Rupay does not add the same amount again if a Stripe refund webhook has already reported it. Requests can be rejected before completion or cancelled before review by support.

For disputes, record an existing provider case and update it when the provider's status changes. Complete the case with the provider reference and won, lost, or withdrawn outcome. Support communicates evidence to the provider outside Rupay. Automatic provider dispute synchronization and evidence upload/submission are not implemented.

Refund balances account for provider-recorded refunds, active support reservations, and recorded lost dispute amounts. Per-payment locking protects concurrent updates. Pending refunds and open disputes cannot overlap. Merchants can read support history but cannot invoke review or cancellation actions.

Rollout and verification

Apply migration 0033_fantastic_fenris through the repository's normal database migration procedure before enabling the dashboard/API. No production migration or deployment is performed by this change.

SDK and lifecycle integration tests use an explicitly supplied disposable local database:

PAYMENT_REQUEST_TEST_DATABASE_URL=postgres://user@127.0.0.1:5432/rupay_requests_test \
  bun test apps/web/lib/payment-requests.integration.test.ts

The runner refuses non-local URLs and names without test, applies migrations, creates isolated fixtures, and removes those fixtures. It tests the actual Stripe SDK against the route handlers as well as isolation, balances, concurrency, and pagination.

Reference contracts: Stripe refunds and Stripe disputes.