# 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.

{% tabs %}
{% tab title="Python" %}

### 📦 Project Structure

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

***

#### `config.py` — 🔧 Configuration

```python
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

```python
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)

```python
from pydantic import BaseModel, Field

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

***

### `main.py` — 🚀 FastAPI App with Validation

```python
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

```bash
uvicorn main:app --reload
```

{% endtab %}

{% tab title="TypeScript Express JS" %}

#### 🧱 **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`

```typescript
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`

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

```

***

### 📁 `src/service/signatureService.ts`

```typescript
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`

```typescript
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`

```typescript
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
```

{% endtab %}

{% tab title="Java Spring Boot" %}

#### 🧱 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

```java
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

```java
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

```java
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>

```

```yaml
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:

```xml
<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!"
        }
    }
]
```

{% endtab %}

{% tab title=".NET" %}

### 📋 Prepare configuration options

#### 1. Define a configuration class:

```csharp
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`:

```csharp
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:

```csharp
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:

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

```

#### Dependency Injection in `Program.cs`:

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

```

***

### 🛡️ Implement the webhook endpoint with the validation logic

```csharp
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

```json
{
  "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
```

{% endtab %}
{% endtabs %}
