Decrypt Webhook Payload
Securely decrypt and verify webhook payloads
All OZZOBiT webhooks are encrypted using AES-256-GCM encryption to ensure data integrity and prevent tampering. You must decrypt payloads using your webhook secret key before processing them.
Get Your Webhook Secret
- Navigate to your Partner Dashboard
- Go to Settings → Webhooks
- Copy your Webhook Secret Key
- Store it securely in your environment variables
Decryption Implementation
OZZOBiT-webhook.tstypescript
// lib/OZZOBiT-webhook.ts
import crypto from 'crypto'
const ALGORITHM = 'aes-256-gcm'
const AUTH_TAG_LENGTH = 16
const IV_LENGTH = 12
interface DecryptedPayload {
eventName: string
data: Record<string, any>
timestamp: string
}
export function decryptWebhookPayload(
encryptedPayload: string,
webhookSecret: string
): DecryptedPayload {
// The payload is base64 encoded
const encryptedBuffer = Buffer.from(encryptedPayload, 'base64')
// Extract components (OZZOBiT format: iv + authTag + ciphertext)
const iv = encryptedBuffer.subarray(0, IV_LENGTH)
const authTag = encryptedBuffer.subarray(
IV_LENGTH,
IV_LENGTH + AUTH_TAG_LENGTH
)
const ciphertext = encryptedBuffer.subarray(IV_LENGTH + AUTH_TAG_LENGTH)
// Create decipher
const decipher = crypto.createDecipheriv(
ALGORITHM,
Buffer.from(webkeySecret.padStart(32, '0').slice(0, 32), 'utf-8'),
iv
)
decipher.setAuthTag(authTag)
// Decrypt
let decrypted: string
try {
decrypted = decipher.update(ciphertext, undefined, 'utf8')
decrypted += decipher.final('utf8')
} catch (error) {
throw new Error(`Decryption failed: ${error.message}`)
}
return JSON.parse(decrypted)
}
// Verify signature (additional security layer)
export function verifyWebhookSignature(
payload: string,
signature: string,
webhookSecret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
// Next.js Route Handler example:
// app/api/webhooks/OZZOBiT/route.ts
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
// Get raw body for signature verification
const payload = await request.text()
const signature = request.headers.get('x-OZZOBiT-signature')!
// Verify signature first
if (!verifyWebhookSignature(payload, signature, process.env.OZZOBiT_WEBHOOK_SECRET!)) {
return new Response('Invalid signature', { status: 401 })
}
// Decrypt payload
const event = decryptWebhookPayload(
payload,
process.env.OZZOBiT_WEBHOOK_SECRET!
)
console.log('Received event:', event.eventName)
// Handle event...
switch (event.eventName) {
case 'ORDER_COMPLETED':
await handleOrderCompleted(event.data)
break
case 'ORDER_FAILED':
await handleOrderFailed(event.data)
break
}
return Response.json({ success: true })
}OZZOBiTWebhookDecryptor.javajava
// OZZOBiTWebhookDecryptor.java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class OZZOBiTWebhookDecryptor {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
public static String decrypt(String encryptedPayload, String webhookSecret) throws Exception {
byte[] decoded = Base64.getDecoder().decode(encryptedPayload);
// Extract IV (first 12 bytes)
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decoded, 0, iv, 0, GCM_IV_LENGTH);
// Prepare key (pad or truncate to 32 bytes for AES-256)
byte[] keyBytes = Arrays.copyOf(
webhookSecret.getBytes(StandardCharsets.UTF_8),
32
);
// Initialize cipher
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
// Decrypt (remaining bytes after IV)
byte[] ciphertext = Arrays.copyOfRange(decoded, GCM_IV_LENGTH, decoded.length);
byte[] decrypted = cipher.doFinal(ciphertext);
return new String(decrypted, StandardCharsets.UTF_8);
}
public static boolean verifySignature(
String payload,
String receivedSignature,
String webhookSecret
) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
webhookSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(keySpec);
byte[] expectedHash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expectedSig = Base64.getEncoder().encodeToString(expectedHash);
return MessageDigest.isEqual(
expectedSig.getBytes(StandardCharsets.UTF_8),
receivedSignature.getBytes(StandardCharsets.UTF_8)
);
}
}
// Spring Boot Controller example:
@RestController
@RequestMapping("/api/webhooks")
public class OZZOBiTWebhookController {
@Value("${OZZOBiT.webhook.secret}")
private String webhookSecret;
@PostMapping("/OZZOBiT")
public ResponseEntity<?> handleWebhook(
@RequestBody String payload,
@RequestHeader("X-OZZOBiT-Signature") String signature
) {
try {
// Verify signature
if (!OZZOBiTWebhookDecryptor.verifySignature(payload, signature, webhookSecret)) {
return ResponseEntity.status(401).body("Invalid signature");
}
// Decrypt payload
String decrypted = OZZOBiTWebhookDecryptor.decrypt(payload, webhookSecret);
JsonObject event = JsonParser.parseString(decrypted).getAsJsonObject();
String eventName = event.get("eventName").getAsString();
switch (eventName) {
case "ORDER_COMPLETED":
handleOrderCompleted(event.getAsJsonObject("data"));
break;
case "ORDER_FAILED":
handleOrderFailed(event.getAsJsonObject("data"));
break;
}
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
log.error("Webhook processing error", e);
return ResponseEntity.status(500).body("Processing error");
}
}
}OZZOBiT_webhook.gogo
// OZZOBiT_webhook.go
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"io"
)
const (
algorithm = "aes-256-gcm"
ivLength = 12
tagLength = 16
)
type WebhookEvent struct {
EventName string `json:"eventName"`
Data map[string]interface{} `json:"data"`
Timestamp string `json:"timestamp"`
}
func DecryptWebhookPayload(encryptedPayload, webhookSecret string) (*WebhookEvent, error) {
// Decode base64
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPayload)
if err != nil {
return nil, err
}
// Extract IV (first 12 bytes)
if len(ciphertext) < ivLength+tagLength {
return nil, errors.New("payload too short")
}
iv := ciphertext[:ivLength]
tag := ciphertext[ivLength : ivLength+tagLength]
encData := ciphertext[ivLength+tagLength:]
// Prepare key
key := []byte(webhookSecret)
if len(key) > 32 {
key = key[:32]
}
for len(key) < 32 {
key = append(key, 0)
}
// Create block cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Create GCM mode with tag appended
gcm, err := cipher.NewGCMWithTagSize(block, tagLength*8)
if err != nil {
return nil, err
}
// Decrypt (ciphertext includes tag)
plaintext, err := gcm.Open(nil, iv, append(encData, tag...), nil)
if err != nil {
return nil, err
}
var event WebhookEvent
if err := json.Unmarshal(plaintext, &event); err != nil {
return nil, err
}
return &event, nil
}
func VerifyWebhookSignature(payload, signature, webhookSecret string) bool {
mac := hmac.New(sha256.New, []byte(webhookSecret))
io.WriteString(mac, payload)
expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expectedSig))
}
// HTTP handler example (using net/http):
func OZZOBiTWebhookHandler(w http.ResponseWriter, r *http.Request) {
// Read body
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
signature := r.Header.Get("X-OZZOBiT-Signature")
// Verify signature
if !VerifyWebhookSignature(string(body), signature, webhookSecret) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Invalid signature"))
return
}
// Decrypt
event, err := DecryptWebhookPayload(string(body), webhookSecret)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Decryption failed"))
return
}
// Handle event
switch event.EventName {
case "ORDER_COMPLETED":
handleOrderCompleted(event.Data)
case "ORDER_FAILED":
handleOrderFailed(event.Data)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}Important Security Notes
- Always verify the signature before decrypting
- Use constant-time comparison (
crypto.timingSafeEqual) to prevent timing attacks - Never log the raw encrypted payload or your webhook secret
- Return HTTP 200 quickly even if processing fails to avoid unnecessary retries
Testing Your Webhook Handler
Use a Tunneling Service
Use ngrok, cloudflared tunnel, or similar to expose your local endpoint:
bashbash
ngrok http 3000Register the URL
Add your tunnel URL (e.g., https://abc123.ngrok.io/api/webhooks/OZZOBiT) in your dashboard under Settings → Webhooks.
Trigger a Test Webhook
Create a sandbox order through the widget or API to trigger a test webhook delivery.