Tylt provides a webhook mechanism for merchants to receive real-time updates on the status of their transactions, whether for pay-ins. Merchants can specify a callBackUrl in their API requests, and Tylt will send notifications to this URL whenever there is a status change in the transaction.
Setting Up the Webhook
Implement a Callback Endpoint: Merchants must set up an HTTP POST endpoint that can receive JSON payloads. This endpoint should be capable of processing the incoming webhook data and verifying its authenticity using HMAC-SHA256 signature validation.
Insert the Callback URL: During a pay-in request, insert your endpoint URL in the callBackUrl field. Tylt will send updates to this URL whenever the transaction status changes.
Status Updates: When a transaction status changes to Pending, Completed, Under Payment, Over Payment, or Expired Tylt will send a JSON payload with the updated status.
Callback Validation: To ensure the integrity and authenticity of the callback, Tylt signs each callback payload using HMAC-SHA256 with the merchant’s API secret key. This signature is sent in the HTTP header X-TLP-SIGNATURE.
Acknowledge the Callback: Upon receiving the callback, merchants must respond with an HTTP 200 status code and the text "ok" in the response body. This acknowledges the successful receipt of the callback. If the acknowledgment is not received, the webhook will not be retried automatically. Merchants can manually resend webhooks from their Tylt dashboard.
Validating Callbacks
Merchants should validate the HMAC signature included in the X-TLP-SIGNATURE header to ensure the callback is from Tylt and has not been tampered with. The HMAC signature is generated using the raw POST data and the MERCHANT_API_SECRET as the shared key.
Example Web-hook Handling Code
constexpress=require('express');constcrypto=require('crypto');constapp=express();constPORT=3000;constapiSecretKey='YOUR_TLP_API_SECRET_KEY';// Replace with your actual API secret key// Middleware to parse incoming JSON requestsapp.use(express.json());// Callback endpointapp.post('/callback',(req,res)=>{constdata=req.body; // Calculate HMAC signatureconsttlpSignature=req.headers['x-tlp-signature'];constcalculatedHmac=crypto.createHmac('sha256',apiSecretKey).update(JSON.stringify(data)) // Use raw body string for HMAC calculation.digest('hex');if (calculatedHmac===tlpSignature) { // Signature is validif (data.type==='pay-in') {console.log('Received pay-in callback:',data); // Process pay-in data here} // Return HTTP Response 200 with content "ok"res.status(200).send('ok');}else{ // Invalid HMAC signatureres.status(400).send('Invalid HMAC signature');}});// Start the serverapp.listen(PORT,()=>{console.log(`Server listening on port ${PORT}`);});
Again, please note that these code snippets serve as examples and may require modifications based on your specific implementation and framework.
Example of Web-hook Responses
Again, please note that these response snippets serve as examples and may require modifications based on your specific implementation and framework.
Again, please note that these response snippets serve as examples and may require modifications based on your specific implementation and framework.
Understanding and Handling Transactions Based on Status
The response field status represents the current state of a transaction. Applications should interpret and handle transactions according to the following possible states:
Status
Description
Pending
The transaction is awaiting payment or confirmation.
Completed
The transaction is successfully completed and settled. The customer has paid exactly the settledAmountRequested.
Under Payment
Applicable to Pay-in: The transaction is completed and settled, but the customer paid less than the settledAmountRequested.
Over Payment
Applicable to Pay-in: The transaction is completed and settled, but the customer paid more than the settledAmountRequested.
Expired
Applicable to Pay-in: The transaction expired without any payment being received from the customer.
Understanding and Handling Over-Payment and Under-Payment
How a merchant handles over-payments and under-payments depends on their business model, use case, and internal policies. Broadly, practices differ between industries that accept deposits and those that sell goods or services.
Received: 95 USDT (under-payment) → Merchant credits 95 USDT to user wallet.
Received: 105 USDT (over-payment) → Merchant credits 105 USDT to user wallet.
Since both the baseCurrency and settledCurrency are the same (USDT), the merchant may use either baseCurrencyReceived or settledCurrencyReceived to handle the business logic.
Example B: Deposit Requested in Fiat (FX-Denominated)
Requested: 500 BRL equivalent
Received: 95 USDT → At settlement, worth 475 BRL.
Merchant may credit:
475 BRL (using baseCurrencyReceived), OR
95 USDT (using settledCurrencyReceived).
Received: 105 USDT → At settlement, worth 525 BRL.
Merchant may credit:
525 BRL (using baseCurrencyReceived), OR
105 USDT (using settledCurrencyReceived).
Response Records:
baseCurrencyReceived = 475 BRL / 525 BRL
settledCurrencyReceived = 95 USDT / 105 USDT
This dual recording ensures flexibility: deposits can be credited in either fiat terms or crypto terms, depending on merchant policy.
2. Industries Accepting Payments for Sale of Merchandise
(e.g., Retail, eCommerce, SaaS, Subscriptions)
Here, payments correspond to a specific invoice for goods or services. Merchants may settle either in fiat equivalent value or in the crypto amount received.
Under-Payment
Merchant may hold the order until the missing balance is paid.
Merchant may accept partial payment and adjust/store credit accordingly.
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
# Your TL Pay API Secret Key
TLP_API_SECRET_KEY = 'YOUR_TLP_API_SECRET_KEY' # Replace with your actual API secret key
@app.route('/callback', methods=['POST'])
def callback():
# Get the raw request data for HMAC calculation
raw_data = json.dumps(request.get_json(), separators=(',', ':'), ensure_ascii=False)
# Parse JSON data from the request
data = request.get_json()
# Retrieve the signature from the request headers
tlp_signature = request.headers.get('X-TLP-SIGNATURE')
# Calculate the HMAC SHA-256 signature
calculated_hmac = hmac.new(
key=TLP_API_SECRET_KEY.encode(),
msg=raw_data,
digestmod=hashlib.sha256
).hexdigest()
# Compare the calculated HMAC signature with the one in the request header
if hmac.compare_digest(calculated_hmac, tlp_signature):
# Signature is valid
if data['type'] == 'pay-in':
print('Received pay-in callback:', data)
# Process pay-in data here
# Return HTTP Response 200 with content "ok"
return jsonify({"message": "ok"}), 200
else:
# Invalid HMAC signature
return jsonify({"error": "Invalid HMAC signature"}), 400
if __name__ == '__main__':
app.run(port=3000, debug=True)
{
"data": {
"orderId": "sample-id-1", // an id like ajdng-adgh-1433-adad
"merchantOrderId": "sample-id-2", // an id like ajdng-adgh-1433-adad
"baseAmount": 10,
"baseCurrency": "USDT",
"baseAmountReceived": 10
"settledCurrency": "USDT",
"settledAmountRequested": 10,
"settledAmountReceived": 10,
"settledAmountCredited": 9.9,
"commission": 0.1,
"network": "BSC",
"depositAddress": "address", // a crypto wallet address
"status": "Completed",
"paymentURL": "https://app.tylt.money/pscreen/sample-id-1", // sample-id-1 is the orderId above
"callBackURL": "https://www.domain.com/your_callback_url", // your url where this callback data is sent
"transactions": [
{
"fromAddress": "address", // a crypto wallet address
"transactionHash": "txn_hash", // a crypto transaction hash
"amount": 10,
"confirmationStatus": 1,
"createdAt": "2024-11-06T19:00:29Z",
"updatedAt": "2024-11-06T19:01:21Z"
}
],
"createdAt": "2024-11-06T18:54:44Z",
"expiresAt": "2024-11-06T19:54:44Z",
"updatedAt": "2024-11-06T19:01:21Z",
"isFinal": 1,
"isCredited": 1,
"customerName": "",
"comments": "",
"accounts" = {
rate: 90, // for transaction where a fiat equivalent crypto is required
cryptoReceived: 10,
equivalentFiat: 900,// for transaction where a fiat equivalent crypto is required
}
}
},
"type": "pay-in"
}
{
"data": {
"orderId": "sample-id-1", // an id like ajdng-adgh-1433-adad
"merchantOrderId": "sample-id-2", // an id like ajdng-adgh-1433-adad
"settledCurrency": "USDT",
"settledAmountRequested": 10,
"settledAmountDebited": 10,
"settledAmountSent": 9.9,
"commission": 0.1,
"network": "BSC",
"toAddress": "address", // a crypto wallet address
"status": "Completed",
"insufficientBalance": 0,
"paymentURL": "", // sample-id-1 is the orderId above
"callBackURL": "", // your url where this callback data is sent
"transactions": [
{
"fromAddress": "address", // a crypto wallet address
"transactionHash": "txn_hash", // a crypto transaction hash
"amount": 10,
"confirmationStatus": 1,
"createdAt": "2024-11-06T19:00:29Z",
"updatedAt": "2024-11-06T19:01:21Z"
}
],
"createdAt": "2024-11-06T18:54:44Z",
"expiresAt": "2024-11-06T19:54:44Z",
"updatedAt": "2024-11-06T19:01:21Z",
"isFinal": 1,
"isDebited": 1,
"customerName": "",
"comments": ""
},
"type": "pay-out"
}