Skip to main content
The Cashfree Subscription Custom Card Component lets you embed a secure, SDK-managed card input field directly into your iOS application UI. The CFSubsCardComponent captures and processes the card number entirely within the SDK. Your application never receives the raw card number. This integration is suitable for non-PCI merchants. A non-PCI merchant is not certified to store, process, or transmit raw cardholder data and relies on the SDK to handle card data securely on their behalf. The component provides auto-formatted card number input (groups of four digits), real-time card network detection with icon display, Luhn validation on every keystroke, and automatic BIN lookup after eight digits. Metadata is delivered through CFSubsCardListener. You receive only digit count, Luhn result, and BIN info, not the raw card number.
This page covers the custom card component integration only. For the full iOS Subscription Element integration, including UPI and eNach (net banking), see iOS Integration.
For merchants with raw card access (PCI-DSS certified), use .setCardNumber() on CFCardSubs.CFCardSubsBuilder() directly instead of CFSubsCardComponent. That path follows the standard subscription card flow.

Prerequisites

Complete the following tasks before you start the integration: The integration consists of three steps:

Step 1

Create a subscription

Step 2

Open the payment page

Step 3

Confirm the payment

Step 1: Create a subscription Server-side

Create a subscription from your backend server before you process any payment.
This API requires your secret key. Create subscriptions through your server only. Do not call this API directly from your mobile application.
After the subscription is created, your backend receives a subscription_id and a subscription_session_id. Pass both values to your iOS client to proceed with the payment.
The following example shows how to create a subscription using the Create Subscription API:
curl --request POST \
  --url https://sandbox.cashfree.com/pg/subscriptions \
  --header 'Content-Type: application/json' \
  --header 'x-api-version: 2025-01-01' \
  --header 'x-client-id: <your-client-id>' \
  --header 'x-client-secret: <your-client-secret>' \
  --data '{
    "subscription_id": "Demo_Subscription",
    "customer_details": {
      "customer_name": "john",
      "customer_email": "john@dummy.com",
      "customer_phone": "9908730221",
      "customer_bank_account_number": "59108290701802",
      "customer_bank_ifsc": "HDFC0002614",
      "customer_bank_code": "HDFC",
      "customer_bank_account_type": "SAVINGS"
    },
    "plan_details": {
      "plan_name": "plan12345",
      "plan_type": "PERIODIC",
      "plan_amount": 10,
      "plan_max_amount": 100,
      "plan_max_cycles": 100,
      "plan_intervals": 2,
      "plan_currency": "INR",
      "plan_interval_type": "WEEK",
      "plan_note": "Bi-weekly INR 10 plan"
    },
    "authorization_details": {
      "authorization_amount": 100,
      "authorization_amount_refund": true,
      "payment_methods": [
        "card"
      ]
    },
    "subscription_meta": {
      "return_url": "https://example.com/subscription/return",
      "notification_channel": [
        "EMAIL",
        "SMS"
      ],
      "session_id_expiry": "2025-06-01T23:00:08+05:30"
    },
    "subscription_expiry_time": "2100-01-01T23:00:08+05:30",
    "subscription_first_charge_time": "2025-06-01T23:00:08+05:30",
    "subscription_tags": {
      "psp_note": "Monthly subscription payment"
    }
  }'
A successful response returns the subscription_id and subscription_session_id. Use these values to build the CFSubscriptionSession object in Step 2. The API returns the following response on success:
{
  "subscription_id": "Demo_Subscription",
  "subscription_session_id": "subs_token_tc9JCN4MzUIJ",
  "subscription_status": "INITIALIZED",
  "cf_subscription_id": "4"
}
For the full list of request parameters and response fields, see the Create Subscription API.

Step 2: Open the payment page Client-side

After you create the subscription, set up the card component and open the payment page so the customer can provide their card details.

1. Set up the SDK

The Cashfree iOS SDK is available via CocoaPods. The integration requires version 2.4.0 or above and iOS 13.0 or higher. Add the following to your Podfile:
platform :ios, '13.0'

target 'YourAppTarget' do
  use_frameworks!
  pod 'CashfreePG', '2.4.0'
end
Run pod install to install the dependency.

2. Complete the payment

To complete the payment, follow these steps:
  1. Add the CFSubsCardComponent to your view.
  2. Initialise the card component after the session is ready.
  3. Create a CFSubscriptionSession object.
  4. Conform to CFSubsCardListener to receive card metadata.
  5. Set up the payment callback.
  6. Build the payment object and initiate the payment.

Add the card component to your view

CFSubsCardComponent is a UIView that you can add in Interface Builder or programmatically. In Interface Builder, set the custom class of a UIView to CFSubsCardComponent and connect it as an outlet:
@IBOutlet weak var cfCardComponent: CFSubsCardComponent!
Apply standard UIView styling as needed:
cfCardComponent.layer.borderColor = UIColor.systemGray4.cgColor
cfCardComponent.layer.borderWidth = 1.0
cfCardComponent.layer.cornerRadius = 8.0
The component manages only the card number field. Collect cardholder name, expiry month, expiry year, and CVV in separate input fields in your checkout UI.

Initialise the card component

Call initializeCardComponent only after you have a valid CFSubscriptionSession. The component uses the session to authenticate BIN lookup requests.
ParameterTypeRequiredDescription
sessionCFSubscriptionSessionYesSession from your backend.
card_listenerCFSubsCardListenerYesReceives metadata on every keystroke.
hint_textStringYesPlaceholder text. Pass "" for the default XXXX XXXX XXXX XXXX format.
fontUIFont?NoCustom font. Pass nil for the system default.
textColorUIColor?NoText colour. Pass nil for the system default.
enable_pastingBoolYesWhether the user can paste from the clipboard.
The following example initialises the component with custom font and text colour:
cfCardComponent.initializeCardComponent(
    session: subsSession,
    card_listener: self,
    hint_text: "",
    font: UIFont.systemFont(ofSize: 16),
    textColor: UIColor.label,
    enable_pasting: false
)

Create a session

The CFSubscriptionSession object holds the session context for the payment. It accepts the subscription_session_id and subscription_id obtained from Step 1, and the Cashfree environment (.SANDBOX or .PRODUCTION). Set the environment to .SANDBOX for testing or .PRODUCTION for live payments. The following example shows how to create the session object:
do {
    let subsSession = try CFSubscriptionSession.CFSubscriptionSessionBuilder()
        .setSubscriptionId("your_subscription_id")
        .setSubscriptionSessionId("your_subscription_session_id")
        .setEnvironment(.SANDBOX) // or .PRODUCTION
        .build()
} catch {
    print(error.localizedDescription)
}

Set up the CFSubsCardListener callback

Conform your view controller to CFSubsCardListener and implement cardMetaData(card_listener_response:). This callback fires on every keystroke and again after the BIN lookup completes.
class CheckoutViewController: UIViewController, CFSubsCardListener {

    func cardMetaData(card_listener_response: CFSubsCardListenerResponse) {
        let charCount = card_listener_response.numberOfCharacters ?? 0
        let meta = card_listener_response.meta_data ?? [:]

        let isLuhnValid = meta["luhn_check"] as? Bool ?? false
        let binInfo = meta["card_bin_info"] as? [String: Any]

        // Enable Pay button when the card number is complete and valid
        payButton.isEnabled = (charCount == 16 && isLuhnValid)

        if let scheme = binInfo?["card_scheme"] as? String {
            print("Card scheme: \(scheme)")
        }
    }
}
The callback delivers a CFSubsCardListenerResponse with the following top-level fields:
FieldTypeDescription
numberOfCharactersInt?Number of digits entered in the card component (spaces excluded).
messageString?Informational message about the response.
typeString?Always "card_info".
meta_data[String: Any]?Contains card validation and BIN data (see below).
The meta_data dictionary contains the following keys:
KeyTypeDescription
card_lengthIntSame as numberOfCharacters; total digits entered.
luhn_checkBooltrue if the entered card number passes the Luhn algorithm; false otherwise.
card_bin_info[String: Any]Populated once 8 digits are entered. Contains BIN lookup data from the server. nil if fewer than 8 digits are entered.
The card_bin_info object is only present in the callback after the customer has entered at least 8 digits. Always check that the key exists in meta_data before accessing it.
After 8 digits are entered, the component calls the Cashfree BIN API automatically. The response is delivered in meta_data["card_bin_info"] on the next cardMetaData callback. No additional setup is required. Authentication uses the x-sub-session-id from the session provided at initialisation. The card_bin_info dictionary returned from the BIN lookup API contains the following fields:
FieldDescription
Card_binBank Identification Number.
logoCard network logo reference.
sub_typeCard sub-type classification.
typeCard type classification.
schemaCard network schema.
brandCard brand classification.
bank nameIssuing bank name.
The component auto-detects the following card networks and displays the corresponding icon: Visa, Mastercard, American Express (Amex), RuPay, Diners Club, Discover, and JCB. The following example illustrates how callback data evolves as the customer enters their card number:
// After the 1st digit
{"card_length": 1, "luhn_check": false}

// After the 8th digit — card_bin_info becomes available
{
  "card_length": 8,
  "luhn_check": false,
  "card_bin_info": {
    "card_scheme": "visa",
    "type": "filtered",
    "sub_type": "filtered",
    "brand": "filtered",
    "bank name": "axis bank"
  }
}

// After all 16 digits, luhn_check passes
{
  "card_length": 16,
  "luhn_check": true,
  "card_bin_info": {
    "card_scheme": "visa",
    "type": "filtered",
    "sub_type": "filtered",
    "brand": "filtered",
    "bank name": "axis bank"
  }
}

Set up the payment callback

The SDK exposes the CFResponseDelegate protocol to receive callbacks when the subscription payment journey ends. This protocol consists of two methods:
func onError(_ error: CFErrorResponse, order_id: String)
func verifyPayment(order_id: String)
Register the callback on CFPaymentGatewayService before you call doSubsPayment. Set paymentService.setCallback(self) in your view controller before initiating payment.
The following example shows how to implement the callback:
extension CheckoutViewController: CFResponseDelegate {

    func onError(_ error: CFErrorResponse, order_id: String) {
        print("Error: \(error.message ?? "")")
        // Show error to user
    }

    func verifyPayment(order_id: String) {
        print("Flow complete. Verify order: \(order_id)")
        // Verify subscription status from your backend before proceeding.
    }
}
Backend verification is mandatory. The SDK signals that the payment UI flow ended, not that payment succeeded. Always verify payment status from your backend before presenting success.

Build the payment object and initiate payment

When the customer fills in their card details and taps the pay button, build the CFCardSubs and CFCardSubsPayment objects and call doSubsPayment() on CFPaymentGatewayService.
You must call .setCardComponent(cfCardComponent) on the CFCardSubs builder. Because the SDK manages the card number internally via CFSubsCardComponent, your application does not have access to the raw card number. .setCardComponent() and .setCardNumber() are mutually exclusive. Never call both. Omitting .setCardComponent() or calling .setCardNumber() alongside it causes the payment to fail.
The CFCardSubs.CFCardSubsBuilder() supports the following methods:
MethodRequiredDescription
.setChannel(_ channel: String)Yes (default: "link")Payment flow type.
.setCardComponent(_ component: CFSubsCardComponent)YesComponent reference; replaces .setCardNumber().
.setCardHolderName(_ name: String)NoCardholder name.
.setCardExpiryMonth(_ month: String)YesExpiry month in "MM" format.
.setCardExpiryYear(_ year: String)YesExpiry year in "YY" format.
.setCVV(_ cvv: String)YesCard CVV.
@IBAction func payButtonTapped(_ sender: Any) {
    do {
        let card = try CFCardSubs.CFCardSubsBuilder()
            .setChannel("link")
            .setCardComponent(cfCardComponent)
            .setCardExpiryMonth(expiryMonthField.text ?? "")
            .setCardExpiryYear(expiryYearField.text ?? "")
            .setCardHolderName(cardHolderNameField.text ?? "")
            .setCVV(cvvField.text ?? "")
            .build()

        let payment = try CFCardSubsPayment.CFCardPaymentSubsBuilder()
            .setCard(card)
            .setSession(subsSession)
            .build()

        let paymentService = CFPaymentGatewayService.getInstance()
        paymentService.setCallback(self)
        try paymentService.doSubsPayment(payment, viewController: self)

    } catch {
        print(error.localizedDescription)
    }
}
There is no public API to read the card number out of CFSubsCardComponent. The SDK uses it securely when doSubsPayment is called.

Sample code

The following example shows a complete custom card component payment flow, including session creation, card component initialisation, metadata handling, and payment initiation.
The following example demonstrates a complete non-PCI card payment integration using CFSubsCardComponent:
class CheckoutViewController: UIViewController, CFSubsCardListener, CFResponseDelegate {

    @IBOutlet weak var cfCardComponent: CFSubsCardComponent!
    @IBOutlet weak var payButton: UIButton!

    private var subsSession: CFSubscriptionSession!

    override func viewDidLoad() {
        super.viewDidLoad()

        cfCardComponent.layer.borderColor = UIColor.systemGray4.cgColor
        cfCardComponent.layer.borderWidth = 1.0
        cfCardComponent.layer.cornerRadius = 8.0

        do {
            subsSession = try CFSubscriptionSession.CFSubscriptionSessionBuilder()
                .setSubscriptionId("your_subscription_id")
                .setSubscriptionSessionId("your_subscription_session_id")
                .setEnvironment(.SANDBOX) // or .PRODUCTION
                .build()

            cfCardComponent.initializeCardComponent(
                session: subsSession,
                card_listener: self,
                hint_text: "",
                font: UIFont.systemFont(ofSize: 16),
                textColor: UIColor.label,
                enable_pasting: false
            )

            let paymentService = CFPaymentGatewayService.getInstance()
            paymentService.setCallback(self)

        } catch {
            print(error.localizedDescription)
        }
    }

    func cardMetaData(card_listener_response: CFSubsCardListenerResponse) {
        let charCount = card_listener_response.numberOfCharacters ?? 0
        let meta = card_listener_response.meta_data ?? [:]
        let isLuhnValid = meta["luhn_check"] as? Bool ?? false
        payButton.isEnabled = (charCount == 16 && isLuhnValid)
    }

    @IBAction func payButtonTapped(_ sender: Any) {
        do {
            let card = try CFCardSubs.CFCardSubsBuilder()
                .setChannel("link")
                .setCardComponent(cfCardComponent)
                .setCardExpiryMonth(expiryMonthField.text ?? "")
                .setCardExpiryYear(expiryYearField.text ?? "")
                .setCardHolderName(cardHolderNameField.text ?? "")
                .setCVV(cvvField.text ?? "")
                .build()

            let payment = try CFCardSubsPayment.CFCardPaymentSubsBuilder()
                .setCard(card)
                .setSession(subsSession)
                .build()

            let paymentService = CFPaymentGatewayService.getInstance()
            try paymentService.doSubsPayment(payment, viewController: self)

        } catch {
            print(error.localizedDescription)
        }
    }

    func onError(_ error: CFErrorResponse, order_id: String) {
        print("Error: \(error.message ?? "")")
    }

    func verifyPayment(order_id: String) {
        print("Flow complete. Verify order: \(order_id)")
    }
}
For a working end-to-end implementation, refer to the sample integration on GitHub.

Step 3: Confirm the payment Server-side

After the SDK delivers a callback via verifyPayment, confirm the payment status from your backend before taking any action. The SDK callback signals only that the payment flow has ended. It does not guarantee a successful payment. Use the Fetch Details of All Payments of a Subscription API to retrieve the current payment status.
The following request fetches all payments for a subscription:
curl --request GET \
  --url https://api.cashfree.com/pg/subscriptions/{subscription_id}/payments \
  --header 'x-api-version: 2025-01-01' \
  --header 'x-client-id: <your-client-id>' \
  --header 'x-client-secret: <your-client-secret>'
A successful response returns an array of payment objects. Check the payment_status field to determine the outcome. The API returns the following response on success:
[
  {
    "cf_payment_id": "123456",
    "cf_subscription_id": "7891011",
    "payment_id": "your-payment-id",
    "payment_amount": 1,
    "payment_status": "SUCCESS",
    "payment_type": "AUTH",
    "payment_initiated_date": "2025-06-01T22:14:58+0530",
    "subscription_id": "your-subscription-id",
    "retry_attempts": 0,
    "failure_details": {
      "failure_reason": ""
    }
  }
]
Always verify the subscription status from your backend before delivering goods or services to the customer. Proceed only when payment_status is SUCCESS.

Error codes

If a required field or object is missing when you initiate payment, the SDK returns an error through the catch block or onError callback. The following table lists common SDK-level validation errors and when they occur.
The SDK validation errors are grouped by category as follows:

Session errors

These errors occur when the CFSubscriptionSession object or its required fields are not provided.
Error codeMessageDescription
SUBSCRIPTION_SESSION_OBJECT_MISSINGThe “CFSubscriptionSession” is missing in the request.The CFSubscriptionSession object was not passed to the payment builder.
SUBSCRIPTION_SESSION_ID_MISSINGThe “subscription_session_id” is missing in the request.The subscription_session_id was not set on the CFSubscriptionSession builder.
SUBSCRIPTION_ID_MISSINGThe “subscription_id” is missing in the request.The subscription_id was not set on the CFSubscriptionSession builder.
ENVIRONMENT_MISSINGThe “environment” is missing in the request.The Cashfree environment (.SANDBOX or .PRODUCTION) was not set on the CFSubscriptionSession builder.

Payment method object errors

These errors occur when a payment method object is absent or one of its required fields is not set.
Error codeMessageDescription
SUBSCRIPTION_CARD_OBJECT_MISSINGThe “CFCardSubs” object is missing in the request.The CFCardSubs object was not passed to the CFCardSubsPayment builder.

Callback errors

These errors occur when the payment callback is not registered before initiating payment.
Error codeMessageDescription
CALLBACK_MISSINGThe “callback” cannot be null.setCallback was not called on CFPaymentGatewayService before doSubsPayment(). Register the callback before you initiate payment.

Other options

The following optional configurations let you customise the payment screen behaviour.
To present the payment flow in full screen, call setFullScreen(true) on the payment object before you call doSubsPayment():
payment.setFullScreen(true)