This WhatsApp Payment API feature is only available for WhatsApp businesses operating in India, catering specifically to their Indian customer base. Please note that access to this functionality is limited to transactions within the Indian region from Meta.
You can now send the Payment Message (order_details) and your business can enable customers to pay for their orders using all the UPI Apps installed on their devices or via Credit cards or Internet Banking via WhatsApp.
A WhatsApp Payment flow is a user-initiated conversation that is initiated by the user or available in the existing chatbot flow and sent as a free-form message.
Businesses can send customers invoice (order_details) messages, and then get notified about payment status updates via webhook notifications from Payment Gateway or Exotel (via Meta) based on the Payment method you choose to integrate.
Based on particular use cases, businesses can collect payment from customers using one of the following integrations.
In case you want to support all the types of payment methods to your end users, then we would suggest you proceed ahead with the Payment Gateway method with preferred PGs (RazorPay or PayU) as natively supported by Meta. Other PGs would require a deep integration based on the requirement.
The below comparison will help you understand the differences and proceed accordingly.
User Experience:
User Experience |
UPI Intent Mode |
Payment Gateway Deep Integration Mode |
Native support for "Other payment methods" Eg: Netbanking, cards, wallets |
❌ Alternative: Send payment links |
✅ |
Native support for UPI Intent |
✅ |
✅ |
Native Payment Status Notification |
❌ |
✅ |
Integration Features |
UPI Intent Mode |
Payment Gateway Deep Integration Mode |
Refunds from WhatsApp APIs |
❌ |
✅ |
Payment Status from WhatsApp webhooks |
❌ |
✅ |
Note: In the case of UPI Intent payment flow, the payment success & failure need to be integrated with the VPA's payment switch or the Payment Gateway to whomever the VPA IDs belong to. Exotel (via Meta) will not be able to send any callbacks natively and will require an integration to be done.
Before you start sending the Message with the order details and payment button, you need to complete the setup and follow the steps:
Please go through our detailed WhatsApp Payments Integration Guide here.
To send messages to a single number with configured Flow button content via Exotel API, make an HTTP POST request to:
https://<your_api_key>:<your_api_token><subdomain>/v2/accounts/<your_sid>/messages
<your_api_key>
and <your_api_token>
with the API key and token created by you.<your_sid>
with your “Account sid".<subdomain>
with the region of your account
<your_api_key>
, <your_api_token>
and <your_sid>
are available in the API settings page of your Exotel Dashboard
The following are the POST parameters -
Payment Interactive Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
type | String | Optional | The type of interactive message you want to send. Supported values:button: Use it for Reply Buttons. list: Use it for List Messages. |
header | HeaderObject | Optional | Header content displayed on top of a message. If a header is not provided, the API uses an image of the first available product as the header |
body | BodyObject | Mandatory | Optional for type product. Required for other message types. |
footer | FooterObject | Optional | An object with the footer of the message. The object contains the following fields: text string |
action | ActionObject | Mandatory | An action object you want the user to perform after reading the message. This action object contains the following fields: name string Must be "review_and_pay"PaymentParametersObject Refer to PaymentParametersObject for details on the fields |
Payment Parameters Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
reference_id | string | Mandatory | Unique identifier for the order or invoice provided by the business. This cannot be an empty string and can only contain English letters, numbers, underscores, dashes, or dots, and should not exceed 35 characters. The reference_id must be unique for each order_details message for the same business. If the partner would like to send multiple order_details messages for the same order, invoice, etc. it is recommended to include a sequence number in the reference_id (for example, -) to ensure reference_id uniqueness. |
type | string | Mandatory | The type of goods being paid for in this order. supported options are digital-goods physical-goods |
beneficiaries | PaymentBeneficiariesObject | Mandatory/Optional | Mandatory for shipped physical-goods. See PayementBeneficiariesObject |
payment_type | string | Mandatory | Must be "payment_gateway" |
payment_configuration | string | Mandatory | The name of the pre-configured payment configuration to use for this order and must not exceed 60 characters. |
currency | string | Mandatory | The currency for this order. Currently the only supported value is INR. |
total_amount object |
PaymentAmountObject | Mandatory | The total_amount object Refer to PaymentAmountObject for details on the parameters total_amount.value must be equal to order.subtotal.value + order.tax.value + order.shipping.value - order.discount.value. |
object | PaymentParametersOrderObject | Mandatory | See PaymentParametersOrderObject for details on this object |
Payment Parameter Items Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
retailer_id | string | Mandatory | Unique identifier for an item in the order. |
name | string | Mandatory | The item’s name to be displayed to the user. Cannot exceed 60 characters |
amout | string | Mandatory | The price per item Refer to PaymentAmount object |
sale_amount | AmountObject | Optional | The discounted price per item. This should be less than the original amount. If included, this field is used to calculate the subtotal amount |
quantity | string | Mandatory | The number of items in this order, this field cannot be decimal has to be integer. |
country_of_origin | string | Mandatory/Optional | Required if catalog_id is not present. Name of the importer company |
importer_name | string | Mandatory/Optional | Required if catalog_id is not present. Name of the importer company |
importer_address | string | Mandatory/Optional | Required if catalog_id is not present. Name of the importer company |
Payment Amount Object
Parameter Name | Parameter Type | Mandatory/Optional | Value |
offset | Integer | Mandatory | Must be 100 for INR |
value | Integer | Mandatory | Positive integer representing the amount value multiplied by offset. For example, ₹12.34 has value 1234. |
Payment Beneficiaries Object (Required for shipped physical-goods)
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
name | string | Mandatory | Name of the individual or business receiving the physical goods. Cannot exceed 200 characters |
address_line1 | string | Mandatory | Shipping address (Door/Tower Number, Street Name etc.). Cannot exceed 100 characters |
address_line2 | string | Optional | Shipping address (Landmark, Area, etc.). Cannot exceed 100 characters |
city | string | Mandatory | Name of the city |
state | string | Mandatory | Name of the state |
country | string | Mandatory | Support values India |
postal_code | string | Mandatory | 6-digit zipcode of shipping address. |
Payment Sub Total Amount Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
offset | Integer | Mandatory | Must be 100 for INR |
value | Integer | Mandatory | Positive integer representing the amount value multiplied by offset. For example, ₹12.34 has value 1234. |
Payment Tax Amount Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
offset | Integer | Mandatory | Must be 100 for INR |
value | Integer | Mandatory | Positive integer representing the amount value multiplied by offset. For example, ₹12.34 has value 1234. |
description | string | Optional | Max character limit is 60 characters |
Payment Shipping Cost Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
offset | Integer | Mandatory | Must be 100 for INR |
value | Integer | Mandatory | Positive integer representing the amount value multiplied by offset. For example, ₹12.34 has value 1234. |
description | string | Optional | Max character limit is 60 characters |
Payment Discount Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
offset | Integer | Mandatory | Must be 100 for INR |
value | Integer | Mandatory | Positive integer representing the amount value multiplied by offset. For example, ₹12.34 has value 1234. |
description | string | Optional | Max character limit is 60 characters |
Payment Expiration Object
Parameter Name | Parameter Type | Mandatory/Optional | Parameter Description |
timestamp | string | Mandatory | UTC timestamp in seconds of time when order should expire. Minimum threshold is 300 seconds |
description | string | Optional | Text explanation for expiration. Max character limit is 120 characters |
curl --location 'https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages' \ --header 'Content-Type: application/json' \ --data '{ "whatsapp": { "messages": [ { "from": "9199XXXXXXXX", "to": "9190XXXXXXXX", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "link": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Home_made_sour_dough_bread.jpg/640px-Home_made_sour_dough_bread.jpg" } }, "body": { "text": "Click on the Pay Now button to complete the order" }, "footer": { "text": "Thank You!" }, "action": { "name": "review_and_pay", "parameters": { "reference_id": "57848", "type": "digital-goods", "payment_type": "payment_gateway:razorpay", "payment_configuration": "XXXXXXXX", "currency": "INR", "total_amount": { "value": 1000, "offset": 100 }, "order": { "status": "pending", "items": [ { "retailer_id": "1234567", "name": "Bread", "amount": { "value": 1400, "offset": 100 }, "sale_amount": { "value": 1000, "offset": 100 }, "quantity": 1 } ], "subtotal": { "value": 1000, "offset": 100 }, "tax": { "value": 100, "offset": 100, "description": "GST Inclusive" }, "shipping": { "value": 0, "offset": 100, "description": "Via Postal" }, "discount": { "value": 100, "offset": 100, "description": "For Premium Customers", "discount_program_name": "optional_text" } } } } } } } ] } }'
import requests import json url = "https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages" payload = json.dumps({ "whatsapp": { "messages": [ { "from": "9172XXXXXXXX", "to": "9190XXXXXXXX", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "link": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Home_made_sour_dough_bread.jpg/640px-Home_made_sour_dough_bread.jpg" } }, "body": { "text": "Click on the Pay Now button to complete the order" }, "footer": { "text": "Thank You!" }, "action": { "name": "review_and_pay", "parameters": { "reference_id": "57848", "type": "digital-goods", "payment_type": "payment_gateway:razorpay", "payment_configuration": "Checking-paymentstest", "currency": "INR", "total_amount": { "value": 1000, "offset": 100 }, "order": { "status": "pending", "items": [ { "retailer_id": "1234567", "name": "Bread", "amount": { "value": 1400, "offset": 100 }, "sale_amount": { "value": 1000, "offset": 100 }, "quantity": 1 } ], "subtotal": { "value": 1000, "offset": 100 }, "tax": { "value": 100, "offset": 100, "description": "GST Inclusive" }, "shipping": { "value": 0, "offset": 100, "description": "Via Postal" }, "discount": { "value": 100, "offset": 100, "description": "For Premium Customers", "discount_program_name": "optional_text" } } } } } } } ] } }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
curl --location 'https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages' \ --header 'Content-Type: application/json' \ --data '{ "status_callback": "https://webhook.site/90XXXXXX", "whatsapp": { "messages": [ { "from": "+9180XXXXXXXX", "to": "+9186XXXXXXXX", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "link": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Home_made_sour_dough_bread.jpg/640px-Home_made_sour_dough_bread.jpg" } }, "body": { "text": "Click on the Pay Now button to complete the order" }, "footer": { "text": "Thank You!" }, "action": { "name": "review_and_pay", "parameters": { "type": "digital-goods", "currency": "INR", "reference_id": "1033XXXX", "payment_type": "upi", "payment_configuration": "XXXXXXXX", "total_amount": { "value": 1100, "offset": 100 }, "order": { "status": "pending", "items": [ { "retailer_id": "12345678", "name": "Bread", "amount": { "value": 1500, "offset": 100 }, "sale_amount": { "value": 1000, "offset": 100 }, "quantity": 1 } ], "subtotal": { "value": 1000, "offset": 100 }, "tax": { "value": 100, "offset": 100, "description": "GST Inclusive" }, "shipping": { "value": 100, "offset": 100, "description": "Via Postal" }, "discount": { "value": 100, "offset": 100, "description": "For Premium Customers", "discount_program_name": "optional_text" } } } } } } } ] } }'
HTTP Response:
{ "request_id": "b434e92XXXXXXXXXXXXX", "method": "POST", "http_code": 202, "metadata": { "failed": 0, "total": 1, "success": 1 }, "response": { "whatsapp": { "messages": [ { "code": 202, "error_data": null, "status": "success", "data": { "sid": "2FdiXXXXXXXXXXXXXXXXX", } } ] } } }
The following are the response parameters -
Parameter | Type | Mandatory/Optional | Notes |
request_id | String | Mandatory | This indicates the unique id of the request. Useful for debugging and tracing purposes. |
method | String | Mandatory | This indicates the HTTP method for the request such as POST |
http_code | Integer | Mandatory | This indicates the HTTP code for the request such as 202, 400, 500 etc. |
metadata | Metadata Object | Mandatory | Metadata pertaining to the request. Count of failed, total and success records. |
response | Response Object | Mandatory | Response for the request |
Parameter | Type | Mandatory/Optional | Notes |
total | Integer | Mandatory | Total number of the messages in the request |
success | Integer | Mandatory | Number of messages successfully accepted |
failed | Integer | Mandatory | Number of messages that couldn’t be accepted (failed) |
Parameter | Type | Mandatory/Optional | Notes |
Channel Response Object | Mandatory | Response for Whatsapp messages specified in the request |
Parameter | Type | Mandatory/Optional | Notes |
messages | []Create Message Response Object | Mandatory | Array of messages response for each message |
Parameter | Type | Mandatory/Optional | Notes |
code | Integer | Mandatory | Response code for the individual message |
error_data | Error Response Object | Optional | Error related to a single message |
status | String | Mandatory | Status of the single message |
data | Message Response Object | Optional | Data pertaining to a single message |
Parameter | Type | Mandatory/Optional | Notes |
code | Numeric | Mandatory | Numeric HTTP code for a Single message |
message | String | Mandatory | Brief description of the error |
description | String | Mandatory | Detailed explanation of error |
Parameter | Type | Mandatory/Optional | Notes |
sid | String | Mandatory | SID (Unique identifier) of the single message |
HTTP Error Codes | Error Message |
202 | Accepted - Request accepted. |
400 | Bad Request - Something in your header or request body was malformed/missing.More than 100 messages specified in a request |
401 | Unauthorized - Necessary credentials were either missing or invalid. |
402 | Payment Required - The action is not available on your plan, or you have exceeded usage limits for your current plan. |
403 | Your credentials are valid, but you don’t have access to the requested resource. |
404 | Not Found - The object you’re requesting doesn’t exist. |
5xx | Server Errors - Something went wrong at our end. Please try again. |
Status callback URL can be passed in the status_callback parameter in send message APIs and it can also be configured as default to receive the responses. The Exotel team will help you configure the default URL while onboarding.
*NOTE: Any callback can be received only in one status callback URL at any time.
The sent message with the Order Details message will have a different Status Callback when the user completes the payment.
{ "params": { "whatsapp": { "messages": [ { "callback_type": "dlr", "sid": "e96ec08a641612cXXXXXXXX", "to": "+9186XXXXXXXX", "exo_status_code": 30002, "exo_detailed_status": "EX_MESSAGE_DELIVERED", "description": "Message Delivered", "timestamp": "2024-05-21T12:48:26+05:30", "payment_reference_id": "1033XX", "conversation_id": "675625c2XXXXXXX" } ] } }
{ "whatsapp": { "messages": [ { "callback_type": "dlr", "sid": "659f8XXXXXXXX", "from": "+9180XXXXXXXX", "to": "+9194XXXXXXXX", "exo_status_code": 30050, "exo_detailed_status": "EX_PAYMENT_SUCCESS", "description": "Payment Succeeded", "timestamp": "2024-08-06T14:59:10+05:30", "payment_reference_id": "ka90XXXXXX", "payment_transaction_id": "order_OhXXXXXX", "payment": { "reference_id": "ka90XXXXXX", "currency": "INR", "amount": { "value": 100, "offset": 100 }, "transaction": { "id": "order_OhXXXXXX", "type": "razorpay", "status": "success", "created_timestamp": 1722936549, "updated_timestamp": 1722936549, "amount": { "value": 100, "offset": 100 }, "currency": "INR" } } } ] } }
{ "whatsapp": { "messages": [ { "callback_type": "dlr", "sid": "4ad65c8751815192820af097e0501886", "from": "+9180XXXXXXXX", "to": "+9194XXXXXXXX", "exo_status_code": 30049, "exo_detailed_status": "EX_PAYMENT_PENDING", "description": "Payment is pending", "timestamp": "2024-08-06T16:54:26+05:30", "payment_reference_id": "ka90XXXXXX", "payment_transaction_id": "order_OaXXXXXX", "payment": { "reference_id": "ka90XXXXXX", "currency": "INR", "amount": { "value": 100, "offset": 100 }, "transaction": { "id": "order_OaXXXXXX", "type": "razorpay", "status": "failed", "created_timestamp": 1722943465, "updated_timestamp": 1722943465, "amount": { "value": 100, "offset": 100 }, "currency": "INR" } } } ] } }
You can now send the Payment Order Status message to the end-users after the payment success or failure to update the status of the order purchase using our Exotel APIs.
https://<your_api_key>:<your_api_token><subdomain>/v2/accounts/<your_sid>/messages
Upon receiving transaction signals from the payment gateway through webhook, the business must update the order status to keep the user up to date. Currently, we support the following order status values:
Value | Description |
pending | User has not successfully paid yet |
processing | User payment authorized, merchant/partner is fulfilling the order, performing service, etc. |
partially-shipped | A portion of the products in the order have been shipped by the merchant |
shipped | All the products in the order have been shipped by the merchant |
completed | The order is completed and no further action is expected from the user or the partner/merchant |
canceled | The partner/merchant would like to cancel the order_details message for the order/invoice. The status update will fail if there is already a successful or pending payment for this order_details message |
Typically businesses update the order_status using either the WhatsApp payment status change notifications or their own internal processes. To update order_status, the partner sends an order_status message to the user.
curl --location --globoff 'https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages' \ --header 'Content-Type: application/json' \ --data '{ "custom_data": "ORDXXXXXX", "status_callback": "https://webhook.site", "whatsapp": { "messages": [ { "from": "{{FromNumber}}", "to": "{{ToNumber}}", "content": { "type": "interactive", "interactive": { "type": "order_status", "body": { "text": "your-text-body-content" }, "action": { "name": "review_order", "parameters": { "reference_id": "reference-id-value", "order": { "status":"processing | partially_shipped | shipped | completed | canceled", "description": "optional-text" } } } } } } ] } }'
import requests import json url = "https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages" payload = json.dumps({ "custom_data": "ORDER12356894", "status_callback": "https://eoj72flq0lwx2gs.m.pipedream.net/", "whatsapp": { "messages": [ { "from": "{{FromNumber}}", "to": "{{ToNumber}}", "content": { "type": "interactive", "interactive": { "type": "order_status", "body": { "text": "your-text-body-content" }, "action": { "name": "review_order", "parameters": { "reference_id": "reference-id-value", "order": { "status": "processing | partially_shipped | shipped | completed | canceled", "description": "optional-text" } } } } } } ] } }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)