Webhooks
Last updated: 2026-01-25
Overview
Webhooks are HTTP-based callbacks that notify your system when specific events occur on your entities. Instead of polling for updates, you configure a webhook once, and it pushes data to your system when relevant events happen - such as updates on payments, tokens, scheduled payments, risk decisions or transaction state changes.
Why use webhooks?
Webhooks help you:
- Receive data automatically when something changes
- Automate workflows (for example, update order status, trigger refunds)
- Simplify integration - just provide a URL to start receiving events
- Reduce load on your systems by avoiding polling
When not to use webhooks
Webhooks are asynchronous and typically fast, but not guaranteed to be instant. In rare cases - such as during platform releases or data center switchovers - delivery may be delayed by several minutes. If your workflow depends on real-time transaction status, use the Transaction Status query API instead.
Use webhooks when:
- You want event-driven automation
- You need transaction updates for reporting or reconciliation
- A short delay (up to ~15 minutes) is acceptable
Use transaction status query when:
- You need immediate confirmation (for example, to capture funds or deliver goods while the shopper is present)
- Your workflow is time-sensitive
Availability
Webhooks are typically delivered within seconds of the triggering event. However, during platform releases, data center switchovers, or application restarts, delivery may be delayed by up to 15 minutes.
If your use case requires immediate awareness of transaction status (for example, to capture funds or deliver goods while the shopper is still present), we strongly recommend using the Transaction Status query API instead of relying solely on webhooks.
| Use case type | Recommended approach | Reason |
|---|---|---|
| Real-time decisions (for example, capture, fulfillment) | Transaction Status query API | Ensures immediate and reliable status |
| Reporting & reconciliation | Webhooks | Delays are acceptable; automation-friendly |
| Non-critical automation | Webhooks | Efficient and event-driven |
| High-frequency polling | Not recommended | Use webhooks or transaction export via SFTP instead |
Retry amd failure behavior
| Behavior | Description |
|---|---|
| Timeout | No response within 30 seconds → marked as failed |
| Failure | Non-2xx HTTP response → marked as failed |
| Retry intervals | 1 min → 2 min → 4 min → 8 min → 15 min → 30 min → 1 hour → daily (up to 30 days) |
| Failing retry pattern | Retries pause if all messages fail at a given interval; resume once delivery succeeds |
| Daily failure summary | Email with up to 100 failed notifications per endpoint |
| Retention | Failed messages stored for 30 days, then purged |
Message ordering and load
- No guaranteed order: Events may arrive out of sequence. Design your system to handle this.
- Multiple final messages: You may receive more than one final status (for example, success + failure). Deduplicate based on transaction ID and status.
- High throughput: Your server must handle bursts (for example, 30+ notifications/sec). Use asynchronous processing and caching where possible.
Configuration
You can configure webhooks at any level of your entity hierarchy. Each webhook will receive notifications for the entity it's configured on, as well as all its descendants.
Scope and hierarchy
| Feature | Description |
|---|---|
| Entity scope | A webhook receives events from its entity and all child entities |
| Multiple endpoints | The same notification is sent to all active webhook URLs configured at or above the entity level |
| Filtering | You can filter which types of events (for example, payments, risks) each webhook should receive |
| Automatic retries | Failed deliveries are automatically retried based on the retry policy |
| Failure alerts | Daily email alerts are sent when delivery fails repeatedly |
Add a webhook
To add a webhook, configure the following parameters:
| Parameter | Description |
|---|---|
| URL | The public endpoint that will receive the webhook notifications |
| Types | Event categories to subscribe to: PAYMENTS, REGISTRATIONS, SCHEDULES, RISKS |
| Fields | Choose ALL (full payload) or NON_CUSTOMER_DATA (exclude sensitive fields) |
| Secret | A 64-character hex string used to encrypt the payload |
| Wrapper | Format of the payload: None (hex string) or JSON (wrapped in JSON) |
| Emails | One or more email addresses to receive daily failure summaries |
Newly created webhooks are inactive by default. They must be tested and activated before receiving real notifications.
Test a webhook
Before activation, you must contact support to test the webhook.
The test ensures:
- The URL is reachable
- Your firewall allows incoming traffic
- Your server responds with HTTP 2xx
- The payload is correctly received and decrypted
Once the test succeeds, the support team makes the webhook active and it starts receiving real notifications.
If the test fails, the webhook remains inactive and no events will be delivered.
Format
Webhook notifications are sent as JSON objects with a consistent structure. This allows your system to parse and process them reliably.
{
"type": [notification_type],
"action": [status],
"payload": [content]
}Field descriptions
| Field | Description |
|---|---|
| type | The category of the event: PAYMENT, REGISTRATION, SCHEDULE, or RISK |
| action | Only present for REGISTRATION events. Indicates the change: CREATED, UPDATED, or DELETED |
| payload | The full content of the event. This mirrors the response you would receive from the corresponding API (for example, payment, risk transaction) |
The payload contains all relevant transaction data, depending on your selected field configuration (ALL or NON\_CUSTOMER\_DATA)
The structure is consistent across event types, but the payload schema varies depending on the event (for example, payment, risk, and so on)
Examples
{
"type": "PAYMENT",
"payload": {
"id": "8a829449515d198b01517d5601df5584",
"paymentType": "PA",
"paymentBrand": "VISA",
"amount": "92.00",
"currency": "EUR",
"presentationAmount": "92.00",
"presentationCurrency": "EUR",
"descriptor": "3017.7139.1650 OPP_Channel ",
"result": {
"code": "000.000.000",
"description": "Transaction succeeded"
},
"authentication": {
"entityId": "8a8294185282b95b01528382b4940245"
},
"card": {
"bin": "420000",
"last4Digits": "0000",
"holder": "Jane Jones",
"expiryMonth": "05",
"expiryYear": "2018"
},
"customer": {
"givenName": "Jones",
"surname": "Jane",
"merchantCustomerId": "jjones",
"sex": "F",
"email": "[email protected]"
},
"customParameters": {
"SHOPPER_promoCode": "AT052"
},
"risk": {
"score": "0"
},
"buildNumber": "ec3c704170e54f6d7cf86c6f1969b20f6d855ce5@2015-12-01 12:20:39 +0000",
"timestamp": "2015-12-07 16:46:07+0000",
"ndc": "8a8294174b7ecb28014b9699220015ca_66b12f658442479c8ca66166c4999e78",
"channelName": "OPP_Channel",
"source": "SYSTEM",
"paymentMethod": "CC",
"shortId": "5420.6916.5424"
}
}{
"type": "REGISTRATION",
"action": "CREATED",
"payload": {
"id": "8a82944a53e6a0150153eaf693584262",
"paymentBrand": "VISA",
"result": {
"code": "000.000.000",
"description": "Transaction succeeded",
"randomField1315125026": "Please allow for new unexpected fields to be added"
},
"card": {
"bin": "420000",
"last4Digits": "0000",
"holder": "Jane Jones"
},
"authentication": {
"entityId": "8a8294174b7ecb28014b9699220015ca"
},
"redirect": {
"parameters": []
},
"risk": {
"score": ""
},
"timestamp": "2016-04-06 09:45:41+0000",
"ndc": "8a8294174b7ecb28014b9699220015ca_b1539494024c411684b544574716e608",
"channelName": "OPP_Channel",
"source": "SYSTEM",
"paymentMethod": "CC",
"shortId": "7820.6916.2918"
}
}{
"type": "SCHEDULE",
"payload": {
"id": "8acda4a489919d63018996faf10b2a66",
"registrationId": "8acda4a889919e5e018996f86a8f127a",
"paymentType": "SD",
"presentationAmount": "92.00",
"presentationCurrency": "EUR",
"result": {
"code": "000.000.000",
"description": "Transaction succeeded",
"randomField1730751282": "Please allow for new unexpected fields to be added"
},
"resultDetails": {
"ConnectorTxID1": "8acda4a489919d63018996faf10b2a66"
},
"customer": {
"givenName": "Jones",
"surname": "Jane",
"merchantCustomerId": "jjones",
"sex": "F",
"email": "[email protected]"
},
"authentication": {
"entityId": "8a8294174b7ecb28014b9699220015ca"
},
"redirect": {
"parameters": []
},
"risk": {
"score": ""
},
"timestamp": "2023-07-27 10:52:55+0000",
"ndc": "ef2c099f5b29455a9dbd260c59bcc224",
"channelName": "OPP_Channel",
"source": "SCHEDULER",
"paymentMethod": "DC",
"shortId": "3833.0396.7654"
}
}{
"type": "RISK",
"payload": {
"id": "8ac9a4a86461239601646522acb26523",
"referencedId": "8ac9a4a86461239601646522aaf96510",
"paymentType": "RI",
"paymentBrand": "VISA",
"presentationAmount": "0.0",
"result": {
"code": "000.000.000",
"description": "Transaction succeeded"
},
"card": {
"bin": "420000",
"last4Digits": "0000",
"holder": "Jane Jones",
"expiryMonth": "03",
"expiryYear": "2025"
},
"authentication": {
"entityId": "8a8294174b7ecb28014b9699220015ca"
},
"redirect": {
"parameters": []
},
"risk": {
"score": ""
},
"timestamp": "2018-07-04 11:52:08+0000",
"ndc": "8a8294174b7ecb28014b9699220015ca_b1539494024c411684b544574716e608",
"channelName": "OPP_Channel",
"source": "SYSTEM",
"paymentMethod": "RM",
"shortId": "3833.0396.7654"
}
}Encryption
To protect sensitive transaction data from tampering or unauthorized access, all webhook payloads are encrypted before being sent to your configured endpoint. Decryption is required on your side to access the actual event content.
Encryption details
| Parameter | Description |
|---|---|
| Algorithm | AES (Advanced Encryption Standard) |
| Key | 64-character hexadecimal string (configured in the webhook settings) |
| Key length | 256 bits (32 bytes) |
| Block mode | GCM (Galois/Counter Mode) |
| Padding | None |
| Initialization vector | Sent in HTTP header X-Initialization-Vector (hexadecimal) |
| Authentication tag | Sent in HTTP header X-Authentication-Tag (hexadecimal) |
| Payload format | Encrypted hexadecimal string in the body |
| Wrapper option | None (raw hex string) or JSON (for example, { "encryptedBody": "..." }) |
Payload wrappers
Depending on your configuration, the system delivers the encrypted payload in one of two formats:
- None (default)
- Content-Type:
text/plain - Body contains only the encrypted hexadecimal string
- Content-Type:
- JSON wrapper
- Content-Type:
application/json - Body format:
{"encryptedBody": "hexadecimal\_string"}
- Content-Type:
Decryption
Decryption is the process of converting the encrypted webhook payload back into its original, readable format. This step is essential to interpret and act on the transaction data securely delivered to your endpoint.
What you need to decrypt
To decrypt the payload, your system must use the same encryption parameters configured in the webhook setup. These include:
| Component | Source |
|---|---|
| Encrypted body | In the HTTP request body (hexadecimal string or JSON wrapper) |
| Secret key | Configured in the webhook settings (64-character hex string) |
| Initialization vector | In HTTP header X-Initialization-Vector |
| Authentication tag | In HTTP header X-Authentication-Tag |
| Algorithm | AES-256-GCM (no padding) |
Decryption steps
- Extract the encrypted body from the request (either raw or from the
encryptedBodyfield in JSON). - Read the initialization vector and authentication tag from the headers.
- Use the configured secret key to decrypt the payload using AES-256-GCM.
- Validate the authentication tag to ensure integrity.
- Parse the decrypted payload as JSON to access the event data.
Common pitfalls
- Ensure the key format and length match the expected AES-256 requirements.
- Use UTF-8 encoding when converting strings to bytes.
- Validate the authentication tag to prevent tampered data.
- Handle decryption errors properly to avoid processing invalid payloads.
Example - decryption
Use the code snippets below to decrypt the encrypted webhook request body.
using System;
using System.Linq;
using System.Text;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
namespace DecryptionExample
{
// You need to install bccrypto-csharp from the BouncyCastle page (https://www.bouncycastle.org/download/bouncy-castle-c/).
class Program
{
static void Main(string[] args)
{
string keyFromConfiguration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
// Data from server
string ivFromHttpHeader = "000000000000000000000000";
string authTagFromHttpHeader = "CE573FB7A41AB78E743180DC83FF09BD";
string httpBody = "0A3471C72D9BE49A8520F79C66BBD9A12FF9";
// Convert data to process
byte[] key = ToByteArray(keyFromConfiguration);
byte[] iv = ToByteArray(ivFromHttpHeader);
byte[] authTag = ToByteArray(authTagFromHttpHeader);
byte[] encryptedText = ToByteArray(httpBody);
byte[] cipherText = encryptedText.Concat(authTag).ToArray();
// Prepare decryption
GcmBlockCipher cipher = new GcmBlockCipher(new AesFastEngine());
AeadParameters parameters = new AeadParameters(new KeyParameter(key), 128, iv);
cipher.Init(false, parameters);
// Decrypt
var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];
var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
cipher.DoFinal(plainText, len);
Console.WriteLine(Encoding.ASCII.GetString(plainText));
}
static byte[] ToByteArray(string HexString)
{
int NumberChars = HexString.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(HexString.Substring(i, 2), 16);
}
return bytes;
}
}
}import org.bouncycastle.jce.provider.BouncyCastleProvider
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.Security
// For JVM-based languages, you might need to install unrestricted policy files provided by Sun Microsystems.
// If you encounter errors like `java.lang.SecurityException: Unsupported keysize or algorithm parameters` or `java.security.InvalidKeyException: Illegal key size`, check the
// BouncyCastle FAQ page(https://www.bouncycastle.org/about/bouncy-castle-fips-faq/) for guidance.
// If installing the unrestricted policy files isn't an option, try using reflection as a workaround, as explained in this StackOverflow discussion on avoiding the need for unlimited strength JCE policy files(https://stackoverflow.com/questions/1179672/how-to-avoid-installing-unlimited-strength-jce-policy-files-when-deploying-an).
class Cipher {
static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider())
// Data from configuration
def keyFromConfiguration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"
// Data from server
def ivFromHttpHeader = "000000000000000000000000"
def authTagFromHttpHeader = "CE573FB7A41AB78E743180DC83FF09BD"
def httpBody = "0A3471C72D9BE49A8520F79C66BBD9A12FF9"
// Convert data to process
def key = keyFromConfiguration.decodeHex()
def iv = ivFromHttpHeader.decodeHex()
def authTag = authTagFromHttpHeader.decodeHex() as Byte[]
def encryptedText = httpBody.decodeHex() as Byte[]
// Unlike other programming languages, we have to append the auth tag at the end of encrypted text
def cipherText = encryptedText + authTag as Byte[]
// Prepare decryption
def keySpec = new SecretKeySpec(key, 0, 16, "AES")
def cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv))
// Decrypt
def result = cipher.doFinal(cipherText)
println(new String(result, "UTF-8"))
}
}import com.google.common.base.Charsets;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Security;
// For Java, you might need to install unrestricted policy files provided by Sun Microsystems.
// If you encounter errors like `java.lang.SecurityException: Unsupported keysize or algorithm parameters` or `java.security.InvalidKeyException: Illegal key size`, check the
// BouncyCastle FAQ page(https://www.bouncycastle.org/about/bouncy-castle-fips-faq/) for guidance.
// If installing the unrestricted policy files isn't an option, try using reflection as a workaround, as explained in this StackOverflow discussion on avoiding the need for unlimited strength JCE policy files(https://stackoverflow.com/questions/1179672/how-to-avoid-installing-unlimited-strength-jce-policy-files-when-deploying-an).
public class Decryption
{
public static void main(String[] args) throws Exception
{
Security.addProvider(new BouncyCastleProvider());
// Data from configuration
String keyFromConfiguration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
// Data from server
String ivFromHttpHeader = "000000000000000000000000";
String authTagFromHttpHeader = "CE573FB7A41AB78E743180DC83FF09BD";
String httpBody = "0A3471C72D9BE49A8520F79C66BBD9A12FF9";
// Convert data to process
byte[] key = DatatypeConverter.parseHexBinary(keyFromConfiguration);
byte[] iv = DatatypeConverter.parseHexBinary(ivFromHttpHeader);
byte[] authTag = DatatypeConverter.parseHexBinary(authTagFromHttpHeader);
byte[] encryptedText = DatatypeConverter.parseHexBinary(httpBody);
// Unlike other programming languages, we have to append the auth tag at the end of encrypted text in Java
byte[] cipherText = ArrayUtils.addAll(encryptedText, authTag);
// Prepare decryption
SecretKeySpec keySpec = new SecretKeySpec(key, 0, 16, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
// Decrypt
byte[] bytes = cipher.doFinal(cipherText);
System.out.println(new String(bytes, Charsets.UTF_8));
}
}var crypto = require("crypto");
// Data from configuration
var secretFromConfiguration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
// Data from server
var ivfromHttpHeader = "000000000000000000000000";
var authTagFromHttpHeader = "CE573FB7A41AB78E743180DC83FF09BD";
var httpBody = "0A3471C72D9BE49A8520F79C66BBD9A12FF9";
// Convert data to process
var key = new Buffer(secretFromConfiguration, "hex");
var iv = new Buffer(ivfromHttpHeader, "hex");
var authTag = new Buffer(authTagFromHttpHeader, "hex");
var cipherText = new Buffer(httpBody, "hex");
// Prepare decryption
var decipher = crypto.createDecipheriv("aes-128-gcm", key, iv);
decipher.setAuthTag(authTag);
// Decrypt
var result = decipher.update(cipherText) + decipher.final();
console.log(result);<?php
/* Php 7.1 or later */
$key_from_configuration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
$iv_from_http_header = "000000000000000000000000";
$auth_tag_from_http_header = "CE573FB7A41AB78E743180DC83FF09BD";
$http_body = "0A3471C72D9BE49A8520F79C66BBD9A12FF9";
$key = hex2bin($key_from_configuration);
$iv = hex2bin($iv_from_http_header);
$auth_tag = hex2bin($auth_tag_from_http_header);
$cipher_text = hex2bin($http_body);
$result = openssl_decrypt($cipher_text, "aes-128-gcm", $key, OPENSSL_RAW_DATA, $iv, $auth_tag);
print($result);
/* Php prior to 7.1 */
/* Use [Libsodium in PHP projects](https://paragonie.com/book/pecl-libsodium/read/08-advanced.md#crypto-aead-aes256gcm) */
$key_from_configuration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f";
$iv_from_http_header = "000000000000000000000000";
$auth_tag_from_http_header = "CE573FB7A41AB78E743180DC83FF09BD";
$http_body = "0A3471C72D9BE49A8520F79C66BBD9A12FF9";
$key = hex2bin($key_from_configuration);
$iv = hex2bin($iv_from_http_header);
$cipher_text = hex2bin($http_body . $auth_tag_from_http_header);
$result = \Sodium\crypto_aead_aes128gcm_decrypt($cipher_text, NULL, $iv, $key);
print($result);
?>import os
import binascii
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# Data from configuration
key_from_configuration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f" # 16 bytes for AES-128
# Data from server
iv_from_http_header = "000000000000000000000000"
auth_tag_from_http_header = "CE573FB7A41AB78E743180DC83FF09BD"
http_body = "0A3471C72D9BE49A8520F79C66BBD9A12FF9" # ciphertext
# Convert data to process
key = binascii.unhexlify(key_from_configuration)
iv = binascii.unhexlify(iv_from_http_header)
auth_tag = binascii.unhexlify(auth_tag_from_http_header)
cipher_text = binascii.unhexlify(http_body)
# Prepare decryption
decryptor = Cipher(algorithms.AES(key), modes.GCM(iv, auth_tag), backend = default_backend()).decryptor()
# Decrypt
result = decryptor.update(cipher_text) + decryptor.finalize()
# Print the result
print(result.decode('utf-8', errors='ignore'))require("openssl")
# Convert hexadecimal string
def convert(hex)
return [hex].pack("H*")
end
# Create new decipher
def new_decipher(key, iv)
cipher = OpenSSL::Cipher.new("aes-128-gcm")
cipher.decrypt
cipher.key = key
cipher.iv = iv
return cipher
end
# Data from configuration
key_from_configuration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"
# Data from server
iv_from_http_header = "000000000000000000000000"
auth_tag_from_http_header = "CE573FB7A41AB78E743180DC83FF09BD"
http_body = "0A3471C72D9BE49A8520F79C66BBD9A12FF9"
# Convert data to process
key = convert(key_from_configuration)
iv = convert(iv_from_http_header)
auth_tag = convert(auth_tag_from_http_header)
cipher_text = convert(http_body)
# Prepare decryption
decipher = new_decipher(key, iv)
decipher.auth_tag = auth_tag
# Decrypt
result = decipher.update(cipher_text) + decipher.final
puts resultimport java.nio.charset.Charset;
import java.security.Security
import java.security.SecureRandom;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider
// For JVM-based languages, you might need to install unrestricted policy files provided by Sun Microsystems.
// If you encounter errors like `java.lang.SecurityException: Unsupported keysize or algorithm parameters` or `java.security.InvalidKeyException: Illegal key size`, check the
// BouncyCastle FAQ page(https://www.bouncycastle.org/about/bouncy-castle-fips-faq/) for guidance.
// If installing the unrestricted policy files isn't an option, try using reflection as a workaround, as explained in this StackOverflow discussion on avoiding the need for unlimited strength JCE policy files(https://stackoverflow.com/questions/1179672/how-to-avoid-installing-unlimited-strength-jce-policy-files-when-deploying-an).
object Cipher {
def main(args: Array[String]) = {
Security.addProvider(new BouncyCastleProvider())
// Data from configuration
val keyFromConfiguration = "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"
// Data from server
val ivFromHttpHeader = "000000000000000000000000"
val authTagFromHttpHeader = "CE573FB7A41AB78E743180DC83FF09BD"
val httpBody = "0A3471C72D9BE49A8520F79C66BBD9A12FF9"
// Convert data to process
val key = hexToBin(keyFromConfiguration)
val iv = hexToBin(ivFromHttpHeader)
val authTag = hexToBin(authTagFromHttpHeader)
val encryptedText = hexToBin(httpBody)
// Unlike other programming languages, we have to append the auth tag at the end of encrypted text
val cipherText = encryptedText ++ authTag
// Prepare decryption
val keySpec = new SecretKeySpec(key, 0, 16, "AES")
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv))
// Decrypt
val result = cipher.doFinal(cipherText)
println(new String(result, "UTF-8"))
}
def hexToBin(hex: String) : Array[Byte] = {
return DatatypeConverter.parseHexBinary(hex)
}
}Response handling
When your system receives a webhook, it must respond with a 2xx HTTP status code (for example, 200 OK). This confirms successful delivery and prevents retries.
If your server:
- Returns a non-2xx status code, or
- Fails to respond within 30 seconds,
the webhook is considered undelivered and will be retried according to the retry policy.
Protocol requirements
| Parameter | Requirement |
|---|---|
| Protocol | HTTPS (TLS 1.2 or higher) |
| Method | POST |
| Content-type | text/plain or application/json (based on wrapper) |
| SSL certificates | Must be valid and trusted; self-signed certificates are not accepted in production |
Best practices
- Ensure your endpoint is always available and can respond
- Use asynchronous processing to avoid delays in response
- Monitor for non-2xx responses and fix issues immediately
- Validate that your SSL certificate chain is complete and trusted
Updated about 7 hours ago