X-Loop-Signature header.
This header contains a value you can use to verify that the webhook originated from Loop’s system. The value is generated using the SHA256 algorithm.
When you receive a webhook from Loop, use the webhook’s secret to generate a SHA256 HMAC of the request body, encoded as a base64 value. You can find the webhook secret in the Developer Tools page of the Loop Admin (the same place you can create webhook subscriptions).
Compare your calculated HMAC to the value in the X-Loop-Signature header included with the request. If the values match, you can confirm that the webhook came from Loop and has not been tampered with.
Code Samples
const assert = require('assert');
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.raw({
type: '*/*',
verify: function(req, _res, buf) {
if (Buffer.isBuffer(buf)) {
req.buffer = buf;
}
},
}));
app.use(express.json())
app.post('/endpoint', (req, res) => {
const jsonPayload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', '<SECRET>') // substitute secret
.update(req.buffer, 'utf-8')
.digest('base64');
const signature = req.get('X-Loop-Signature');
assert.equal(hmac, signature); // validate match
res.json({ signature, jsonPayload });
});
app.listen(3000, () => console.log('ready'));
<?php
define('SECRET', '<SECRET>'); // Substitute secret
header("Content-Type: application/json");
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LOOP_SIGNATURE'] ?? '';
// Generate HMAC
$calculated_hmac = base64_encode(hash_hmac('sha256', $body, SECRET, true));
// Validate signature
if ($calculated_hmac !== $signature) {
http_response_code(401);
echo json_encode(["error" => "Invalid signature"]);
exit();
}
// Decode JSON payload
$jsonPayload = json_decode($body, true);
echo json_encode([
"signature" => $signature,
"jsonPayload" => $jsonPayload
]);
require 'sinatra'
require 'openssl'
require 'base64'
require 'json'
SECRET = "<SECRET>" # Substitute secret
post '/endpoint' do
request.body.rewind
raw_body = request.body.read
signature = request.env['HTTP_X_LOOP_SIGNATURE']
# Calculate HMAC using SHA256
hmac = OpenSSL::HMAC.digest('sha256', SECRET, raw_body)
calculated_hmac = Base64.encode64(hmac).strip
# Verify the signature
if calculated_hmac != signature
status 401
return { error: 'Invalid signature' }.to_json
end
# Parse the JSON payload
json_payload = JSON.parse(raw_body)
# Return response
content_type :json
{ signature: signature, jsonPayload: json_payload }.to_json
end
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
app = Flask(__name__)
SECRET = b"<SECRET>" # Substitute secret
@app.route('/endpoint', methods=['POST'])
def webhook():
raw_body = request.data # Get raw request body
signature = request.headers.get('X-Loop-Signature')
# Calculate HMAC
calculated_hmac = base64.b64encode(
hmac.new(SECRET, raw_body, hashlib.sha256).digest()
).decode()
# Validate signature
if calculated_hmac != signature:
return "Invalid signature", 401
# Process payload
json_payload = request.json
return jsonify({"signature": signature, "jsonPayload": json_payload})
if __name__ == '__main__':
app.run(port=3000)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@SpringBootApplication
public class WebhookVerificationApp {
public static void main(String[] args) {
SpringApplication.run(WebhookVerificationApp.class, args);
}
}
@RestController
@RequestMapping("/endpoint")
class WebhookController {
private static final String SECRET = "<SECRET>"; // Substitute secret
@PostMapping
public ResponseEntity<Object> handleWebhook(@RequestBody byte[] body, @RequestHeader("X-Loop-Signature") String signature) {
try {
// Generate HMAC using SHA256 and the provided secret
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(SECRET.getBytes(), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] hmacBytes = hmacSha256.doFinal(body);
String calculatedHmac = Base64.getEncoder().encodeToString(hmacBytes);
// Compare the calculated HMAC with the signature
if (!calculatedHmac.equals(signature)) {
return ResponseEntity.status(401).body("Invalid signature");
}
// Convert byte array payload to JSON string
String jsonPayload = new String(body);
return ResponseEntity.ok(String.format("Signature: %s, Payload: %s", signature, jsonPayload));
} catch (Exception e) {
return ResponseEntity.status(500).body("Error verifying webhook");
}
}
}
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
const string SECRET = "<SECRET>"; // Substitute secret
app.MapPost("/endpoint", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
// Get the signature from the headers
string signature = request.Headers["X-Loop-Signature"].ToString();
// Calculate the HMAC
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SECRET));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
string calculatedHmac = Convert.ToBase64String(hash);
// Verify the signature
if (calculatedHmac != signature)
{
return Results.StatusCode(401, "Invalid signature");
}
return Results.Json(new { signature, jsonPayload = body });
});
app.Run();
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
)
const secret = "<SECRET>" // Substitute secret
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
// Get signature from header
signature := r.Header.Get("X-Loop-Signature")
// Calculate HMAC
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
calculatedHMAC := base64.StdEncoding.EncodeToString(h.Sum(nil))
// Verify signature
if calculatedHMAC != signature {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Convert payload to JSON string
var jsonPayload map[string]interface{}
if err := json.Unmarshal(body, &jsonPayload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"signature": "%s", "jsonPayload": %s}`, signature, string(body))
}
func main() {
http.HandleFunc("/endpoint", webhookHandler)
fmt.Println("Server listening on port 3000...")
http.ListenAndServe(":3000", nil)
}