Merchants can set up a callbackUrl in their merchant requests, and upon processing payments, OxaPay will send you notifications in JSON format with all required information about the transaction, such as the amount and status, when the payment status is changed to Waiting, Confirming, Paid, Failed, or Expired.
To receive webhook notifications, first, you should implement a standard HTTP POST (application/json) call over an HTTPS URL to a CGI script on your server. Insert your created endpoint address in the callbackUrl field of merchant requests. You will receive payment updates (statuses) to this URL address.
However, you will initially receive a callback with the "Confirming" status. You should wait for a second callback where the "status" value will be "Paid". When we confirm the transaction, we are ready to manage all the risks concerning accepting real funds later. You should not be concerned about it.
Merchant's callbackUrl must return an HTTP Response 200 with content: ok for OxaPay API to confirm the callback as successful. The system will try to deliver a webhook notification up to 5 times or until a successful delivery occurs, whichever happens first. If the first attempt fails, the second one is triggered after approximately 1 minute. The third one is delayed for 3 more minutes, the fourth for 30 minutes, and the last one for 3 hours.
The payout webhook is similar to a merchant webhook exactly. But for withdrawals, you will receive different values of the "address" parameter in every callback.
Validating Callbacks
Merchants must validate the signature of callbacks coming from OxaPay using their MERCHANT_API_KEY to prevent fraudulent activity. also for payout webhooks you must validate the signature using your PAYOUT_API_KEY. OxaPay uses your MERCHANT_API_KEY as the HMAC shared secret key to generate an HMAC(sha512) signature of the raw POST data. The HMAC signature is sent as an HTTP header called HMAC. The body of the request contains the callback data in JSON format, similar to the Payment Information API response body.
Testing Webhook
For payment callback handling, you can use tools like requestcatcher.com to create a test callbackUrl and inspect the callback data sent from OxaPay. Note that OxaPay payment callbacks will not be sent to private networks (e.g., localhost).
For local debugging, you can use services like https://ngrok.io to expose your local server to the internet and receive webhook callbacks.
To utilize the payment webhook, please follow these steps:
1. Create a web server and define an endpoint to handle POST requests.
- Ensure that the firewall software on your server (e.g., Cloudflare) allows incoming requests from OxaPay. You may need to whitelist OxaPay's IP addresses on your side. Please reach out to support@oxapay.com to obtain the list of IP addresses.
2. Configure your server to receive POST requests at the specified endpoint URL.
- The POST request body will contain the necessary parameters sent by OxaPay in JSON format.
3. Validate the HMAC signature of the request to ensure the authenticity of the callback.
- Use your API_SECRET_KEY to calculate the HMAC signature and compare it with the received HMAC header.
4. Process the callback data accordingly based on the status and other parameters provided.
Sample Payment IPN data
In this section, we will provide examples of the data OxaPay send to your system at various stages of the payment process.
// Waiting IPN // Payer selected the payment currency and geting address. Awaiting payment.{"status":"Waiting","trackId":"35092972","amount":"100","currency":"TRX","feePaidByPayer":0,"underPaidCover":2,"email":"","orderId":"665673996","description":"","date":"1698107946","payDate":0,"type":"payment"}
// Confirming IPN// Payer transferred the payment for the invoice. Awaiting blockchain network confirmation.{"status":"Confirming","trackId":"35092972","address":"TCue3HjMWfARcf1W5NK7MEqLyLcWtmbZcx","senderAddress":"TH7Q7AVjwXRbC6hnJua8MFyfvkVnoyeHB6","txID":"918341f6c280b3b4ca2f8e50d8a32054a20cc999fd94cfa5da6d1b9f0b3f9a1e","amount":"100","currency":"TRX","price":"9.23","payAmount":"14","payCurrency":"TRX","network":"TRC20","feePaidByPayer":0,"underPaidCover":2,"email":"","orderId":"665673996","description":"","date":"1698156969","payDate":"1698160515","type":"payment"}
// Paid IPN// Payment is confirmed by the network and has been credited to the merchant. Purchased goods/services can be safely delivered to the payer.
{"status":"Paid","trackId":"35092972","address":"TCue3HjMWfARcf1W5NK7MEqLyLcWtmbZcx","senderAddress":"TH7Q7AVjwXRbC6hnJua8MFyfvkVnoyeHB6","txID":"918341f6c280b3b4ca2f8e50d8a32054a20cc999fd94cfa5da6d1b9f0b3f9a1e","amount":"100","currency":"TRX","price":"9.23","payAmount":"100","payCurrency":"TRX","network":"TRC20","feePaidByPayer":0,"underPaidCover":2,"email":"","orderId":"665673996","description":"","date":"1698156969","payDate":"1698160515","type":"payment"}
// Expired IPN// Payer did not pay within the required time (default: 20 minutes) and the invoice expired.{"status":"Expired","trackId":"40769539","amount":"0.1","currency":"USD","feePaidByPayer":0,"underPaidCover":0,"email":"","orderId":"","description":"","date":"1697812600","payDate":0,"type":"payment"}
Sample Payout IPN data
In this section, we will provide examples of the data OxaPay send to your system at various stages of the payout process.
// Confirming IPN// You payout request sent and awaiting blockchain network confirmation.{"status":"Confirming","trackId":"28156600","txID":"x","address":"x","amount":"200","currency":"DGB","price":"1.53600000","network":"DigiByte","date":"1700847655","description":"","type":"payout"}
// Complete IPN// Payout is confirmed by the network.{"status":"Complete","trackId":"28156600","txID":"x","address":"x","amount":"200","currency":"DGB","price":"1.53600000","network":"DigiByte","date":"1700847655","description":"","type":"payout"}
Example codes
<?php// Get the request data$postData =file_get_contents('php://input');$data =json_decode($postData,true);// Validate HMAC signatureif ($data['type'] ==='payment') { $apiSecretKey ='YOUR_MERCHANT_API_KEY';} elseif ($data['type'] ==='payout') { $apiSecretKey ='YOUR_PAYOUT_API_KEY';} else {http_response_code(400);echo'Invalid data.type';exit;}$hmacHeader = $_SERVER['HTTP_HMAC'];$calculatedHmac =hash_hmac('sha512', $postData, $apiSecretKey);if ($calculatedHmac === $hmacHeader) {// HMAC signature is valid// Process the callback dataif ($data['type'] ==='payment') {echo'Received payment callback: '.json_encode($data);// Process payment data here } elseif ($data['type'] ==='payout') {echo'Received payout callback: '.json_encode($data);// Process payout data here }// Return HTTP Response 200 with content "OK"http_response_code(200);echo'OK';} else {// HMAC signature is not valid// Handle the error accordinglyhttp_response_code(400);echo'Invalid HMAC signature';}?>
consthttp=require('http');constcrypto=require('crypto');constserver=http.createServer((req, res) => {if (req.url ==='/callback'&&req.method ==='POST') {// Validate HMAC signaturelet postData ='';req.on('data', chunk => { postData += chunk; });req.on('end', () => {// Parse the JSON datalet data =null;try { data =JSON.parse(postData); } catch (error) {res.statusCode =400;res.end('Invalid JSON data');return; }constapiSecretKey= (data.type ==='payment') ?'YOUR_MERCHANT_API_KEY':'YOUR_PAYOUT_API_KEY';consthmacHeader=req.headers['hmac'];constcalculatedHmac= crypto.createHmac('sha512', apiSecretKey).update(postData).digest('hex');if (calculatedHmac === hmacHeader) {// HMAC signature is valid// Process the callback data based on the typeif (data.type ==='payment') {console.log('Received payment callback:', data);// Process payment data here } elseif (data.type ==='payout') {console.log('Received payout callback:', data);// Process payout data here }// Return HTTP Response 200 with content "OK"res.statusCode =200;res.end('OK'); } else {// HMAC signature is not valid// Handle the error accordinglyres.statusCode =400;res.end('Invalid HMAC signature'); } }); } else {// Invalid path or methodres.statusCode =404;res.end('Not Found'); }});server.listen(3000, () => {console.log('Server listening on port 3000');});
from flask import Flask, requestimport hmacimport hashlibimport jsonapp =Flask(__name__)@app.route('/callback', methods=['POST'])defhandle_callback(): post_data = request.get_data(as_text=True) data = json.loads(post_data)# Validate HMAC signatureif data['type']=='payment': api_secret_key ='YOUR_MERCHANT_API_KEY'elif data['type']=='payout': api_secret_key ='YOUR_PAYOUT_API_KEY'else:return'Invalid data.type',400 hmac_header = request.headers.get('HMAC') post_data = request.get_data() calculated_hmac = hmac.new(api_secret_key.encode(), post_data, hashlib.sha512).hexdigest()if calculated_hmac == hmac_header:# HMAC signature is valid# Process the callback dataif data['type']=='payment':print('Received payment callback:', data)# Process payment data hereelif data['type']=='payout':print('Received payout callback:', data)# Process payout data here# Return HTTP Response 200 with content "OK"return'OK',200else:# HMAC signature is not valid# Handle the error accordinglyreturn'Invalid HMAC signature',400if__name__=='__main__': app.run(host='YOUR_HOST_ADDRESS',port=3000)
Again, please note that these code snippets serve as examples and may require modifications based on your specific implementation and framework.