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 typeRecommended approachReason
Real-time decisions (for example, capture, fulfillment)Transaction Status query APIEnsures immediate and reliable status
Reporting & reconciliationWebhooksDelays are acceptable; automation-friendly
Non-critical automationWebhooksEfficient and event-driven
High-frequency pollingNot recommendedUse webhooks or transaction export via SFTP instead

Retry amd failure behavior

BehaviorDescription
TimeoutNo response within 30 seconds → marked as failed
FailureNon-2xx HTTP response → marked as failed
Retry intervals1 min → 2 min → 4 min → 8 min → 15 min → 30 min → 1 hour → daily (up to 30 days)
Failing retry patternRetries pause if all messages fail at a given interval; resume once delivery succeeds
Daily failure summaryEmail with up to 100 failed notifications per endpoint
RetentionFailed 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

FeatureDescription
Entity scopeA webhook receives events from its entity and all child entities
Multiple endpointsThe same notification is sent to all active webhook URLs configured at or above the entity level
FilteringYou can filter which types of events (for example, payments, risks) each webhook should receive
Automatic retriesFailed deliveries are automatically retried based on the retry policy
Failure alertsDaily email alerts are sent when delivery fails repeatedly

Add a webhook

To add a webhook, configure the following parameters:

ParameterDescription
URLThe public endpoint that will receive the webhook notifications
TypesEvent categories to subscribe to: PAYMENTS, REGISTRATIONS, SCHEDULES, RISKS
FieldsChoose ALL (full payload) or NON_CUSTOMER_DATA (exclude sensitive fields)
SecretA 64-character hex string used to encrypt the payload
WrapperFormat of the payload: None (hex string) or JSON (wrapped in JSON)
EmailsOne 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

FieldDescription
typeThe category of the event: PAYMENT, REGISTRATION, SCHEDULE, or RISK
actionOnly present for REGISTRATION events. Indicates the change: CREATED, UPDATED, or DELETED
payloadThe 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

ParameterDescription
AlgorithmAES (Advanced Encryption Standard)
Key64-character hexadecimal string (configured in the webhook settings)
Key length256 bits (32 bytes)
Block modeGCM (Galois/Counter Mode)
PaddingNone
Initialization vectorSent in HTTP header X-Initialization-Vector (hexadecimal)
Authentication tagSent in HTTP header X-Authentication-Tag (hexadecimal)
Payload formatEncrypted hexadecimal string in the body
Wrapper optionNone (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
  • JSON wrapper
    • Content-Type: application/json
    • Body format: {"encryptedBody": "hexadecimal\_string"}

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:

ComponentSource
Encrypted bodyIn the HTTP request body (hexadecimal string or JSON wrapper)
Secret keyConfigured in the webhook settings (64-character hex string)
Initialization vectorIn HTTP header X-Initialization-Vector
Authentication tagIn HTTP header X-Authentication-Tag
AlgorithmAES-256-GCM (no padding)

Decryption steps

  1. Extract the encrypted body from the request (either raw or from the encryptedBody field in JSON).
  2. Read the initialization vector and authentication tag from the headers.
  3. Use the configured secret key to decrypt the payload using AES-256-GCM.
  4. Validate the authentication tag to ensure integrity.
  5. 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 result
import 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

ParameterRequirement
ProtocolHTTPS (TLS 1.2 or higher)
MethodPOST
Content-typetext/plain or application/json (based on wrapper)
SSL certificatesMust 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