0.0
The project is in a healthy, maintained state
A small, well-tested Ruby client for Cashfree Payment Gateway APIs: orders, payments, UPI, refunds, payment links and webhook verification. Instance-based, thread-safe, with API version pinning, exact money handling and secrets redacted by default. Not affiliated with or endorsed by Cashfree Payments.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 2.0, < 3
 Project Readme
cashfree_pg: Unofficial Ruby Client for Cashfree Payments

Orders, payments, UPI, refunds, payment links and webhook verification
for the Cashfree Payment Gateway, from plain Ruby or Rails.

CI Ruby Cashfree API License: MIT

Quick start · Payment flow · Rails · API reference · Webhooks · Sandbox testing

Important

Community project, not an official SDK. cashfree_pg is built and maintained independently. It is not affiliated with, endorsed by, or supported by Cashfree Payments.

⚡ Overview

cashfree_pg wraps the Cashfree Payment Gateway (PG) REST API in a small, instance-based Ruby interface. It keeps Cashfree's own field names, so Cashfree's docs still apply, and it handles the parts that are easy to get wrong in a payment integration.

You write: client.orders.create(order_id: "order_42", order_amount: "499.00", ...)
You get: a Response with Cashfree's fields, amounts as BigDecimal, the request_id for support, and a typed exception for every failure. No raw HTTP, no Float money, no secrets in your logs.

The gem talks only to Cashfree over HTTPS. It never handles card data, and it has no telemetry, request logging or automatic retries. Faraday is its only runtime dependency.

🧾 Orders

Create, fetch and terminate, with an optional idempotency key.

💳 Payments

List and fetch, plus capture or void pre-authorised payments.

📱 UPI

Intent, QR and collect on the order session, without sending your secret.

↩️ Refunds

Create with your own refund_id, then fetch or list.

🔗 Payment Links

Create, fetch and cancel, and find the orders a paid link created.

🔐 Webhooks

Constant-time HMAC-SHA256 check, optional timestamp window.

🧯 Typed Errors

One exception per HTTP status, with Cashfree's code and request_id.

💰 Exact Money

Two-decimal JSON amounts out, BigDecimal back in.

🛡️ Safe Defaults

HTTPS only, TLS verified, secret redacted from inspect and errors.

🔄 Payment flow

How one payment moves through your app, cashfree_pg and Cashfree:

Payment flow: 1. create the order with orders.create on your server, 2. the customer pays on Cashfree checkout or UPI, 3. verify the webhook with Webhook.construct_event, 4. confirm with orders.fetch that order_status is PAID, 5. optionally refund with refunds.create

  • 🔁 Retries are yours. The gem never retries. Reuse your own order_id or refund_id and the call is safe to repeat.
  • 🔔 Verify, then confirm. Check the webhook signature, deduplicate repeats, and fetch the order before you fulfil it.
  • 🧾 Log the request_id. Every response and error carries one, and it is what Cashfree support asks for.

🚀 Quick start

Requirement Version
Ruby 3.3 or newer
Cashfree account PG API credentials, sandbox or production

1. Install. Add the gem to your Gemfile and run bundle install:

gem "cashfree_pg", "~> 0.1.0"

2. Create a client. There is no global configuration: credentials live on the instance, one client per merchant account. The gem reads nothing from ENV itself.

require "cashfree_pg"

client = CashfreePg::Client.new(
  client_id:     ENV.fetch("CASHFREE_CLIENT_ID"),
  client_secret: ENV.fetch("CASHFREE_CLIENT_SECRET"),
  environment:   :sandbox # or :production, with production credentials
)

3. Take a payment.

order = client.orders.create(
  order_id:         "order_#{SecureRandom.hex(6)}",
  order_amount:     "499.00", # String, Integer or BigDecimal; never Float
  order_currency:   "INR",
  customer_details: { customer_id: "cust_42", customer_phone: "9999999999" }
)

order.payment_session_id # hand this to Cashfree checkout on the frontend
client.orders.fetch(order.order_id).order_status # => "ACTIVE", "PAID", ...
All client options
Option Default Notes
client_id required Your Cashfree PG app id.
client_secret required Never shown by inspect, to_s or pp. Also the webhook signing secret.
environment :sandbox :sandbox (sandbox.cashfree.com/pg) or :production (api.cashfree.com/pg). Must be a Symbol.
api_version "2025-01-01" Sent as x-api-version on every call.
open_timeout 30 Seconds to wait for a connection.
read_timeout 60 Seconds to wait for a response.

.env.example lists the variable names this repository's tooling uses. Never commit real credentials; .env is git-ignored.

To try unreleased changes, install from GitHub: gem "cashfree_pg", github: "Shadbox-Infosystem/cashfree_pg", branch: "main".

📦 Usage

Single objects come back as a CashfreePg::Response, with Cashfree's field names exactly:

order.order_status                          # field as a method
order["order_amount"]                       # => BigDecimal("499.0")
order.dig(:customer_details, :customer_id)  # use [] or dig for keys like hash or class
order.request_id                            # quote this to Cashfree support

payments.list and payment_links.orders return an Array of Response. Every method and its Cashfree endpoint is listed in docs/api.md.

UPI: intent, QR and collect
client.payments.upi(order, channel: :intent)  # sent as Cashfree's "link"
client.payments.upi(order, channel: :qr)      # sent as "qrcode"
client.payments.upi(order, channel: :collect, upi_id: "testsuccess@gocash")
client.payments.upi(order.payment_session_id, channel: :qr, upi_expiry_minutes: 10)

Calling Order Pay from a server needs Cashfree's S2S feature on your account. Without it, Cashfree returns 403 and the gem raises CashfreePg::PermissionError saying so.

Payments and pre-authorisation
payments = client.payments.list(order.order_id)
client.payments.fetch(order.order_id, payments.first.cf_payment_id)

client.payments.authorize(order.order_id, action: :capture, amount: "499.00", idempotency_key: "cap_1")
client.payments.authorize(order.order_id, action: :void, idempotency_key: "void_1")
Refunds
client.refunds.create(order.order_id, refund_amount: "499.00", refund_id: "refund_00912")
client.refunds.fetch(order.order_id, "refund_00912")
client.refunds.list(order.order_id)

Always pass your own refund_id. If a create times out, fetch that id before trying again; a duplicate raises CashfreePg::ConflictError.

Payment links
link = client.payment_links.create(
  link_id: "link_123", link_amount: "499.00", link_currency: "INR", link_purpose: "Invoice 1042",
  customer_details: { customer_phone: "9999999999" }
)
link.link_url                                   # send this to the customer
client.payment_links.cancel(link.link_id)       # ACTIVE links only

paid = client.payment_links.orders(link.link_id) # Cashfree creates the order when the link is paid
client.refunds.create(paid.first.order_id, refund_amount: "499.00", refund_id: "refund_link_1")
Terminating orders and calling other endpoints
client.orders.terminate(order.order_id).order_status # => "TERMINATED" or "TERMINATION_REQUESTED"
client.get("/orders/order_123") # any PG endpoint, same auth headers, errors and Response

client.get, client.post and client.patch take paths relative to /pg.

🧯 Error handling

Every exception inherits from CashfreePg::Error and exposes status, code, type and request_id. Messages never contain the client secret.

begin
  order = client.orders.create(order_id: "order_123", order_amount: "499.00", order_currency: "INR",
                               customer_details: { customer_id: "cust_42", customer_phone: "9999999999" })
rescue CashfreePg::ConflictError, CashfreePg::TimeoutError
  order = client.orders.fetch("order_123") # already exists, or outcome unknown: check, don't recreate
rescue CashfreePg::Error => e
  warn "Cashfree #{e.class}: status=#{e.status} code=#{e.code} request_id=#{e.request_id}"
  raise
end

HTTP 400, 401, 403, 404, 409, 422 and 429 each have their own class; 5xx raises APIError, and a missing response raises ConnectionError or TimeoutError. The full mapping is in docs/api.md. Bad local input, such as a blank id or an unknown UPI channel, raises ArgumentError before any request is sent.

🚂 Rails integration

No Railtie needed: an initializer, a service object, and a controller for webhooks.

# config/initializers/cashfree.rb
CASHFREE = CashfreePg::Client.new(
  client_id:     ENV.fetch("CASHFREE_CLIENT_ID"),
  client_secret: ENV.fetch("CASHFREE_CLIENT_SECRET"),
  environment:   ENV.fetch("CASHFREE_ENVIRONMENT", "sandbox").to_sym
)

# app/services/cashfree_checkout.rb
class CashfreeCheckout
  def self.call(order_id:, amount:, customer_id:, phone:)
    CASHFREE.orders.create(
      order_id: order_id, order_amount: amount.to_s, order_currency: "INR",
      customer_details: { customer_id: customer_id, customer_phone: phone }
    ).payment_session_id
  end
end

# app/controllers/cashfree_webhooks_controller.rb  (route: post "/webhooks/cashfree")
class CashfreeWebhooksController < ActionController::API
  def create
    event = CashfreePg::Webhook.construct_event(
      raw_body: request.raw_post,
      headers:  request.headers,
      secret:   ENV.fetch("CASHFREE_CLIENT_SECRET")
    )
    CashfreeEventJob.perform_later(event.type, event.data) # your job; deduplicate there
    head :ok
  rescue CashfreePg::SignatureVerificationError
    head :bad_request
  end
end

Warning

Pass request.raw_post, never parsed params. Re-encoding changes amounts like 10.10 and the signature will not match. See docs/webhooks.md for deduplication, the optional tolerance: window and Cashfree's IP allow-list.

🧪 Sandbox testing

Unit specs run on every push. RSpec with WebMock (real network blocked) covers every resource method, headers, money handling, each HTTP status mapping, timeouts, secret redaction and webhook signatures. SimpleCov fails the run below 90% line coverage, and CI runs on Ruby 3.3, 3.4 and latest, with bundler-audit and a gitleaks secret scan.

Integration specs hit the real sandbox. spec/integration/sandbox_spec.rb runs against sandbox.cashfree.com/pg when you provide credentials. It skips without them, refuses to run with CASHFREE_ENVIRONMENT=production, and does not run in CI.

Flow Verified against the sandbox
Order → UPI intent → payments payment_session_id returned, intent channel is link, payments list is an Array
Terminate Unpaid order becomes TERMINATED or TERMINATION_REQUESTED
Payment link lifecycle Created, fetched, orders listed, cancelled
Refund Created on a paid order and fetched by refund_id (needs CASHFREE_PAID_ORDER_ID)
cp .env.example .env                  # sandbox credentials only
set -a; source .env; set +a
bundle exec rspec spec/integration
bin/console                           # IRB with the gem loaded, sandbox only
Sandbox test values
Value Effect in sandbox
upi_id: "testsuccess@gocash" Successful UPI collect
refund_note: "SUCCESS", "FAILED", "PENDING", "ACTIVE" Simulates that refund status
link_notify: { send_email: true } Sandbox sends email; SMS is production only

🧭 Coverage and roadmap

Status Scope
✅ Orders, payments, pre-authorisation, UPI (intent, QR, collect), refunds, payment links
✅ Webhook verification, typed errors, exact money, RBS signatures, idempotency keys
🚧 First RubyGems release (v0.1.0) through the trusted-publishing release workflow
📋 Stabilise the public API for v1.0
📋 More PG endpoints, such as Order Pay for cards and netbanking, settlements and subscriptions

Until v1.0, a minor release may include breaking changes, listed in the changelog. Pin with ~> 0.1.0 to get only patch updates. Missing something? Open an issue before starting larger work.

🤝 Contributing

Bug reports and pull requests are welcome. This gem sits in payment flows, so correctness and security come before new features. Read CONTRIBUTING.md first.

git checkout dev && git checkout -b fix/short-description
bin/setup            # install dependencies
bundle exec rake     # specs, RuboCop, RBS validation and gem contents check

Match new endpoints to Cashfree's current API reference, cover them with RSpec and WebMock, never use Float for amounts, add a line under [Unreleased] in CHANGELOG.md, and open the pull request against dev. Never put real credentials or customer data in code, fixtures or screenshots. Report vulnerabilities privately as described in SECURITY.md. Everyone taking part follows the code of conduct.

📄 License

Released under the MIT License. Copyright © 2026 Muballigh Faraz.

"Cashfree" and "Cashfree Payments" are names of their respective owner, used here only to describe which API this library talks to. For official SDKs and support, see cashfree.com.


Built for the Ruby community ❤️

cashfree_pg · Unofficial Ruby Client for Cashfree Payments