Webhooks

Webhooks let you receive real-time updates on the progress and outcome of Sticky Recovery sessions. This enables immediate downstream actions, such as internal logging, customer notifications, or dashboard updates, without needing to poll the API.

Configuring Webhook Endpoints

To start receiving webhooks:

1️⃣ Go to Developer > Webhooks in the Sticky Recovery UI.

2️⃣ Add the URL(s) where you want events to be sent.

3️⃣ Select one or more of the following supported event types.



Supported Webhook Events

Webhook EventDescription
recovery_startedTriggered when a recovery session is created.
recovery_attempt_failedFired when a retry attempt fails.
recovery_successfulFired when a retry attempt results in a successful payment.
recovery_unsuccessfulSent when the recovery session ends without success (max retries or cancel).

Webhook Encryption Key Phrase

To secure webhook delivery, Sticky encrypts all payloads using a symmetric encryption key that you must configure in advance. This key is referred to as the webhook encryption key phrase.

Requirements:

  • Must be exactly 32 characters
  • Alphanumeric only (letters A-Z, numbers 0-9)
  • Case-sensitive (e.g., A ≠ a)

Sending Test event

To help validate your integration setup, Sticky provides a Send Test Event feature within the Developer > Webhooks UI. This sends a mock webhook payload to your configured endpoint to confirm delivery and decryption.




The test payload will be delivered encrypted using your configured webhook secret and will decode to the following JSON:

{
  "message": "Congrats! You successfully received the webhook.",
  "reference": "See the integration guide for more information on handling webhook responses.",
  "URL": "https://sticky.readme.io/docs/sticky-recovery-integration-guide-recover#-webhooks-optional"
}

This response confirms that:

  • Your webhook URL is reachable and responsive.
  • Your decryption key is correctly implemented.
  • Sticky can deliver events securely to your endpoint.

Decryption Code Snippets

public String decrypt(String key, String ciphertext) {
    byte[] cipherbytes = Base64.getDecoder().decode(ciphertext);
    byte[] initVector = Arrays.copyOfRange(cipherbytes,0,16);
    byte[] messagebytes = Arrays.copyOfRange(cipherbytes,16,cipherbytes.length);
    IvParameterSpec iv = new IvParameterSpec(initVector);
    SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
    byte[] byte_array = cipher.doFinal(messagebytes);
    return new String(byte_array, StandardCharsets.UTF_8);
}
from Crypto.Cipher import AES
import base64
import sys
import os

# AES 'pad' byte array to multiple of BLOCK_SIZE bytes
def pad(byte_array):
    BLOCK_SIZE = 16
    pad_len = BLOCK_SIZE - len(byte_array) % BLOCK_SIZE
    return byte_array + (bytes([pad_len]) * pad_len)

# Remove padding at end of byte array
def unpad(byte_array):
    last_byte = byte_array[-1]
    return byte_array[0:-last_byte]

def encrypt(key, message):
    """
    Input String, return base64 encoded encrypted String
    """
    byte_array = message.encode("UTF-8")
    padded = pad(byte_array)

    # generate a random iv and prepend that to the encrypted result.
    # The recipient then needs to unpack the iv and use it.
    iv = os.urandom(AES.block_size)
    cipher = AES.new( key.encode("UTF-8"), AES.MODE_CBC, iv )
    encrypted = cipher.encrypt(padded)
    # Note we PREPEND the unencrypted iv to the encrypted message
    return base64.b64encode(iv+encrypted).decode("UTF-8")

def decrypt(key, message):
    """
    Input encrypted bytes, return decrypted bytes, using iv and key
    """
    byte_array = base64.b64decode(message)
    iv = byte_array[0:16] # extract the 16-byte initialization vector
    messagebytes = byte_array[16:] # encrypted message is the bit after the iv
    cipher = AES.new(key.encode("UTF-8"), AES.MODE_CBC, iv )
    decrypted_padded = cipher.decrypt(messagebytes)
    decrypted = unpad(decrypted_padded)
    return decrypted.decode("UTF-8");

def main():
    key = 'YOUR_KEY_HERE'
    message = 'YOUR_ENCRYPTED_MESSAGE'
    print(decrypt(key,message))

main()

Decrypted JSON Encoded Payloads

recovery_started

{
  "clientAppKey": "7bebb0d5-ea45-44aa-9122-fb522eb12314",
  "type": "recovery.started",
  "time": "2025-05-23T19:48:17.097Z",
  "data": {
    "stickyTransactionId": "249fa7da9125b036011e97d24719586f7442d97405125801f9",
    "gatewayProfileId": "1568",
    "dunningProfileId": "297",
    "dunningSessionId": "2025060518423881610"
  }
}

recovery_attempt_failed

{
  "clientAppKey": "7bebb0d5-ea45-44aa-9122-fb522eb12314",
  "type": "recovery.attempt_failed",
  "time": "2025-05-23T19:48:17.097Z",
  "data": {
    "stickyTransactionId": "054440bf260fd6a205f20b83dea7e16401d440f70d844d9399",
    "gatewayTransactionId": "100019740BA5CBB", 
    "amount": 50000,
    "currency": "USD",
    "gatewayProfileName": "RocketGate",
    "gatewayProfile": "RocketGate",
    "gatewayProfileId": "12345",
    "dunningProfileId": "297",
    "dunningSessionId": "2025060518423881610",
    "attemptNumber": 1,
    "gatewayResponse": {
     //GATEWAY RESPONSE
    }
  }
}

recovery_successful

{
  "clientAppKey": "7bebb0d5-ea45-44aa-9122-fb522eb12314",
  "type": "recovery.successful",
  "time": "2025-05-23T19:48:17.097Z",
  "data": {
    "stickyTransactionId": "054440bf260fd6a205f20b83dea7e16401d440f70d844d9399",
    "gatewayTransactionId": "100019740BA5CBB",
    "dunningSessionId": "2025060518423881610",
    "dunningProfileId": "297",
    "amount": 50000,
    "currency": "USD",
    "gatewayProfileName": "RocketGate",
    "gatewayProfile": "RocketGate",
    "gatewayProfileId": "12345",
    "gatewayResponse": { 
		  //GATEWAY RESPONSE
    }
  }
}

recovery_unsuccessful

{
  "clientAppKey": "7bebb0d5-ea45-44aa-9122-fb522eb12314",
  "type": "recovery.unsuccessful",
  "time": "2025-05-23T19:48:17.097Z",
   "data": {
    "stickyTransactionId": "054440bf260fd6a205f20b83dea7e16401d440f70d844d9399",
    "dunningSessionId": "2025060518423881610",
  }
}

Webhook Retries

Sticky automatically retries webhook delivery if your endpoint:

  • Times out
  • Returns an HTTP status between 500 and 503
    • 500 - Internal Server Error: A generic error from the server when no specific message is available. Usually indicates an unexpected failure.
    • 501 - Not Implemented: The server doesn't support the functionality required to fulfill the request. Rare in webhook contexts.
    • 502 - Bad Gateway: The server (often a reverse proxy or gateway) received an invalid response from an upstream server.
    • 503 - Service Unavailable: The server is temporarily unable to handle the request. Often due to overload or maintenance.

Retry Policy:

  • 5 attempts total per failed delivery.
  • Interval: Every 6 hours, controlled via cron (runs 4 times daily).