This integration guide is for payment aggregators located outside India, who want to collect payments from Indian buyers (individuals and businesses) for their merchants located outside India. Please note that currently Xflow supports collections for only INR
invoices.
In this integration guide, we will provide step by step instructions on collecting payments via two payment methods : (i) local bank transfers and (ii) Unified Payments Interface or UPI.
Your merchants/users are called connected users in Xflow. To start collecting payments for your connected users, you must do the following.
As of now, you can completed the steps above through our Dashboard. We will shortly provide APIs for you to do the same.
Create a connected user account by passing basic details about your merchants as shown below. Your connected users do not have access to these accounts. The information provided as part of the connected user account is required for due diligence on the ultimate beneficiary of the transaction.
curl -L -X POST 'https://api.xflowpay.com/v1/accounts' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Content-Type: application/json' \
-d '{
"business_details": {
"date_of_incorporation": "2010-01-01",
"dba": "Marvelous",
"email": "finance@marvelousw.com",
"ids" : {
"business": "12-3456789"
},
"legal_name": "Marvelous Software Inc",
"merchant_category_code": "5734",
"merchant_size": "large",
"physical_address": {
"city": "Seattle",
"country": "US",
"line1": "Bldg. 3, Main Street",
"postal_code": "98103",
"state": "WA"
},
"product_category": "software",
"product_description": "We sell productivity softwares.",
"type": "company",
"website": "www.marvelous.com"
},
"nickname": "Marvelous Softwares",
"type": "user"
}'
{
"address": "requested",
"business_details": {
"date_of_incorporation": "2010-01-01",
"dba": "Marvelous",
"email": "finance@marvelousw.com",
"ids": {
"business": "12-3456789",
"tax": null,
"tax_deduction": null,
"tax_gst": null,
"tax_trade": null
},
"legal_name": "Marvelous Software Inc",
"merchant_category_code": "5734",
"merchant_size": "large",
"physical_address": {
"city": "Seattle",
"country": "US",
"line1": "Bldg. 3, Main Street",
"line2": null,
"postal_code": "98103",
"state": "WA"
},
"product_category": "software",
"product_description": "We sell productivity softwares.",
"type": "company",
"website": "www.marvelous.com"
},
"created": 1727424388,
"id": "account_F0A_1727424388583_DeEtl_000",
"link": null,
"livemode": false,
"logo_id": null,
"metadata": null,
"nickname": "Marvelous Softwares",
"object": "account",
"parent_account_id": "account_F0A_1726296734091_G7Zuq_000",
"supporting_documentation": null,
"status": "draft",
"sub_type": null,
"tos_acceptance": null,
"type": "user"
}
Explaining some of the key fields above.
type
: When you create a connected user, you are creating an account of type = user
. Your own account will be of type = platform
and sub_type = aggregator
.
business_details.nickname
: This is your connected user’s brand name. This must be unique among your connected users. Among other things, this field is used to create UPI handles for your merchants. This is covered further in the guide.
business_details.id.business
: This is a government issued identifier for the business (e.g. EIN in the US).
business_details.product_category
: This field is used to classify your merchants into goods,software and services.
It is useful to note that you can create an account for a connected user by passing only a subset of the mandatory information required to activate the account. This will allow you to pass information in a staged manner. Before initiating account activation, you will have to provide additional mandatory information. You can use the Update endpoint to provide additional information after the account is created. The mandatory information required as part of the account object is provided here.
If your connected user wishes to accept payments via UPI then they must accept NPCI's UPI Terms & Conditions. When your connected user accepts these terms, please capture the IP address, time of acceptance and the user agent details. This information must be passed in the tos_acceptance
hash on the account object. A UPI VPA will be created only if the information if tos_acceptance
has been provided.
Xflow does not support nor recommend cross-origin domain requests from the browser. If you make such requests from the browser, Xflow will return the CORS header 'Access-Control-Allow-Origin' missing
error code. If you encounter this error, you're likely calling an API endpoint that is not intended to be called by a web frontend. Or rather, it can't be safely called by your frontend, because it requires a secret API key, and you don't want to expose that key to clients. Please fix this issue by calling this API endpoint from your backend instead.
After you have created the connected user, you must now provide names of at least 2 directors for the connected user. This is done by creating a Person
object as shown below.
At this stage, we would like to introduce the header Xflow-Account
. The header Xflow-Account
indicates the connected user's account for which the call is being made. You should pass this header with the connected user's account identifier when taking action in the context of that specific connected user. In the example below, we have passed this header because we are creating the person for a specific connected user.
curl -L -X POST 'https://api.xflowpay.com/v1/persons' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"full_name": "Mike McGallan",
"relationship": {
"director": true
}
}'
{
"created": 1666078214,
"full_name": "Mike McGallan",
"id": "person_F0A_1666078214977_3Du5D_000",
"livemode": true,
"object": "person",
"relationship": {
"director": true,
"owner": false,
"representative": false
},
"status": "unverified",
"supporting_documentation": null,
"supportings_ids": null
}
Before you submit the connected user account for activation, you also need to provide connected user's bank account details. This information is used for regulatory reporting purposes only, and not for making any payouts directly to your merchants. To do this, you should create an Address object with bank account number and the SWIFT code as shown below.
If you do not have the SWIFT code, you can also provide the bank name in the field bank_account.bank_name
. For IBANs, instead of bank_account.number
use bank_account.iban
field.
curl -L -X POST 'https://api.xflowpay.com/v1/addresses' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d $'{
"bank_account": {
"global_wire": "USBKUS44XXX",
"number": "102940182401"
},
"billing_details": {
"city": "San Francisco",
"country": "US",
"line1": "786 West 16th Avenue",
"postal_code": "67ZB8",
"state": "WA"
},
"category": "user_payout",
"currency": "USD",
"linked_id": "account_F0A_1727424388583_DeEtl_000",
"linked_object": "account",
"name": "Marvelous Software Inc",
"type": "bank_account"
}'
{
"bank_account": {
"bank_name": null,
"domestic_credit": null,
"domestic_debit": null,
"domestic_fast_credit": null,
"domestic_wire": null,
"global_wire": "USBKUS44XXX",
"iban": null,
"last4": "2401",
"number": "102940182401"
},
"billing_details": {
"city": "San Francisco",
"country": "US",
"line1": "786 West 16th Avenue",
"line2": null,
"postal_code": "67ZB8",
"state": "WA"
},
"category": "user_payout",
"created": 1727424974,
"currency": "USD",
"id": "address_f0A_1727424974114_GMXp5_000",
"is_reusable": false,
"linked_id": "account_F0A_1727424388583_DeEtl_000",
"linked_object": "account",
"livemode": false,
"metadata": null,
"name": "Marvelous Software Inc",
"object": "address",
"status": "deactivated",
"supporting_documentation": null,
"type": "bank_account",
"vpa": null,
"wallet": null
}
Once you have completed all the steps above, you will now need to initiate the activation process by hitting the activate endpoint as shown below.
curl -L -X POST 'https://api.xflowpay.com/v1/accounts/account_F0A_1727424388583_DeEtl_000/activate' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json'
Post this, the connected user status will change to verifying
. At this stage, Xflow and its banking partner will evaluate the information and activate (status changes to activated
) or ask for additional information over email. The account is activated within 3 business days. You can subscribe to events related to account status account.status.*
to receive these updates. While a virtual bank account number is allocated immediately after activation, the UPI VPA is provided in 4 business days after activation. You can subscribe to the event address.status.activated
to know when the UPI id/vpa has been created for your connected user.
Before we proceed to the next steps, we will introduce 2 other objects associated with your account.
AccountSettingsFor every account of type = platform
and user
on Xflow there will be a corresponding AccountSettings
object. Key parameters on the AccountSettings
object are
address
hash which configures the default creation of INR VBANs (virtual bank account numbers) at different levels. Your account is configured with defaults such that every connected user will get an INR VBAN upon activation. However if you wish to create INR VBANs for every partner of the connected user then you can set the field address.connected_user_partner
to true. This helps the connected user with reconciliation provided they share the right VBAN details with the right partner. You will find similar settings on the connected user’s AccountSettings
object which enables you to have different experiences for different connected users.
The UPI VPAs (virtual payment address) creation is not controlled via AccountSettings
. A VPA is created by default for every connected user for whom the additional information stated on the Activate endpoint has been provided. You cannot create UPI VPAs at partner level.
payouts
At the moment, we will process all your payouts daily, but in future we will enable you to configure your payout cadence through the payout
hash. Since your connected users are not getting paid out, the payout
hash on connected user's AccountSettings
is set to null
.
There are 2 types of FeePlan
objects associated with your account.
type = account_fees_passthrough
: This FeePlan object holds the fees that will be applied on every transaction of your connected users. The fees are organised under the hash receivable_reconcile
. This FeePlan is inherited as-is on your connected user’s account for processing. The FeePlan on your connected user’s account is of type account_fees
. You cannot edit the FeePlans on your account or your connected user’s account.
type = account_fees
: This FeePlan object holds the fees that will be applied while making payouts to you. In essence, these fees are charged per payout, and not per connected user transaction. An example of this is the FX mark-up or SWIFT fees charged per payout that we make to your bank account. The fees are organised under payout
and payout_fx
hash. The FeePlan cannot be edited.
Your connected user’s customers are called partners in Xflow. A partner is modeled as an account of type = partner
within Xflow. Use the create endpoint to create an account of type = partner
. You need to create the account on behalf of your user, so remember to pass the Xflow-Account header populated with the user’s account identifier. Please note that you can create the partner later just before withdrawing the incoming funds.
curl -L -X POST 'https://api.xflowpay.com/v1/accounts' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"business_details": {
"email": "ops@shoepower.com",
"legal_name": "Shoe Power",
"physical_address": {
"country": "IN",
"line1": "788 Embassy Industrial Area",
"city": "Bangalore",
"state": "Karnataka",
"postal_code": "560065"
},
"type": "business"
},
"nickname": "Shoe Power",
"type": "partner"
}'
{
"address": "deactivated",
"business_details": {
"date_of_incorporation": null,
"dba": null,
"email": "ops@shoepower.com",
"ids": null,
"legal_name": "Shoe Power",
"merchant_category_code": null,
"merchant_size": null,
"physical_address": {
"city": "Bangalore",
"country": "IN",
"line1": "788 Embassy Industrial Area",
"line2": null,
"postal_code": "560065",
"state": "Karnataka"
},
"product_category": null,
"product_description": null,
"type": "business",
"website": null
},
"created": 1727426459,
"id": "account_F0A_1727426459005_jRi3o_000",
"link": null,
"livemode": false,
"logo_id": null,
"metadata": null,
"nickname": "Shoe Power",
"object": "account",
"parent_account_id": "account_F0A_1727424388583_DeEtl_000",
"supporting_documentation": null,
"status": "verifying",
"sub_type": null,
"tos_acceptance": null,
"type": "partner"
}
Unlike the connected user, activation of a partner is near instantaneous post creation and there is no separate call for activation required. However, given that this is still an asynchronous step, you can listen to the account.status.activated
event to determine when the partner account has been activated.
Please ensure that you are classifying your partners correctly as business
or individual
via the business_details.type
field.
If the partner is a business entity, and your connected user sells physical goods, then you must also provide the Importer Exporter Code (IEC) for the partner in business_details.ids.tax_trade
.
If you will be processing transaction size greater than INR 2.5L, then you must provide the PAN of the partner in the field business_details.ids.tax
A receivable is Xflow’s wrapper around the connected user’s invoice. You must create the receivable on behalf of your connected user against an activated partner by using the below curl snippet. Again, like partner, you can create the receivable later just before withdrawing the funds.
For creating a receivable, first create a File
object as shown below by using your invoice file which must be a .jpeg, .pdf or a .png file.
curl -L -X POST 'https://api.xflowpay.com/v1/files' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-F file=@"Inv_AB887.pdf" \
-F payload='{
"purpose": "finance_document"
}'
{
"created": 1675153265,
"file_name": "Inv_AB887.pdf",
"id": "file_F0A_1727427038355_ecoFL_000",
"livemode": false,
"metadata": null,
"object": "file",
"purpose": "finance_document",
"size": 493117,
"type": "pdf",
"url": "https://api.xflowpay.com/v1/files/file_F0A_1727427038355_ecoFL_000/contents"
}
Once you have the id of the File
object, you can create the receivable as shown below. The file id is passed in the field invoice.document
.
curl -L -X POST 'https://api.xflowpay.com/v1/receivables' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"account_id": "account_F0A_1727427042466_UKUbp_000",
"amount_maximum_reconcilable": "830000.00",
"currency": "INR",
"invoice": {
"amount": "830000.00",
"creation_date": "2022-10-18",
"currency": "INR",
"document": "file_F0A_1727427038355_ecoFL_000",
"due_date": "2022-11-07",
"reference_number": "Inv_30475"
},
"hsn_code": "85238090",
"purpose_code": "S0802",
"transaction_type": "software"
}'
{
"account_id": "account_F0A_1727427042466_UKUbp_000",
"amount_locked": null,
"amount_maximum_reconcilable": "830000.00",
"amount_reconcilable": null,
"amount_reconciled": null,
"amount_reconciled_not_settled": null,
"amount_settled_payouts": null,
"created": 1727427046,
"currency": "INR",
"deposit_ids_amount_locked": [],
"description": null,
"hsn_code": "85238090",
"id": "receivable_f0A_1727427046725_mjXJy_000",
"invoice": {
"amount": "830000.00",
"creation_date": "2022-10-18",
"currency": "INR",
"document": "file_F0A_1727427038355_ecoFL_000",
"due_date": "2022-11-07",
"reference_number": "Inv_30475"
},
"livemode": false,
"metadata": null,
"object": "receivable",
"purpose_code": "S0802",
"purpose_code_description": "Software consultancy/implementation",
"status": "draft",
"supporting_documentation": {
"documents": [],
"status": "not_applicable"
},
"system_message": [],
"transaction_type": "software"
}
After creating the receivable, you must confirm the receivable which submits the receivable for verification. Xflow will now verify and activate the receivable with an SLA of 2 hours. You can subscribe to the receivable.status.activated
event to know when a receivable has been activated.
For some receivables, Xflow will require additional inputs and will move your receivable to an Input Required status. You will be notified through webhook receivable.status.input_required
.
curl -L -X POST 'https://api.xflowpay.com/v1/receivables/receivable_f0A_1727427046725_mjXJy_000/confirm' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json'
Please note that,
Receivables with value > INR 25 Lakhs are not supported for transaction type software and services.
Receivables where an individual SKU is > INR 25 Lakhs are not supported for transaction type goods.
For goods receivable, you/your connected user must provide additional supporting documents (shipping and customs related) after the transaction has been settled. This is covered in detail in section 6.
Supported purpose codes for goods receivable are S0101
or S0102
.
For software receivable, you must use purpose code S0802
.
Supported purpose code for services receivable are S0803
S1005
S1009
S1010
S1013
S1015
S1016
S1017
S1105
S1106
Need support for a different purpose code, please reach out to support@xflowpay.com.
hsn_code
must be set to 85238090
. For goods transactions, the hsn_code
will be provided by your connected user.Once your connected user has been activated, you can start collecting payments for them. In this section, we will show you how to collect payments via local bank transfer.
Upon activation, an INR VBAN (also known as VA or Virtual Account) is automatically generated for your connected user. Think about this as the collection account that is specifically created for your connected user, i.e. one VBAN per connected user. Your connected users can share this VBAN with their customers in India to receive payments. This VBAN is modeled as an Address (see below) with category = xflow_receive
and type = bank_account
.
{
"bank_account": {
"bank_name": "ICICI Bank",
"domestic_credit": "ICICTMP3440",
"domestic_debit": null,
"domestic_fast_credit": "ICICTMP3440",
"domestic_wire": "ICICTMP3440",
"global_wire": null,
"iban": null,
"last4": "0400",
"number": "51744431540400"
},
"billing_details": null,
"category": "xflow_receive",
"created": 1709474909,
"currency": "INR",
"id": "address_f0A_1709474909321_p7bVk_000",
"is_reusable": false,
"linked_id": "account_F0A_1727424388583_DeEtl_000",
"linked_object": "account",
"livemode": false,
"metadata": null,
"name": null,
"object": "address",
"status": "activated",
"supporting_documentation": null,
"type": "bank_account",
"vpa" : null,
"wallet": null
}
India supports 3 types of bank transfer payment methods: NEFT, RTGS and IMPS. For using these bank transfer payment methods, the partner will need an account number and routing code which is called IFSC (Indian Financial System Code). The account number is provided in bank_account.number
field. The IFSC is provided in the following fields:
bank_account.domestic_credit
bank_account.domestic_fast_credit
bank_account.domestic_wire
Note: As of today, the IFSC is the same across all the payment methods.
Once funds arrive in the VBAN, a deposit object (see below) is created with the amount received. You should subscribe to the deposit.status.completed
event to know when a connected user has received a deposit.
{
"amount": "830000.00",
"created": 1666079759,
"currency": "INR",
"from": {
"account_id": "account_F0A_1727424388583_DeEtl_000",
"address_id": "address_f0A_28094133409111_X7bck_003"
},
"id": "deposit_f0A_1666079759165_P3FtR_000",
"livemode": true,
"metadata": null,
"object": "deposit",
"payment_method": "domestic_credit",
"payment_method_details": null,
"reason_code": null,
"statement_descriptor": null,
"status": "completed",
"to": {
"account_id": "account_F0A_1727424388583_DeEtl_000",
"address_id": "address_f0A_1709474909321_p7bVk_000"
}
}
The from.address_id
has the bank account details of the remitter along with remitter name. This Address is of type = bank_account
and category = partner_payment
. We recommend that you show the remitter details to your connected users to help them identify the source of funds.
The received funds will reflect in your connected user's balance. Please refer to the Balance object in the API reference, and look for balance.pending []
. Every acccount at Xflow, irrespective of the type, will have a Balance
object associated with it which shows the funds against the account. When you receive the funds, they reflect in the balance.pending
compartment. To withdraw funds, you will have to reconcile the funds against an activated receivable. Upon reconciliation, the funds move to balance.available
compartment, and are available to be paid out. We will look at reconciliation further in section 6 of the guide.
For testing, you will need a way to introduce some dummy funds into your integration. For this, you can use the below test-mode only Create Deposit API.
curl -L -X POST 'https://api.xflowpay.com/v1/deposits' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"amount": "830000.00",
"currency": "INR",
"from": {
"account_id": "account_F0A_1727424388583_DeEtl_000"
},
"payment_method": "domestic_credit",
"to": {
"account_id": "account_F0A_1727424388583_DeEtl_000",
"address_id": "address_f0A_1666079171474_6jxOu_000"
}
}'
At the time of processing payouts, our banking partner will try to match the incoming funds against outgoing transactions. Funds deposited by partner A cannot be withdrawn against an invoice from partner B, i.e. the funds are not fungible. To adhere to this regulatory constraint, you must ask your connected user to map the incoming funds to a partner by showing them the remitter details against each desposit. Here the mapping of funds to partner means, moving the funds from connected user balance to the partner level balance. To do this, you must use the Transfer API as shown below which moves funds instantly:
curl -L -X POST https://api.xflowpay.com/v1/transfers \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"from": {
"account_id": "account_F0A_1727424388583_DeEtl_000"
},
"linked_id": "deposit_f0A_1666079759165_P3FtR_000",
"linked_object" : "deposit",
"to": {
"account_id": "account_F0A_1727427042466_UKUbp_000"
},
"type": "deposit_transfer"
}'
{
"created": 1645104826,
"description": "Transfer for mapping funds to partner",
"from": {
"account_id": "account_F0A_1727424388583_DeEtl_000",
"amount": "830000.00",
"currency": "INR"
},
"id": "transfer_f0A_1912224718366_laE3R_000",
"linked_id": "deposit_f0A_1666079759165_P3FtR_000",
"linked_object" : "deposit",
"livemode": false,
"metadata": null,
"object": "transfer",
"status": "completed",
"to": {
"account_id": "account_F0A_1727427042466_UKUbp_000",
"amount": "830000.00",
"currency": "INR"
},
"type": "deposit_transfer"
}
It is important to note that Xflow and its banking partner will match the remitter name against the partner name at the time of settlement. Any significant deviation could lead to rejections. Hence its important to inform the connected users about mapping the desposits accurately to partners.
If your connected user has made an error in mapping, please drop a note to support@xflowpay.com with the deposit_id
that was incorrectly mapped. We will try to reverse this for you if it is not already submitted for settlement.
If you are not looking to collect via UPI, you can jump to section 6.
In this section, we will take a look at the step by step process of collecting payments using UPI in your own native checkout experience. Xflow supports both one-time payments and recurring payments for UPI.
When your connected user is activated, Xflow creates UPI handles or Virtual Payment Addresses (VPAs) for your merchant. These are modeled as addresses of type = vpa
and category = xflow_receive
. The VPA can be found in the field address.vpa.id
of the address
object. Using the values of the parameters onetime
and recurring
from the vpa
hash you will be able to know which VPA can be used for one-time payments and which VPA can be used for recurring payments such as those in subscriptions
. You will use these VPAs for collecting payment for your merchants.
{
"bank_account": null,
"billing_details": null,
"category": "xflow_receive",
"created": 1666079069,
"currency": "INR",
"id": "address_f0A_1666184962045_5J7h9_000",
"is_reusable": false,
"linked_id": "account_F0A_1666184956871_JsBZ2_000",
"linked_object": "account",
"livemode": true,
"metadata": null,
"name": "Example Business",
"object": "address",
"status": "activated",
"supporting_documentation": null,
"type": "vpa",
"vpa": {
"id": "merchantname-aggregatorname-cb@aicici.com",
"type": "upi",
"upi": {
"onetime": "enabled",
"recurring": "disabled"
},
},
"wallet": null
}
You can subscribe to the event address.status.activated
to know when the UPI ID/VPAs have been created for your connected user.
TransactionIntent
is the central object that enables collection through UPI via checkout. When your user selects UPI and proceeds to payment in your checkout experience, you will create the TransactionIntent
object as below.
curl -L -X POST 'https://api.xflowpay.com/v1/transaction_intents' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d $'{
"amount": "8000.00",
"currency": "INR",
"payment_method": "upi",
"payment_method_details": {
"upi":{
"flow": "intent"
}
},
"to": {
"account_id": "account_f0A_1646922461210_X7q23_000"
},
"type": "payment"
}'
{
"amount": "8000.00",
"created": 1666079293,
"currency": "INR",
"id": "transaction_intent_f0A_1663339292344_qFGAH_000",
"livemode": true,
"metadata": null,
"object": "transaction_intent",
"payment_method": "upi",
"payment_method_details": {
"upi": {
"flow": "intent",
"intent_url": "upi://pay?pa=<merchant VPA>&pn=<merchant name>&tr=<refid>&am=<amount>&cu=INR&mc=<MCC code>"
}
},
"status": "successful",
"subscription_id": "subscription_f0A_1673345732344_oMLAH_000",
"to": {
"account_id": "account_f0A_1646922461210_X7q23_000"
},
"type": "payment",
"validity": 1666084000
}'
The TransactionIntent
object above provides you with the UPI intent URL (transaction_intent.payment_method_details.upi.intent_url
) which can be used to create a QR code or redirect the partner to a UPI payment app of their choice. The process of enabling these experiences for your payers across different operating systems and front-end types is outlined next. Please note that the TransactionIntent
can only be created with currency = INR
Users can scan a QR code on your front-end interface to make a payment using their UPI-enabled payment service provider app. You can enable this as follows:
Step 1: Choose a Library for QR Code Generation
qrcode.js
or qr-code-styling
. For React, use a library like qrcode.react
to generate QR codes.react-native-qrcode-svg
or react-native-qrcode-generator
. For Flutter, use the qr_flutter
package to generate QR codesStep 2: Using the UPI intent URL (payment_method_details.intent_url
) and the chosen library, integrate the library as follows:
var qrcode = new QRCode(document.getElementById("qrcode"), {
text: "upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode",
width: 256,
height: 256,
});
import QRCode from 'react-native-qrcode-svg';
<QRCode
value="upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode"
size={256}
/>
The library takes the UPI URL string you provided and generates a QR code based on that data.
The QR code is then rendered in the specified location within your application's UI (like a div
in web or a View
in React Native)
import React from 'react';
import QRCode from 'qrcode.react';
const PaymentQRCode = () => {
const upiUrl = "upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode";
return (
<div>
<h2>Scan to Pay</h2>
<QRCode
value={upiUrl}
size={256}
level={"H"}
includeMargin={true}
/>
</div>
);
};
export default PaymentQRCode;
value
is the UPI URL string. size
sets the dimensions of the QR code. level
sets the error correction level (L, M, Q, H). includeMargin
adds margin around the QR code.
Step 3 : Displaying the QR Code
div
with an ID of qrcode
.<div id="qrcode"></div>
The QR code will be displayed in this div
<QRCode
value="upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode"
size={256}
/>
The QR code will appear where you place the <QRCode />
component.
import React from 'react';
import PaymentQRCode from './PaymentQRCode';
const App = () => {
return (
<div className="App">
<h1>Make a Payment</h1>
<PaymentQRCode />
</div>
);
};
export default App;
Step 4 : Consuming the QR Code
You can redirect your users to their preferred payment service provider app (such as Google Pay, PhonePe) when they click a button or similar UI element. You can power this as follows:
Step 1: Handle redirection across platforms
function redirectToPSP() {
window.location.href = "upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode";
}
This will attempt to open the UPI app installed on the device
iOS: iOS will handle the UPI URL schema (payment_method_details.intent_url
) natively and open the relevant app if its installed. If you need to handle different behaviors for different PSP apps, you might use universal links for apps that support them. However, for standard UPI, the payment_method_details.intent_url
scheme should work across apps
Android: Android handles the payment_method_details.intent_url
schema natively. Most UPI PSP apps (Google Pay, PhonePe) will automatically detect the UPI intent and process it.
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("upi://pay?pa=merchantVPA&pn=merchantName&tr=Refid&am=amount&cu=INR&mc=MCCcode"));
startActivity(intent);
By following these steps, you can create a smooth and robust experience for users across different platforms (iOS, Android) and PSP apps (Google Pay, PhonePe etc.)
When the partner makes the payment, the transaction_intent.status
will change to successful
. You must subscribe to the event transaction_intent.status.successful
to track this change. The partner can make multiple payment attempts against the TransactionIntent
till its status changes to expired
or successful
. The underlying attempts are tracked via the Transaction
object. When an underlying Transaction
succeeds, the TransactionIntent
goes to successful
, else stays in processing
. You can list all Transaction
s associated with the TransactionIntent
as follows.
curl -L -X GET 'https://api.xflowpay.com/v1/transactions?linked_id=transaction_intent_f0A_1716217802597_jZR51_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"'
For successful payments, a Deposit
object is created. This deposit will increase the INR balance against the account_id
that was passed during the Create TransactionIntent call. You can retrieve the Balance for an account as shown below. To reconcile the balance you will have to map the incoming deposit amount to the partner as outlined in section 4.2.
curl -L -X GET 'https://api.xflowpay.com/v1/balance?account_id=account_F0A_1666077553446_41Hgq_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"'
You can now go to section 6, to see how to withdraw the received funds.
If you are looking to collect recurring payments for subscriptions, then you must create a Subscription
. The Subscription
object allows you to define the terms of Subscription
like - how much to debit, when to debit, till when to debit and so on. See the create Subscription
request and response below.
curl -L -X POST 'https://api.xflowpay.com/v1/subscriptions' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d $'{
"amount": 500.00,
"currency": "INR",
"execution_anchor": 14,
"interval": "monthly",
"payment_method": "upi",
"payment_method_details": {
"upi": {
"flow": "intent"
}
},
"to": {
"account_id": "account_f0A_1646922461210_X7q23_000"
},
"validity_start_date": "2024-10-08",
"validity_end_date": "2025-10-08",
}'
{
"amount": 500.00,
"created": 1666079293,
"currency": "INR",
"execution_anchor": 14,
"from": {
"address_id": "address_F0A_1666079163481_A7fje_000"
},
"id": "subscription_f0A_1673345732344_oMLAH_000",
"interval": "monthly",
"livemode": true,
"metadata": null,
"object": "subscription",
"payment_method": "upi",
"payment_method_details": {
"upi": {
"flow": "intent"
}
},
"status": "active",
"to": {
"account_id": "account_f0A_1646922461210_X7q23_000"
},
"validity_end_date": "2025-10-08",
"validity_start_date": "2024-10-08"
}
Refer to the API Reference to understand how you can use fields in the Subscription
hash above to create a Subscription
plan of your choice.
Once you create the Subscription
, a corresponding TransactionIntent
of type=authorize_subscription
is automatically created. You can get this TransactionIntent
as follows
curl -L -X GET 'https://api.xflowpay.com/v1/transaction_intents?subscription_id=subscription_f0A_1673345732344_oMLAH_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"'
On the TransactionIntent
object, the field transaction_intent.payment_method_details.upi.intent_url
will have the URL which you must use to generate a QR code or to redirect the partner to their UPI apps. The partner must authorize the Subscription
in their app which will update the status
of TransactionIntent
and Subscription
to successful
and activated
respectively.
Once the Subscription
is activated Xflow will automatically execute the Subscription
and auto-debit the partner’s account at the interval and anchor that you have set. For each Subscription
execution, a TransactionIntent
is created with type = payment
. Please note that when the amount to debit is greater than 15000/- INR the payer will have to authenticate the collect request on their UPI application for every debit. This is required as per NPCI guidelines.
Xflow will automatically trigger a notification from the bank informing the partner of an upcoming debit at least 24 to 48 hours prior to the debit time (for interval=daily
it will be triggered 24 hours in advance). This step is mandated regulatorily and Xflow will do this on your behalf.
Similar to one-time payment, a Deposit
object is created once the transaction_intent.status
of the TransactionIntent
that is executing the Subscription
goes to successful
. You must subscribe to the event transaction_intent.status.successful
to track when the transaction_intent.status
goes to successful
.
This deposit will increase the INR balance against the account_id
that was passed during the Create Subscription call. You can retrieve the Balance for an account as shown below. To reconcile the balance you will have to map the incoming deposit amount to the partner as outlined in section 4.2.
curl -L -X GET 'https://api.xflowpay.com/v1/balance?account_id=account_F0A_1666077553446_41Hgq_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"'
You can use the Revoke Subscription endpoint to revoke a subscription as you see fit. The payer will also have an option to pause and resume the subscription at their will using their PSP application. If the Subscription is paused and then only resumed on the day of the execution, that particular execution will be skipped since regulatorily Xflow has to send a Pre-Debit notification atleast 24 hours in advance. You can subscribe to the event subscription.status.paused
to track this.
At this stage, you have an activated receivable, and you have moved funds to the right partner level balance. You are now all set to withdraw the funds.
:::note The funds must be withdrawn within 10 days from the deposit day else they will be returned to the partner. If there are genuine reasons for delay, you can drop a note to support@xflowpay.com :::
All withdrawals at Xflow must be against an activated receivable. This is done by reconciling the receivable against the pending balance. You will do this by hitting the reconcile endpoint on the Receivable object.
curl -L -X POST 'https://api.xflowpay.com/v1/receivables/receivable_f0A_1727427046725_mjXJy_000/reconcile' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"amount": "830000.00"
}'
Please note that the amount passed in the receivable command is in the currency of the receivable. Furthermore, we support partial reconciliation which means you can reconcile the receivable for a lower value than the actual receivable value (e.g. advance payment use-case).
Once reconciliation is complete, Xflow will initiate a payout to your bank account. All your reconciliations are aggregated into different payouts as per following logic:
{
"amount": "10000.00",
"arrival_date": 1675143082,
"automatic": true,
"created": 1666160776,
"currency": "USD",
"id": "payout_f0A_1666160776922_Q1hjW_000",
"livemode": true,
"metadata": null,
"object": "payout",
"payment_method": "global_wire",
"payment_method_details": null,
"payout_confirmation": null,
"statement_descriptor": "XFLOW PAYOUT F0A-1666160776922-J2uic-000",
"status": "settled",
"to": {
"account_id": "account_F0A_357987759890_X7lb0_056",
"address_id": "address_f0A_357944459890_fJSRH_080"
}
}
Once the payout goes to settled
status, we will update payout_confirmation
with MT103 or an equivalent document from our bank within 1 business day. To track the status of a payout, please listen to payout.status.settled
event.
The USD or the Foreign currency amount on the Payout
object is populated only after the Payout
goes to settled
state. At this stage, an ExchangeRates object is created which helps you understand how we arrived at the USD amount. We will explain ahead how to fetch this ExchangeRate
object.
You will be able to unpack each Payout
to understand which receivables funded the particular payout. To do this, you will need to look at the Payment object. At Xflow, the underlying real world money movement is modelled via the Payment
objects of different types. As an example, the Payout
object has an underlying Payment
of type = payout
. Let’s retrieve this Payment
of type = payout
.
curl -L -X GET 'https://api.xflowpay.com/v1/payments?linked_id=payout_f0A_1666160776922_Q1hjW_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
{
"created": 1666160776,
"fee_plan_id": "fee_plan_F0A_1666077553446_a8sn4_000",
"from": {
"amount": "830000.00",
"currency": “INR”
},
"id": "payment_f0A_1666160776922_DkBou_000",
"is_exchange_rate_applicable": false,
"linked_id": "payout_f0A_1666160776922_Q1hjW_000",
"linked_object": "payout",
"linked_payments": [],
"linked_payments_file_id": "file_F0A_1666078140654_lQm8l_000",
"livemode": true,
"object": "payment",
"payout_eligible_at": null,
"to": {
"amount": "10000.00",
"currency": “USD”
},
"type": "payout"
}
Before we unpack the Payout
further, remember the ExchangeRate
object we mentioned earlier. The payment_id
field on the ExchangeRate
object will be of the Payment
object of type = payout
. This is how you will be able to link an ExchangeRate
to Payout
.
On the Payment
object of type payout
you will find linked_payments_file_id
. This file provides list of transfer
payments associated with the payout
. These transfer
payment moves the funds from your connected user to your account after reconciliation. These transfer
payments are linked backwards to another Payment
of type = funds_debit
which is further backwards linked to Payment
of type = reconcile
. The chain looks something like this
payout
--> transfer
(gives you the connected user) -->funds_debit
(gives Receivable ID) --> reconcile
(gives reconcile events).
Each Payment provides additional information through fields linked_id
and linked_object
.
Let’s focus on the transfer
first. Since the payout made to you is an aggregation of several transaction
s at connected user level, the payout
payment is backwards linked to multiple transfer
payments. This is the reason, we provide this linkage via a File
object. Pick one of the transfer
payment from the file, and let’s fetch this object as shown below.
curl -L -X GET 'https://api.xflowpay.com/v1/payments/payment_f0A_1666160776922_DkBou_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
{
"created": 1666160776,
"fee_plan_id": "fee_plan_F0A_1666077553446_a8sn4_000",
"from": {
"amount": "830000.00",
"currency" : "INR"
},
"id": "payment_f0A_1666160776922_DkBou_000",
"is_exchange_rate_applicable": false,
"linked_id": "transfer_f0A_1666261130393_wuH6k_000",
"linked_object": "transfer",
"linked_payments": [
{
"account_id": "account_F0A_1727424388583_DeEtl_000",
"payment_id": "payment_f0A_1666160779999_ABCou_000",
"payment_type": “funds_debit
}
],
"linked_payments_file_id": null,
"livemode": true,
"object": "payment",
"payout_eligible_at": null,
"to": {
"amount": "830000.00",
"currency" : "INR"
},
"type": “transfer”
}
This transfer
has top-level linked_object
of type Transfer. From the Transfer
object, you will the get connected user’s account (field: from.account_id
).
Now let's focus on the linked_payments
hash on the transfer
payment. You will see that the linked_payments.payment_id
is of type funds_debit
. Since there is 1:1 relationship between transfer
and funds_debit
, the linkage is directly provided on the object itself. Let’s fetch this funds_debit
payment
curl -L -X GET 'https://api.xflowpay.com/v1/payments/payment_f0A_1666160779999_ABCou_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
{
"created": 1666160776,
"fee_plan_id": "fee_plan_F0A_1666077553446_a8sn4_000",
"from": {
"amount": "830000.00",
"currency": "INR"
},
"id": "payment_f0A_1666160779999_ABCou_000",
"is_exchange_rate_applicable": false,
"linked_id": "receivable_f0A_1727427046725_mjXJy_000",
"linked_object": "receivable",
"linked_payments": [
{
"account_id": "account_F0A_1727424388583_DeEtl_000",
"payment_id": "payment_f0A_1666160779999_PQCou_000",
"payment_type": "reconcile"
}
],
"linked_payments_file_id": null,
"livemode": true,
"object": "payment",
"payout_eligible_at": null,
"to": {
"amount": "830000.00",
"currency": "INR"
},
"type": "funds_debit"
}
The linked_id
on the funds_debit
object will provide you the receivable that was reconciled. This way, you will be able to get all the receivables that contributed to the payout.
If you support partial reconciliation, and are looking to get to a specific reconcile event, then use the field linked_payment.payment_id
which will give you the payment
of type = reconcile
. Thus, you can traverse upstream from Payment
of type = payout
to a Payment
of type = reconcile
.
Though rare, it is possible that some of your connected user's transactions are rejected by the banking partner. The individual transactions that are rejected can be identified through payment of type unfunds_debit
present on the file
object provided in linked_payments_file_id
field on the payout
payment. On the unfunds_debit
object look for the hash linked_payments
. Within this hash look for linked_payment.payment_id
of type = transfer
, and go further upstream from transfer
to get the funds_debit
and reconcile
to know which receivable and reconciliation was impacted. We will work with our banking partner to return the funds to the partner, and provide you and offline confirmation about these reversed transactions.
If you have just reconciled a goods receivable, you will notice that the supporting_documentation.status
has turned to pending
status. Within 10 business days, you/your connected user must submit the relevant documents to complete the regulatoty requirements. Following documents will be required
For B2B transactions : Shipping document (Airway Bill or Bill of lading) and Customs document (Bill of Entry or BOE). The BOE is available with the importer (your connected user's partner).
For B2C transactions : Shipping document (Airway Bill or Bill of lading).
These supporting documents are modeled as the File
object. For both shipping and customs document, create the File
object with purpose = transactional_document
. In response, you will receive a unique file identifier which must be used on the receivable object which will be used for updating the receivable.
curl -L -X POST 'https://api.xflowpay.com/v1/files' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-F file=@"AirwayBill.pdf" \
-F payload='{
"purpose": "transactional_document"
}'
{
"created": 1675153265,
"file_name": "Form15CA.pdf",
"id": "file_F0A_1675153265850_IREzx_000",
"livemode": false,
"metadata": null,
"object": "file",
"purpose": "transactional_document",
"size": 493117,
"type": "pdf",
"url": "https://api.xflowpay.com/v1/files/file_F0A_1675153265850_IREzx_000/contents"
}
Once you have created the files, you must now update the receivable. For updating the receivable, you must now mention the type of document as shipping
or customs
as shown below.
curl -L -X POST 'https://api.xflowpay.com/v1/receivables/receivable_f0A_1727427046725_mjXJy_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Content-Type: application/json' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-d '{
"supporting_documentation": {
"documents": [
{
"file_id": "file_F0A_1666078140654_lQm8l_000",
"reconcile_id": "payment_f0A_1666160779999_PQCou_000",
"type":"shipping"
},
{
"file_id": "file_F0A_1576078140852_lbKll_020",
"reconcile_id": "payment_f0A_1666160779999_PQCou_000",
"type":"customs"
}
]
}
}'
The field reconcile_id
is the identifier for the reconciliation action you performed in step 6.1. This is the id
on the Payment
of type = reconcile
which we introduced in section 6.2.
curl -L -X GET 'https://api.xflowpay.com/v1/payments?type=reconcile&linked_id=receivable_f0A_1727427046725_mjXJy_000' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
The above List API call will return a list of Payment
object of type = reconcile
. If you have multiple reconciliations on a receivable, you will have to pick the appropriate reconcile
object because your documents are submitted against each outgoing payment. This is the final step of the end-to-end flow. Also note that your users can provide the document at the time of reconciliation if it's available as follows.
curl -L -X POST 'https://api.xflowpay.com/v1/receivables/receivable_f0A_1727427046725_mjXJy_000/reconcile' \
-H 'Authorization: Bearer sk_your_key' \
-H 'Xflow-Account: "{{Connected_User_Account_ID}}"' \
-H 'Content-Type: application/json' \
-d '{
"amount": "830000.00"
"supporting_documentation": {
"documents": [
{
"file_id":"file_F0A_1675153265850_IREzx_000",
"type": "shipping"
}
]
}
}'
After you have provided the required documents, the supporting_documentation.status
will transition to completed
. Please reach out to support@xflowpay.com if you have challenges in providing the documents within the provided time. Without a legitimate reason, delaying document submission may lead to transactions getting blocked for your connected users. Note that, it is quite likely that the BOE is not available within 10 business days due to delays in shipment and custom clearance. We will manage such events with our banking partner upon receipt of communication from your side.
You can use the testmode to simulate payments to test your integration. In testmode, You will be able to mimic livemode without actually moving real money. The advantage here is that if your integration works in testmode, you can have confidence that livemode will work once you decide to flip the switch. Please note that data from the testmode will not be carried over to the livemode. To make your testmode more productive, we provide additional testmode specific features:
type=vpa
for your connected user. This will be a dummy address that you cannot pay to. You can do this via the API or from the connected user dashboard. You will see a button Create a Deposit which will instantaneously create a VPA id for the connected userTransactionIntent
is created for any amount other than INR 116, a Transaction
will instantaneously be created against it and will go to successful
status. Immediately after, the TransactionIntent
's status will also go to successful
TransactionIntent
is created for an amount of INR 116, a Transaction
will immediately be generated against it and will go to failed
status. Immediately after, the TransactionIntent
's status will go to expired
Subscription
is created for any amount other than INR 116, Xflow will create a TransactionIntent
of type=authorize_subscription
, a successful Transaction
against the TransactionIntent
, transition the TransactionIntent
object to successful
status and transition the Subscription
to activated
statusSubscription
is created for an amount of INR 116, Xflow will create a TransactionIntent
of type=authorize_subscription
, create a Transaction
against the TransactionIntent
which will transition to failed
status, the TransactionIntent
will go to expired
status post the expiration of validity and the Subscription
will stay in draft status foreverSubscription
creation, based on the interval
and the validity
chosen by you, Xflow will automatically carry out a maximum of 5 executions of the Subscription
2 mins apart and create TransactionIntent
and Transactions
for the same against the Subscription
That concludes our integration guide. We will be happy to get on a call and help you integrate faster. Just drop a note to support@xflowpay.com