Webhook
To receive real-time notifications about payments with SP Cuvex, you must configure a webhook in your system.
Webhook Integration
For your system to receive notifications about transactions in real time, complete the following process:
- Provide a publicly accessible endpoint that receives events in JSON format via POST requests.
- Generate a secret string, share it with us, and store it securely. This secret will be used to sign each incoming request.
- For each event received at the webhook, verify its authenticity using the shared secret and the signature included in the request headers.
- After verifying the signature, respond immediately.
Endpoint
Create a public endpoint where we will send event notifications in JSON format via POST requests.
In the x-sign header we will send a signature generated with the HMAC-SHA256 algorithm, using the previously shared secret and the request body (payload). This signature ensures that the request was sent by SP Cuvex.
Secret String
During your enrollment process, you must share both the endpoint URL and a secret string with us.
This string will be used to generate the HMAC-SHA256 signature for each request. It must be alphanumeric, random, high entropy, and no longer than 128 characters.
Store it securely on your server. Never include it in source code or repositories (Git, etc.).
Signature Verification
Each request will also include an x-sign header, similar to the following:
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
This signature is generated with the algorithm:
HMAC-SHA256(UTF-8(secret), body)
You must replicate the signature calculation using your securely stored secret string and the received payload.
- Use UTF-8 for text-to-bytes conversion.
- Compare the received signature with the locally generated one using a timing-safe comparison.
- Remember to omit the
sha256=prefix before comparing. - If the signatures do not match, respond with HTTP 401.
Examples
- Java (Spring Boot)
- Python (Flask)
- Node.js (Express)
@RestController
public class WebhookController {
@PostMapping(value = "/webhook", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> webhookEndpoint(HttpServletRequest request) throws IOException,
NoSuchAlgorithmException,
InvalidKeyException {
String signature = request.getHeader("x-sign");
byte[] body = this.getPayloadBytes(request);
this.validateSignature(signature, body);
// Do your data processing asynchronously to respond ASAP
return ResponseEntity.ok().build();
}
public byte[] getPayloadBytes(HttpServletRequest request) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
InputStream inputStream = request.getInputStream();
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
public void validateSignature(final String receivedSignatue, final byte[] body) throws NoSuchAlgorithmException, InvalidKeyException {
HexFormat hexFormatter = HexFormat.of();
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
String keyString = "Sup3rS4cretT3xt!";
String cleanedReceivedSign = receivedSignatue.replace("sha256=", "");
byte[] receivedHashBytes = hexFormatter.parseHex(cleanedReceivedSign);
SecretKeySpec secretKey = new SecretKeySpec(keyString.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] calculatedHash = hmacSha256.doFinal(body);
if (!Arrays.equals(calculatedHash, receivedHashBytes)) {
throw new RuntimeException("Signatures don't match");
}
}
}
from flask import Flask, request
import hmac
import hashlib
import binascii
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook_endpoint():
try:
signature = request.headers.get('x-sign')
body = request.get_data()
validate_signature(signature, body)
return '', 200
except ValueError:
return '', 401
except Exception:
return '', 500
def validate_signature(received_signature, body):
secret_key = "Sup3rS4cretT3xt!"
cleaned_received_sign = received_signature.replace("sha256=", "")
try:
received_hash_bytes = binascii.unhexlify(cleaned_received_sign)
except (ValueError, binascii.Error):
raise ValueError("Invalid signature format")
calculated_hash = hmac.new(
secret_key.encode('utf-8'),
body,
hashlib.sha256
).digest()
if not hmac.compare_digest(calculated_hash, received_hash_bytes):
raise ValueError("Signatures don't match")
if __name__ == '__main__':
app.run(debug=True, port=8080)
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 8080;
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
try {
const signature = req.headers['x-sign'];
validateSignature(signature, req.body);
return res.status(200).end();
} catch (error) {
if (error.message === "Signatures don't match" || error.message === "Invalid signature format") {
return res.status(401).end();
}
return res.status(500).end();
}
});
function validateSignature(receivedSignature, body) {
const secretKey = "Sup3rS4cretT3xt!";
const cleanedReceivedSign = receivedSignature.replace("sha256=", "");
let receivedHashBuffer = Buffer.from(cleanedReceivedSign, 'hex');
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(body)
.digest();
if (!crypto.timingSafeEqual(calculatedHash, receivedHashBuffer)) {
throw new Error("Signatures don't match");
}
}
app.listen(PORT, () => {
console.log(`Webhook server running on http://localhost:${PORT}`);
});
Handling Incoming Data and Response
After verifying the signature, respond immediately.
Process heavy workloads asynchronously in your system.
Expected responses (without payload):
- 200: signature verified.
- 401: invalid signature.
- 500: unexpected error.
Replay Protection
We include x-timestamp (UTC, seconds) and x-id (unique nonce per event).
Reject requests with x-timestamp outside a ±5 minute window and repeated x-id values.
Retries
If your endpoint responds with anything other than 2xx, we will retry using exponential backoff (e.g., up to 5 attempts in ~5 minutes).
Deduplicate by x-id.
Event Definitions
Payment Created
This event will be sent whenever a new payment is created.
{
"event": "PAYMENT_CREATED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "OPEN",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}
Payment Finished
Event sent when a payment is confirmed on the blockchain.
{
"event": "PAYMENT_FINISHED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "5.25",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "FINISHED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:46:12Z",
"txs": [
{
"tx": "6e3a8368bcc7113cef086156fc4500b3322fa258781652cdc56a855c12e5cb3b",
"block_number": "75279138",
"sender_address": "TVAKUM28o1pNj6pkcckvWHFUs1foRcSwAt",
"amount": "123.456789"
}
]
}
}
Payment Late Finished
Event sent when a payment is confirmed but outside the expected time window.
{
"event": "PAYMENT_LATE_FINISHED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "5.25",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "LATE_PAYMENT",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:46:12Z",
"txs": [
{
"tx": "6e3a8368bcc7113cef086156fc4500b3322fa258781652cdc56a855c12e5cb3b",
"block_number": "75279138",
"sender_address": "TVAKUM28o1pNj6pkcckvWHFUs1foRcSwAt",
"amount": "123.456789"
}
]
}
}
Payment Expired
Event sent when the validity period expires without payment confirmation.
{
"event": "PAYMENT_EXPIRED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "EXPIRED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}
Payment Failed
Event sent if an error occurs during payment processing.
{
"event": "PAYMENT_FAILED",
"data": {
"id": "fca84a27-2a4c-413c-9f0d-edff3c25959e",
"reference": "INV-09-2025-0001",
"network": "TRON",
"token": "USDT",
"error_message": "The network is offline",
"amount": "5.25",
"confirmed_amount": "0",
"receiver": "TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz",
"status": "FAILED",
"valid_until": 1755532150,
"created_at": "2024-04-16T17:44:51Z",
"updated_at": "2024-04-16T17:44:51Z"
}
}