Kulcare Docs

Local webhook tunnel (Cloudflare)

Some flows are webhook-authoritative — the backend only completes them when the payment gateway POSTs a confirmation back to it. For example, the care-package purchase (#1086) activates a patient's subscription only when Cashfree calls POST /billing/webhooks/cashfree after a successful payment. On a deployed environment the gateway can reach the server directly; on localhost it can't, so you need a tunnel that gives your machine a public HTTPS hostname.

backend/bin/dev-tunnel sets up a per-developer Cloudflare Tunnel so every dev can run this in parallel without colliding.

What you get

Everything derives from a per-dev handle (your username by default):

Value
Tunnel namekulcare-dev-<handle>
Hostname<handle>-dev.kulcare.com
Webhook URLhttps://<handle>-dev.kulcare.com/billing/webhooks/cashfree
Marketplace webhook URLshttps://<handle>-dev.kulcare.com/billing/marketplace/webhooks/cashfree, .../razorpay, .../stripe/connect, .../stripe/account
Rendered config~/.cloudflared/kulcare-dev-<handle>.yml

Tunnel names are unique per Cloudflare account and each dev gets their own DNS record, so there are no collisions.

Path rules

The tunnel serves only the webhook paths. The rendered ingress sends a path that matches ^/billing/(marketplace/)?webhooks/ to the local backend. Every other path on the public hostname gets HTTP 404 from Cloudflare. Use the app through localhost, not through the tunnel.

If your config was rendered before this rule, run bin/dev-tunnel setup again. Then check it: curl -s -o /dev/null -w '%{http_code}' https://<handle>-dev.kulcare.com/up must show 404.

Data rule

While the tunnel runs, use the local development database with seed data only. Do not load a copy of staging or production data. A webhook path is public, and the tunnel connects it to your machine.

Prerequisites

  • cloudflared installed: brew install cloudflared
  • Access to the Kulcare Cloudflare account (the setup step runs cloudflared tunnel login; pick the account/zone that owns kulcare.com).

One-time setup

cd backend
bin/dev-tunnel setup

This logs you in (if needed), creates your tunnel, routes the DNS record, and generates your local config at ~/.cloudflared/kulcare-dev-<handle>.yml. The config shape is the heredoc inside bin/dev-tunnel (step 4 of setup) — edit it there to change it for everyone.

Note: cloudflared's login certificate (~/.cloudflared/cert.pem) is scoped to one Cloudflare account/zone. If setup reports it can't route *.kulcare.com, your login is pointed at a different account — re-run cloudflared tunnel login and select the kulcare.com zone. (Being a dashboard super-admin is not the same as the cert's scope.)

Running it

# 1. Start the backend on port 4000
cd backend && bin/dev

# 2. In another shell, start the tunnel
bin/dev-tunnel run

Then register your webhook URL (bin/dev-tunnel url) in the Cashfree dashboard under Developers → Webhooks (Test/Sandbox mode), at version 2025-01-01. Cashfree sends PAYMENT_SUCCESS_WEBHOOK, PAYMENT_FAILED_WEBHOOK and PAYMENT_USER_DROPPED_WEBHOOK. The bare names PAYMENT_SUCCESS and PAYMENT_COMPLETED do not occur.

Marketplace webhooks (vendor packages)

The marketplace has its own endpoints, in Billing::Marketplace::*WebhooksController.

GatewayURL pathEvents to subscribe
Cashfree/billing/marketplace/webhooks/cashfreePayment success, failed, user dropped; Refund; Easy Split vendor status (VENDOR_STATUS_UPDATE); vendor settlement
Razorpay (test mode)/billing/marketplace/webhooks/razorpaypayment.captured, payment.failed, refund.processed, transfer.processed, account.*
Stripe (test mode)/billing/marketplace/webhooks/stripe/connectConnected-account events (account.updated)
Stripe (test mode)/billing/marketplace/webhooks/stripe/accountPlatform events (payment_intent.*, charge.refunded)

A Cashfree marketplace order carries its own order_meta.notify_url. The backend makes it from MARKETPLACE_WEBHOOK_BASE_URL, so a marketplace payment webhook comes to the marketplace endpoint. Production ignores this variable. A plain http URL gives no notify_url, because Cashfree refuses it.

In development, set MARKETPLACE_WEBHOOK_BASE_URL to your tunnel. If you do not set it, a local order has no notify_url, and the backend logs this one time. Cashfree then uses the dashboard URL. The backend never falls back to the route host, because the development credentials point it at staging: a fallback sends the signed webhooks of a local order to staging. Staging uses its own API origin when the variable is not set.

Session variables

Set these in the shell that starts the backend. They live for one session. Do not write a secret into a file, a commit or a log. Do not echo a value.

VariableValue
RAILS_MASTER_KEYFrom the main checkout's backend/config/credentials/development.key. Do not copy the key file into a worktree. development.yml.enc is not in git: in a worktree that has none, copy the encrypted file (never the key) from the main checkout.
MARKETPLACE_WEBHOOK_BASE_URLhttps://<handle>-dev.kulcare.com
RAZORPAY_MARKETPLACE_KEY_ID, RAZORPAY_MARKETPLACE_KEY_SECRETFrom the development credentials test_razorpay_key and test_razorpay_secret
RAZORPAY_MARKETPLACE_WEBHOOK_SECRET_CURRENTThe new secret that you type in the Razorpay test dashboard. It is not the API secret.
STRIPE_CONNECT_PLATFORM_SECRET_KEYFrom the development credentials stripe_api_key (a sk_test_ key)
STRIPE_CONNECT_WEBHOOK_SECRET_CONNECT_CURRENT, STRIPE_CONNECT_WEBHOOK_SECRET_ACCOUNT_CURRENTThe signing secrets of the two Stripe test endpoints

Credentials stay first: a variable is read only when the credential is blank. Read a value with a command that prints nothing, for example:

export RAZORPAY_MARKETPLACE_KEY_ID="$(cd <main checkout>/backend && bin/rails runner 'print Rails.application.credentials.test_razorpay_key')"

Cashfree needs no variable. The marketplace reads the same cashfree: block as the single-merchant path, and its webhook check accepts client_secret.

A direct gateway API call (for example a sandbox check) runs in bin/rails runner with the gateway's HTTP client, which reads the keys inside the process. Do not use curl with x-client-id or x-client-secret on a command line.

Cashfree credentials (local)

The gateway reads from Rails credentials (config/credentials/development.yml.enc):

cashfree:
  client_id:      "<sandbox App ID>"
  client_secret:  "<sandbox Secret Key>"
  env:            sandbox
  # webhook_secret is the SAME value as client_secret (Cashfree PG signs webhooks
  # with your Secret Key). Optional locally — dev allows unverified webhooks;
  # required on staging/production (they fail closed).
  webhook_secret: "<same as client_secret>"

To actually route a clinic's checkout through Cashfree (IN defaults to Razorpay until the BILLING_CASHFREE_ENABLED flag is on), buy a package from a clinic whose settings.billing.gateway is cashfree — the dev seed db/seeds/identity/04d_clinic_care_packages.rb does this for City Hospital.

Commands

CommandWhat
bin/dev-tunnel setupone-time: login + create tunnel + DNS + render config (idempotent)
bin/dev-tunnel runstart the tunnel
bin/dev-tunnel urlprint your webhook URLs (single-merchant and marketplace)
bin/dev-tunnel statusshow tunnel + config state

Overrides: DEV_HANDLE=<handle>, BACKEND_PORT=<port>, KULCARE_TUNNEL_ZONE=<zone>.

Staging / production

No tunnel needed — those are already publicly reachable. Add the cashfree: block (including webhook_secret) to that environment's credentials and register https://<env-host>/billing/webhooks/cashfree in the Cashfree dashboard. For the marketplace, also register https://<env-host>/billing/marketplace/webhooks/cashfree (see docs/guides/cashfree-setup.md). Do not set MARKETPLACE_WEBHOOK_BASE_URL in production.

On this page