RupayDeveloper docsOpenAPI specification

Merchant webhooks

Rupay sends signed lifecycle events so a merchant can fulfill a payment without trusting browser redirects.

Register an endpoint

POST /v1/webhook_endpoints requires an API key with webhook_endpoints:write scope.

{
  "url": "https://merchant.example/api/webhooks/rupay",
  "enabled_events": ["checkout.session.completed"]
}

The URL must be HTTPS, publicly resolvable, and must not resolve to loopback, private, link-local, or reserved addresses. Redirect responses are not followed.

Response:

{
  "id": "we_example",
  "object": "webhook_endpoint",
  "url": "https://merchant.example/api/webhooks/rupay",
  "enabled_events": ["checkout.session.completed"],
  "secret": "whsec_example"
}

Store secret immediately. Endpoints currently use their merchant's signing secret.

Stripe SDK endpoints

When registered through the pinned Stripe SDK contract, an endpoint gets its own secret, stores api_version: "2026-02-25.clover", and accepts checkout.session.completed and checkout.session.expired. Its events contain SDK-shaped checkout objects and also carry Stripe-Signature, compatible with webhooks.constructEventAsync. Native registration below retains the existing merchant secret and event format. See SDK compatibility.

Event types

EventWhen sentRecommended action
checkout.session.completedA provider-confirmed payment moves the session to paidCanonical merchant fulfillment event
invoice.paidThe same confirmed payment is reflected in Rupay's invoice compatibility layerUse only if migrating an existing invoice integration
checkout.session.expiredAn unfinished checkout reaches its expiry or is manually expiredRelease checkout holds; do not fulfill
payment.failedA provider reports a failed attemptUpdate UX/operations; do not fulfill

Subscribe to either checkout.session.completed or invoice.paid for fulfillment, not both, unless your consumer deliberately deduplicates the two event types.

Subscription events

Catalog subscription checkouts also support subscription.activated, subscription.payment_failed, and subscription.expired. These contain the serialized subscription under data.object rather than a checkout session. Payment events additionally include a nested subscription object for these checkouts. Durations are manual prepaid periods, not automatic collection. Expiry events are queued by the authenticated webhook-delivery worker; access checks must enforce current_period_end even when delivery is delayed. See Nepse Edge integration for the full contract.

Delivery

POST /api/webhooks/rupay HTTP/1.1
Content-Type: application/json
Rupay-Event-Id: mwe_example
Rupay-Signature: t=1787261400,v1=hex_hmac_sha256

Payload:

{
  "id": "mwe_example",
  "object": "event",
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "id": "cs_example",
      "object": "checkout.session",
      "client_reference_id": "fyp_01JABCDEF",
      "payment_status": "paid",
      "status": "paid",
      "amount_total": 250000,
      "currency": "npr",
      "payment_method_type": "esewa",
      "payment_provider": "esewa"
    }
  },
  "created": 1787261400,
  "livemode": false,
  "urls": {
    "success_url": "https://merchant.example/success?session=cs_example",
    "cancel_url": "https://merchant.example/cancel"
  }
}

For payment events, the full serialized Checkout Session is present under data.object.

Verify the signature

Use the exact request bytes before JSON parsing. The signed input is:

<timestamp>.<raw_request_body>

Compute HMAC-SHA256 with the endpoint secret and compare the hexadecimal digest to v1 with a constant-time comparison. Reject malformed signatures and timestamps more than five minutes from the server clock.

The runnable Node example is examples/verify-webhook-signature.mjs.

Process safely

For a completion event:

  1. verify the signature and timestamp;
  2. parse JSON only after verification;
  3. deduplicate event.id under a unique constraint;
  4. find the merchant record by client_reference_id;
  5. compare session ID, payment_status, amount, and currency with immutable local values;
  6. atomically transition the local payment and write the business ledger;
  7. return 2xx quickly.

Return 2xx for an already processed valid event. Return non-2xx for a temporary processing failure so Rupay retries. Quarantine and alert on an authenticated event that does not match the expected amount/reference.

Retry behavior

Rupay attempts delivery immediately. Failed deliveries use exponential backoff starting at approximately 30 seconds, capped at six hours, for at most 12 attempts. Delivery calls time out after 10 seconds.

Production Rupay must set a random secret of at least 32 characters and invoke the private background worker from a trusted scheduler. It retries merchant webhooks and sends queued payment-receipt emails (including their invoice and receipt attachments):

RUPAY_WEBHOOK_WORKER_SECRET=replace-with-at-least-32-random-characters
curl --fail --request POST \
  --header "Authorization: Bearer $RUPAY_WEBHOOK_WORKER_SECRET" \
  https://your-rupay-host.example/internal/webhook-deliveries

The dedicated Cloudflare Cron Worker in workers/operations-cron calls the production rupay.dev endpoint every minute. Configure its RUPAY_WEBHOOK_WORKER_SECRET secret with the same value as the production Rupay web Worker, then deploy it with:

node_modules/.bin/wrangler secret put RUPAY_WEBHOOK_WORKER_SECRET \
  --config workers/operations-cron/wrangler.jsonc
node_modules/.bin/wrangler deploy \
  --config workers/operations-cron/wrangler.jsonc --keep-vars

The Cron Worker is a separate Cloudflare script and has no public route. It calls the live rupay-web Worker through a service binding. Check its scheduled invocation logs and the payment_receipt_emails status after deployment. Cloudflare may take up to 15 minutes to activate a new Cron Trigger. The endpoint remains an operator endpoint and is not part of the merchant API.

Provider recovery prerequisite

Rupay marks payments paid only after a verified provider result. Stripe webhooks must have STRIPE_WEBHOOK_SECRET configured, and eSewa production must use its signed callback plus server-to-server status verification.

Cybersource Unified Checkout has an asynchronous receiver at POST /webhooks/cybersource. It accepts only the unifiedCheckout product and uc.orders.transactionresults event. The receiver:

  1. requires and decrypts the message-level-encrypted JWE payload;
  2. verifies v-c-signature over the exact decrypted payload with HMAC-SHA256 and rejects stale timestamps;
  3. binds the notification to the configured signature key, organization, webhook subscription, product, and event type;
  4. verifies Rupay's signed checkout reference plus the immutable amount and currency before changing payment state;
  5. records the provider event and safely resumes reconciliation when Cybersource retries after a partial local failure.

Successful capture/settlement results complete the Checkout Session. Explicit failure results emit payment.failed. Authorized, pending, processing, and unknown future statuses remain pending and never trigger merchant fulfillment.

Cybersource receiver configuration

Configure these server-only values:

CYBERSOURCE_WEBHOOK_ID=the_subscription_webhook_id
CYBERSOURCE_WEBHOOK_MLE_KEY_ID=the_registered_certificate_key_id
CYBERSOURCE_WEBHOOK_MLE_PRIVATE_KEY_BASE64=base64_of_the_matching_pem_private_key
CYBERSOURCE_WEBHOOK_SIGNATURE_KEY_ID=the_digital_signature_key_id
CYBERSOURCE_WEBHOOK_SIGNATURE_SECRET=the_base64_digital_signature_key
CYBERSOURCE_WEBHOOK_RECOVERY_ENABLED=false

GET /webhooks/cybersource is the health-check endpoint. It returns 200 when the receiver keys are present even while recovery remains disabled for buyer checkout. Use this cutover sequence:

  1. create a Cybersource digital-signature key and store its key ID and base64-encoded secret;
  2. generate a separate 2048-bit RSA MLE key pair, register the X.509 public certificate, and store the returned key ID plus the matching private key;
  3. create a webhook subscription for unifiedCheckout / uc.orders.transactionresults with https://<rupay-host>/webhooks/cybersource as both the notification URL and health-check URL;
  4. wait for URL approval and confirm that the subscription is ACTIVE;
  5. send an encrypted test notification or complete a sandbox card payment, close the buyer page before browser reconciliation, and verify that the provider webhook alone completes the Rupay session and merchant webhook;
  6. only then set CYBERSOURCE_WEBHOOK_RECOVERY_ENABLED=true.

In a production Next.js deployment, Rupay exposes Domestic Cards only when all receiver values are present and the explicit recovery flag is true. Otherwise Nepal checkout automatically shows Stripe Cards instead; eSewa remains available for NPR. This lets Fundyatra launch without Cybersource while keeping the Domestic Cards cutover fail-closed.

The recovery flag is an operator attestation, not an automatic provider-status check. Do not turn it on merely because the route is deployed. Cybersource URL review can take one to two business days, so subscription activation and the browser-closed recovery test remain required production evidence.