Skip to main content
Security is critical when dealing with automated event notifications.

Digital Signature Verification

All webhook requests include a digital signature using the ED25519 algorithm. The signature is provided in the following HTTP header:
X-Partoo-Signature-v1: BASE64_SIGNATURE
To verify:
  1. Base64 decode the signature
  2. Recalculate the hash of the payload
  3. Use our public key to validate the signature
Use separate keys for production and sandbox environments.
Use different public keys depending on the environment:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA0G9ciHL6XZQXuWq6W4dFLvwNEPWgcdtQgEVlBIwZWBQ=
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEALsyvX2yVnG3ZKRIFfEvYk2nkzanoNgAqBSqdeNub4sM=
-----END PUBLIC KEY-----

Code Examples

Here are sample snippets to help you implement signature verification:
from base64 import b64decode
import binascii
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519

# load public key from filesystem, you may adapt depending on your secret management framework
public_key = serialization.load_pem_public_key(open("/var/secrets/partoo.pub.pem"))

def validate_signature(request):
  if (signature:=request.headers.get("X-Partoo-Signature-v1")) is None:
    raise ValueError("Missing signature")

  # don't trust your inputs
  # will raise a subclass of ValueError if format is invalid
  decoded_signature = b64decode(signature, validate=True)

  # validate payload's signature
  try:
    public_key.verify(decoded_signature, request.body.encode())
  except Exception as e:
    raise ValueError("Invalid signature") from e
const fs = require('fs');
const crypto = require('crypto');

// Load public key from filesystem
const publicKey = fs.readFileSync('/var/secrets/partoo.pub.pem', 'utf8');

function validateSignature(request) {
  const signature = request.headers['x-partoo-signature-v1'];
  if (!signature) {
    throw new Error('Missing signature');
  }

  // Decode the base64 signature
  let decodedSignature;
  try {
    decodedSignature = Buffer.from(signature, 'base64');
  } catch (err) {
    throw new Error('Invalid signature format');
  }

  // Verify the payload's signature
  const payload = request.body;
  if (!crypto.verify(null, Buffer.from(payload), publicKey, decodedSignature)) {
    throw new Error('Invalid signature');
  }
}
package main

import (
    "crypto/ed25519"
    "encoding/base64"
    "encoding/pem"
    "errors"
    "io/ioutil"
    "net/http"
)

func loadPublicKey(path string) (ed25519.PublicKey, error) {
    keyData, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, err
    }

    block, _ := pem.Decode(keyData)
    if block == nil {
        return nil, errors.New("failed to parse PEM block")
    }

    publicKey := ed25519.PublicKey(block.Bytes[12:]) // Skip ASN.1 header
    return publicKey, nil
}

func validateSignature(r *http.Request, payload []byte) error {
    signature := r.Header.Get("X-Partoo-Signature-v1")
    if signature == "" {
        return errors.New("missing signature")
    }

    // Decode base64 signature
    decodedSignature, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return errors.New("invalid signature format")
    }

    // Load public key
    publicKey, err := loadPublicKey("/var/secrets/partoo.pub.pem")
    if err != nil {
        return err
    }

    // Verify signature
    if !ed25519.Verify(publicKey, payload, decodedSignature) {
        return errors.New("invalid signature")
    }

    return nil
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class WebhookValidator {
    
    private static PublicKey loadPublicKey(String path) throws Exception {
        String keyPem = new String(Files.readAllBytes(Paths.get(path)));
        keyPem = keyPem.replace("-----BEGIN PUBLIC KEY-----", "")
                       .replace("-----END PUBLIC KEY-----", "")
                       .replaceAll("\\s", "");
        
        byte[] keyBytes = Base64.getDecoder().decode(keyPem);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
        return keyFactory.generatePublic(spec);
    }
    
    public static void validateSignature(String signatureHeader, String payload) throws Exception {
        if (signatureHeader == null || signatureHeader.isEmpty()) {
            throw new IllegalArgumentException("Missing signature");
        }
        
        // Decode base64 signature
        byte[] decodedSignature;
        try {
            decodedSignature = Base64.getDecoder().decode(signatureHeader);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid signature format", e);
        }
        
        // Load public key
        PublicKey publicKey = loadPublicKey("/var/secrets/partoo.pub.pem");
        
        // Verify signature
        Signature sig = Signature.getInstance("Ed25519");
        sig.initVerify(publicKey);
        sig.update(payload.getBytes());
        
        if (!sig.verify(decodedSignature)) {
            throw new SecurityException("Invalid signature");
        }
    }
}
<?php

function validateSignature($request, $payload) {
    $signature = $request->header('X-Partoo-Signature-v1');
    
    if (!$signature) {
        throw new Exception('Missing signature');
    }

    // Decode base64 signature
    $decodedSignature = base64_decode($signature, true);
    if ($decodedSignature === false) {
        throw new Exception('Invalid signature format');
    }

    // Load public key from filesystem
    $publicKeyPem = file_get_contents('/var/secrets/partoo.pub.pem');
    $publicKey = openssl_pkey_get_public($publicKeyPem);
    
    if (!$publicKey) {
        throw new Exception('Failed to load public key');
    }

    // Verify signature using Ed25519
    $keyDetails = openssl_pkey_get_details($publicKey);
    $publicKeyRaw = $keyDetails['key'];
    
    // For Ed25519 verification with PHP, you may need sodium extension
    if (!function_exists('sodium_crypto_sign_verify_detached')) {
        throw new Exception('Sodium extension required for Ed25519 verification');
    }
    
    // Extract raw public key (skip ASN.1 header)
    $rawKey = substr(base64_decode(
        str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n"], '', $publicKeyPem)
    ), 12);
    
    if (!sodium_crypto_sign_verify_detached($decodedSignature, $payload, $rawKey)) {
        throw new Exception('Invalid signature');
    }
}
?>

Shared Secret in URL

You may include a shared secret in your webhook URL: Examples:
https://my.integration.io/webhooks/partoo/9fa91de19/business_update
https://my.integration.io/webhooks/partoo/business_update?key=9fa91de19