LogoLogo
Go to WebsiteLoginGet a Demo
  • Introduction
  • Getting Started
  • Authentication
  • Users
    • GET /users
    • GET /users/{id}
  • User Groups
    • GET /UsersGroups/
    • GET /UsersGroups/{usersGroupId}
    • GET /UsersGroups/{usersGroupId}/DefaultWorkApps
    • GET /UsersGroups/{usersGroupId}/HiddenWorkApps
    • GET /UsersGroups/{usersGroupId}/Users
    • POST /UsersGroups/{usersGroupId}/Users
    • DELETE /UsersGroups/{usersGroupId}/Users/{userId}
  • Applications
    • GET /WorkApps
    • GET /WorkApps/{workAppId}
    • GET /WorkApps/{workAppId}/Accesses
    • GET /WorkApps/{workAppId}/PermissionLevels
    • POST /WorkApps/Accesses
    • PUT /WorkApps/{workAppId}/Policies/{requestType}
    • PUT /WorkApps/{workAppId}/PermissionLevels/{permissionLevelId}/Policies/{requestType}
  • Tasks
    • GET /Tasks/{taskId}
    • POST /Tasks/{taskId}/Approve
    • POST /Tasks/{taskId}/Decline
  • Policies
    • GET /Policies/CompatiblePolicies/{requestType}
  • Webhooks
    • Webhook’s signature verification
  • Changelog
Powered by GitBook

Links

  • Privacy policy
  • Imprint
  • Support

© All right reserved, Cakewalk Technology GmbH 2025

On this page

Was this helpful?

  1. Webhooks

Webhook’s signature verification

To ensure the authenticity and integrity of webhook requests, Cakewalk signs each payload with a cryptographic signature. When your application receives a webhook, it must verify this signature using the public key provided by Cakewalk. The signature is included in the X-SIGNATURE header, and it is generated using the SHA hash of the raw request body, signed with Cakewalk’s private RSA key.

Your server retrieves the corresponding public key from the https://open-api.getcakewalk.io/api/Keys endpoint, using your API credentials for authorization. By verifying the signature against the raw payload using this public key, your application can confirm that the request was not tampered with and was genuinely sent by Cakewalk.

📋 Prepare configuration options

1. Define a configuration class:

public class CakewalkApiOptions
{
    public const string SectionName = "CakewalkApi";

    public string ApiKey { get; set; } = string.Empty;
    public string ApiSecret { get; set; } = string.Empty;
    public string PublicKeyEndpoint { get; set; } = "<https://open-api.getcakewalk.io/api/Keys>";
}

2. Register configuration in Program.cs or Startup.cs:

builder.Services.Configure<CakewalkApiOptions>(
    builder.Configuration.GetSection(CakewalkApiOptions.SectionName));

✅ Environment variables like CakewalkApi__ApiKey will automatically be bound if you use double underscores (__) in the name.


🔐 Signature Verification with Remote Public Key

SignatureService Implementation:

public class SignatureService : ISignatureService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly CakewalkApiOptions _options;
    private RSA? _publicKey;

    public SignatureService(
        IHttpClientFactory httpClientFactory,
        IOptions<CakewalkApiOptions> optionsAccessor)
    {
        _httpClientFactory = httpClientFactory;
        _options = optionsAccessor.Value;
    }

    public async Task<bool> VerifyAsync(string payload, string signature)
    {
        if (_publicKey == null)
        {
            await LoadPublicKeyAsync();
        }

        if (_publicKey == null)
        {
            throw new InvalidOperationException("Public key could not be loaded.");
        }

        var data = Encoding.UTF8.GetBytes(payload);
        var signatureBytes = Convert.FromBase64String(signature);

        return _publicKey.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    }

    private async Task LoadPublicKeyAsync()
    {
        var client = _httpClientFactory.CreateClient();
        var request = new HttpRequestMessage(HttpMethod.Get, _options.PublicKeyEndpoint);
        request.Headers.Add("X-API-KEY", _options.ApiKey);
        request.Headers.Add("X-API-SECRET", _options.ApiSecret);

        using var response = await client.SendAsync(request);
        response.EnsureSuccessStatusCode();

        var pemContent = await response.Content.ReadAsStringAsync();

        _publicKey = RSA.Create();
        _publicKey.ImportFromPem(pemContent.ToCharArray());
    }
}

🧪 Interface and Registration

Interface:

public interface ISignatureService
{
    Task<bool> VerifyAsync(string payload, string signature);
}

Dependency Injection in Program.cs:

builder.Services.AddHttpClient(); // Register HttpClientFactory
builder.Services.AddSingleton<ISignatureService, SignatureService>();

🛡️ Implement the webhook endpoint with the validation logic

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("api/webhook")]
public class WebhookController : ControllerBase
{
    private readonly ISignatureService _signatureService;
    private readonly ILogger<WebhookController> _logger;

    public WebhookController(ISignatureService signatureService, ILogger<WebhookController> logger)
    {
        _signatureService = signatureService;
        _logger = logger;
    }

    [HttpPost]
    public async Task<IActionResult> ReceiveWebhook()
    {
        // Validate signature
        var (isValid, body) = await ValidateSignatureAsync(Request);

        if (!isValid)
        {
            _logger.LogWarning("Invalid webhook signature.");
            return Unauthorized("Invalid signature.");
        }

        _logger.LogInformation("Valid webhook received: {Payload}", body);

        // Deserialize and handle payload as needed
        // Example
        try
        {
            var model = JsonSerializer.Deserialize<WebhookPayload>(body);
            if (model == null)
            {
                return BadRequest("Invalid payload format.");
            }

            // Process the payload
            // ...

            return Ok();
        }
        catch (JsonException ex)
        {
            _logger.LogError(ex, "Failed to deserialize webhook payload.");
            return BadRequest("Invalid JSON format.");
        }
    }

    private async Task<(bool isValid, string body)> ValidateSignatureAsync(HttpRequest request)
    {
        if (!request.Headers.TryGetValue("X-SIGNATURE", out var signatureHeader))
        {
            throw new Exception("Missing X-SIGNATURE header.");
        }

        request.EnableBuffering(); // Allow reading body multiple times

        using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
        var rawBody = await reader.ReadToEndAsync();
        request.Body.Position = 0;

        var isValid = await _signatureService.VerifyAsync(rawBody, signatureHeader!);

        return (isValid, rawBody);
    }
}

✅ Sample appsettings.json or environment variables

{
  "CakewalkApi": {
    "ApiKey": "your-api-key-here",
    "ApiSecret": "your-api-secret-here",
    "PublicKeyEndpoint": "<https://open-api.getcakewalk.io/api/Keys>"
  }
}

Or via environment variables:

CakewalkApi__ApiKey=your-api-key
CakewalkApi__ApiSecret=your-api-secret

📦 Project Structure

cakewalk_webhook/
├── config.py
├── signature_service.py
├── models.py
├── main.py
├── requirements.txt

config.py — 🔧 Configuration

from pydantic import Field
from pydantic_settings import BaseSettings

class CakewalkSettings(BaseSettings):
    api_key: str = Field(..., alias="CAKEWALK_API_KEY")
    api_secret: str = Field(..., alias="CAKEWALK_API_SECRET")
    public_key_endpoint: str = "<https://open-api.getcakewalk.io/api/Keys>"

    class Config:
        env_file = ".env"

settings = CakewalkSettings()

signature_service.py — 🔐 Signature Verification Logic

import base64
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature
from config import settings

class SignatureService:
    def __init__(self):
        self._public_key = None

    def _load_public_key(self):
        response = requests.get(
            settings.public_key_endpoint,
            headers={
                "X-API-KEY": settings.api_key,
                "X-API-SECRET": settings.api_secret,
            },
            timeout=10,
        )
        response.raise_for_status()
        pem_data = response.text.encode("utf-8")
        self._public_key = serialization.load_pem_public_key(pem_data)

    def verify(self, payload: bytes, signature_b64: str) -> bool:
        if self._public_key is None:
            self._load_public_key()

        signature = base64.b64decode(signature_b64)

        try:
            self._public_key.verify(
                signature,
                payload,
                padding.PKCS1v15(),
                hashes.SHA256(),
            )
            return True
        except InvalidSignature:
            return False

models.py — 📄 Webhook Payload (Example)

from pydantic import BaseModel, Field

class WebhookPayload(BaseModel):
    event_type: str = Field(..., alias="eventType")
    data: dict

main.py — 🚀 FastAPI App with Validation

import json
from typing import List
from fastapi import FastAPI, Request, Header, HTTPException, status
from pydantic import parse_obj_as
from signature_service import SignatureService
from models import WebhookPayload

app = FastAPI()
signature_service = SignatureService()

@app.post("/webhook")
async def receive_webhook(request: Request, x_signature: str = Header(None)):
    if not x_signature:
        raise HTTPException(status_code=400, detail="Missing X-SIGNATURE header")

    raw_body = await request.body()

    if not signature_service.verify(payload=raw_body, signature_b64=x_signature):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid signature")

    try:
		    data = json.loads(raw_body)
        payload = parse_obj_as(List[WebhookPayload], data)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid payload")

    # 🎯 Your webhook processing logic here
    print("✅ Received verified payload:", [event.dict() for event in payload])

    return {"status": "success"}

requirements.txt

fastapi
uvicorn
pydantic
pydantic-settings
requests
cryptography

✅ .env File or Environment Variables

CAKEWALK_API_KEY=your-api-key
CAKEWALK_API_SECRET=your-api-secret

▶️ Run Your Webhook Server

uvicorn main:app --reload

🧱 Project Structure:

com.example.cakewalk
├── CakewalkWebhookApplication.java        # 🚀 Spring Boot main class
├── config
│   └── CakewalkApiProperties.java         # ⚙️ Loads API credentials and endpoint
├── controller
│   └── WebhookController.java             # 🛡️ Handles incoming webhooks
├── model
│   └── WebhookPayload.java                # 📦 Matches the expected webhook JSON structure
├── service
│   └── SignatureService.java              # 🔐 Fetches & uses public key to verify signature

CakewalkApiProperties - 📋 Configuration Class

package com.example.cakewalk.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "cakewalk.api")
public class CakewalkApiProperties {
    private String apiKey;
    private String apiSecret;
    private String publicKeyEndpoint = "<https://open-api.getcakewalk.io/api/Keys>";

    public String getApiKey() { return apiKey; }
    public void setApiKey(String apiKey) { this.apiKey = apiKey; }

    public String getApiSecret() { return apiSecret; }
    public void setApiSecret(String apiSecret) { this.apiSecret = apiSecret; }

    public String getPublicKeyEndpoint() { return publicKeyEndpoint; }
    public void setPublicKeyEndpoint(String publicKeyEndpoint) { this.publicKeyEndpoint = publicKeyEndpoint; }
}

SignatureService - 🔐 Signature Verification with Remote Public Key

package com.example.cakewalk.service;

import com.example.cakewalk.config.CakewalkApiProperties;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import jakarta.annotation.PostConstruct;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;

@Service
public class SignatureService {

    @Qualifier("cakewalkApiProperties")
    @Autowired
    private CakewalkApiProperties config;

    private PublicKey publicKey;

    @PostConstruct
    public void init() {
        loadPublicKey();
    }

    public boolean verify(String payload, String signatureBase64) {
        try {
            if (publicKey == null) {
                loadPublicKey();
            }

            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initVerify(publicKey);
            signature.update(payload.getBytes(StandardCharsets.UTF_8));

            byte[] signatureBytes = Base64.decodeBase64(signatureBase64);
            return signature.verify(signatureBytes);
        } catch (Exception e) {
            throw new RuntimeException("Signature verification failed", e);
        }
    }

    private void loadPublicKey() {
        try {
            RestTemplate restTemplate = new RestTemplate();
            var headers = new HttpHeaders();
            headers.set("X-API-KEY", config.getApiKey());
            headers.set("X-API-SECRET", config.getApiSecret());

            var entity = new HttpEntity<>(headers);
            var response = restTemplate.exchange(
                    URI.create(config.getPublicKeyEndpoint()),
                    HttpMethod.GET,
                    entity,
                    String.class
            );

            String pem = response.getBody()
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replace("-----END PUBLIC KEY-----", "")
                    .replaceAll("\\\\s+", "");

            byte[] keyBytes = Base64.decodeBase64(pem);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            this.publicKey = keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            throw new RuntimeException("Failed to load public key", e);
        }
    }
}

WebhookController - 🛡️ Implement the webhook endpoint with the validation logic

package com.example.cakewalk.controller;

import com.example.cakewalk.model.WebhookPayload;
import com.example.cakewalk.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;

import java.nio.charset.StandardCharsets;
import java.util.List;

@RestController
@RequestMapping("/api/webhook")
public class WebhookController {

    @Autowired
    private SignatureService signatureService;

    @Autowired
    private ObjectMapper objectMapper;

    @PostMapping
    public ResponseEntity<String> receiveWebhook(HttpServletRequest request) {
        try {
            String signature = request.getHeader("X-SIGNATURE");
            if (signature == null) {
                return ResponseEntity.status(401).body("Missing signature.");
            }

            String rawBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);

            boolean isValid = signatureService.verify(rawBody, signature);
            if (!isValid) {
                return ResponseEntity.status(401).body("Invalid signature.");
            }

            // ✅ Deserialize the array
            List<WebhookPayload> payloadList = objectMapper.readValue(
                    rawBody,
                    objectMapper.getTypeFactory().constructCollectionType(List.class, WebhookPayload.class)
            );

            for (WebhookPayload payload : payloadList) {
                // Process each payload
            }

            return ResponseEntity.ok("Webhook received.");
        } catch (Exception e) {
            return ResponseEntity.status(400).body("Failed to process webhook.");
        }
    }
}

application.properties Example

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
spring.application.name=test-webhook-validation-maven
cakewalk.api.api-key=11bd8aa5-df9c-4b49-9d4a-b8ad63879ad4
cakewalk.api.api-secret=VoImlKQZ1yJhSPj2rpgNy8Sts6Ba3oHGECYBxsxIyGSTKhtzDdApvB4Qt8DhfI0fjKsYF8Xrwcsaomz5V6btR3C6NCY6FmQ8soDHKVxTm0B3fn4dUgvYL5S0csH345qm
cakewalk.api.public-key-endpoint=https://open-api.getcakewalk.io/api/Keys

You can override via environment variables:

CAKEWALK_API_API_KEY=your-api-key
CAKEWALK_API_API_SECRET=your-api-secret

Dependencies (pom.xml)

Make sure you have these dependencies:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
              
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

Expected test payload

When you create the webhook in cakewalk, the webhook URL will be validated by sending automatically the following test event payload:

[
    {
        "eventType": "test",
        "data": {
            "message": "Hello, World!"
        }
    }
]

🧱 Project Structure

bash
CopyEdit
cakewalk-webhook/
├── src/
│   ├── config/
│   │   └── cakewalkApiConfig.ts           # 🔐 API credentials and endpoint
│   ├── controller/
│   │   └── webhookController.ts           # 🛡️ Webhook handler
│   ├── model/
│   │   └── WebhookPayload.ts              # 📦 Payload type definition
│   ├── service/
│   │   └── signatureService.ts            # 🔐 RSA signature verification
│   ├── app.ts                             # ⚙️ Express setup
│   └── index.ts                           # 🚀 Entry point
├── .env                                   # 🔑 API config
├── tsconfig.json
├── package.json

📁 src/config/cakewalkApiConfig.ts

import dotenv from 'dotenv';
dotenv.config();

export const cakewalkApiConfig = {
  apiKey: process.env.CAKEWALK_API_KEY!,
  apiSecret: process.env.CAKEWALK_API_SECRET!,
  publicKeyEndpoint: process.env.CAKEWALK_PUBLIC_KEY_ENDPOINT || '<https://open-api.getcakewalk.io/api/Keys>',
};

📁 src/model/WebhookPayload.ts

export interface WebhookPayload {
  eventType: string;
  data: {
    message: string;
  };
}

📁 src/service/signatureService.ts

import axios from 'axios';
import crypto from 'crypto';
import { cakewalkApiConfig } from '../config/cakewalkApiConfig';

let cachedPublicKey: string | null = null;

async function fetchPublicKey(): Promise<string> {
  if (cachedPublicKey) return cachedPublicKey;

  const response = await axios.get<string>(cakewalkApiConfig.publicKeyEndpoint, {
    headers: {
      'X-API-KEY': cakewalkApiConfig.apiKey,
      'X-API-SECRET': cakewalkApiConfig.apiSecret,
    },
  });

  const cleaned = response.data
    .replace(/-----BEGIN PUBLIC KEY-----/, '')
    .replace(/-----END PUBLIC KEY-----/, '')
    .replace(/\\s+/g, '');

  cachedPublicKey = cleaned;
  return cleaned;
}

export async function verifySignature(payload: string, signatureBase64: string): Promise<boolean> {
  try {
    const pemKey = await fetchPublicKey();
    const publicKey = crypto.createPublicKey({
      key: Buffer.from(pemKey, 'base64'),
      format: 'der',
      type: 'spki',
    });

    const verifier = crypto.createVerify('RSA-SHA256');
    verifier.update(payload);
    verifier.end();

    const signature = Buffer.from(signatureBase64, 'base64');
    return verifier.verify(publicKey, signature);
  } catch (err) {
    console.error('Signature verification failed:', err);
    return false;
  }
}

📁 src/controller/webhookController.ts

import { Request, Response } from 'express';
import { verifySignature } from '../service/signatureService';
import { WebhookPayload } from '../model/WebhookPayload';

export async function webhookHandler(req: Request, res: Response) {
  const signature = req.headers['x-signature'] as string;
  if (!signature) {
    return res.status(401).send('Missing signature.');
  }

  const rawBody = (req as any).rawBody?.toString('utf8');
  if (!rawBody) {
    return res.status(400).send('Missing raw body for signature verification.');
  }

  const isValid = await verifySignature(rawBody, signature);
  if (!isValid) {
    return res.status(401).send('Invalid signature.');
  }

  try {
    const payload: WebhookPayload = req.body;
    console.log('✅ Webhook received:', payload);
    return res.send('Webhook received.');
  } catch (e) {
    return res.status(400).send('Invalid JSON.');
  }
}

⚙️ src/app.ts

import express from 'express';
import bodyParser from 'body-parser';
import { webhookHandler } from './controller/webhookController';

const app = express();

// Capture raw body for signature verification
app.use(bodyParser.json({
  verify: (req: any, res, buf) => {
    req.rawBody = buf;
  }
}));

app.post('/api/webhook', webhookHandler);

export default app;

🔑 .env

CAKEWALK_API_KEY=your-api-key
CAKEWALK_API_API_SECRET=your-api-secret
CAKEWALK_PUBLIC_KEY_ENDPOINT=https://open-api.getcakewalk.io/api/Keys

PreviousWebhooksNextChangelog

Last updated 3 days ago

Was this helpful?