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.
Please understand that this feature is currently in Beta and it’s important to note that certain capabilities, such as Reports and Analytics are not yet available on the dashboard. We will be working on this and is part of our next phase launch.
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 via WhatsApp in a user-initiated conversation 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, all this is now possible using the Exotel APIs.
Before you start sending the Message with the order details and payment button, you need to complete the setup and follow the steps:
How it Works:
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 "upi". |
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 --globoff 'https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages' \ --header 'Content-Type: application/json' \ --data '{ "whatsapp": { "messages": [ { "from": "{{FromNumber}}", "to": "{{ToNumber}}", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "link": "https://image.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": "ABCD1234", "type": "digital-goods", "payment_type": "upi", "payment_configuration": "ExotelPayment", "currency": "INR", "total_amount": { "value": 11100, "offset": 100 }, "order": { "status": "pending", "items": [ { "retailer_id": "1234567", "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" } } } } } } } ] } } '
import requests import json url = "https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages" payload = json.dumps({ "whatsapp": { "messages": [ { "from": "{{FromNumber}}", "to": "{{ToNumber}}", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "link": "https://image.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": "ABCD1234", "type": "digital-goods", "payment_type": "upi", "payment_configuration": "ExotelPayment", "currency": "INR", "total_amount": { "value": 11100, "offset": 100 }, "order": { "status": "pending", "items": [ { "retailer_id": "1234567", "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" } } } } } } } ] } }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print(response.text)
curl --location --globoff 'https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages' \ --header 'Content-Type: application/json' \ --data '{ "custom_data": "ORDERXXXX", "status_callback": "https://webhook.site", "whatsapp": { "messages": [ { "from": "+91XXXXXXXXXX", "to": "+9199XXXXXXXX", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "id": "your-media-id" } }, "body": { "text": "your-text-body-content" }, "footer": { "text": "your-text-footer-content" }, "action": { "name": "review_and_pay", "parameters": { "reference_id": "reference-id-value", "type": "digital-goods", "payment_configuration": "unique-payment-config-id", "currency": "INR", "total_amount": { "value": 21000, "offset": 100 }, "order": { "status": "pending", "expiration": { "timestamp": "utc_timestamp_in_seconds", "description": "cancellation-explanation" }, "items": [ { "retailer_id": "1234567", "name": "Product name, for example bread", "amount": { "value": 10000, "offset": 100 }, "quantity": 1, "sale_amount": { "value": 100, "offset": 100 }, "country_of_origin": "country-of-origin", "importer_name": "name-of-importer-business", "importer_address": { "address_line1": "B8/733 nand nagri", "address_line2": "police station", "city": "East Delhi", "zone_code": "DL", "postal_code": "110093", "country_code": "IN" } }, { "retailer_id": "14325", "name": "Product name, for example bread", "amount": { "value": 10000, "offset": 100 }, "quantity": 1, "sale_amount": { "value": 100, "offset": 100 }, "country_of_origin": "country-of-origin", "importer_name": "name-of-importer-business", "importer_address": { "address_line1": "B8/733 nand nagri", "address_line2": "police station", "city": "East Delhi", "zone_code": "DL", "postal_code": "110093", "country_code": "IN" } } ], "subtotal": { "value": 20000, "offset": 100 }, "tax": { "value": 1000, "offset": 100, "description": "optional_text" }, "shipping": { "value": 1000, "offset": 100, "description": "optional_text" }, "discount": { "value": 1000, "offset": 100, "description": "optional_text", "discount_program_name": "optional_text" } } } } } } } ] } }'
import requests import json url = "https://{{AuthKey}}:{{AuthToken}}@{{SubDomain}}/v2/accounts/{{AccountSid}}/messages" payload = json.dumps({ "custom_data": "ORDERXXXX", "status_callback": "https://webhook.site", "whatsapp": { "messages": [ { "from": "+91XXXXXXXXXX", "to": "+9199XXXXXXXX", "content": { "type": "interactive", "interactive": { "type": "order_details", "header": { "type": "image", "image": { "id": "your-media-id" } }, "body": { "text": "your-text-body-content" }, "footer": { "text": "your-text-footer-content" }, "action": { "name": "review_and_pay", "parameters": { "reference_id": "reference-id-value", "type": "digital-goods", "payment_configuration": "unique-payment-config-id", "currency": "INR", "total_amount": { "value": 21000, "offset": 100 }, "order": { "status": "pending", "expiration": { "timestamp": "utc_timestamp_in_seconds", "description": "cancellation-explanation" }, "items": [ { "retailer_id": "1234567", "name": "Product name, for example bread", "amount": { "value": 10000, "offset": 100 }, "quantity": 1, "sale_amount": { "value": 100, "offset": 100 }, "country_of_origin": "country-of-origin", "importer_name": "name-of-importer-business", "importer_address": { "address_line1": "B8/733 nand nagri", "address_line2": "police station", "city": "East Delhi", "zone_code": "DL", "postal_code": "110093", "country_code": "IN" } }, { "retailer_id": "14325", "name": "Product name, for example bread", "amount": { "value": 10000, "offset": 100 }, "quantity": 1, "sale_amount": { "value": 100, "offset": 100 }, "country_of_origin": "country-of-origin", "importer_name": "name-of-importer-business", "importer_address": { "address_line1": "B8/733 nand nagri", "address_line2": "police station", "city": "East Delhi", "zone_code": "DL", "postal_code": "110093", "country_code": "IN" } } ], "subtotal": { "value": 20000, "offset": 100 }, "tax": { "value": 1000, "offset": 100, "description": "optional_text" }, "shipping": { "value": 1000, "offset": 100, "description": "optional_text" }, "discount": { "value": 1000, "offset": 100, "description": "optional_text", "discount_program_name": "optional_text" } } } } } } } ] } }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print(response.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.
{ "whatsapp": { "messages": [ { "callback_type": "dlr", "sid": "3485XXXXXXXXXXXX", "to": "+9188XXXXXXXX", "exo_status_code": 25001, "exo_detailed_status": "EX_PAYMENT_SUCCESS", "description": "Payment Succeeded", "timestamp": "2022-12-07T17:00:00.000+05:30", "custom_data": "custom_data", "payment": { "reference_id": "72XXXXX" } } ] } }
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)