# Send a signed test notification to your local endpoint
RECUSTOMER_WEBHOOK_SECRET='your_webhook_secret'
BODY=$(cat <<'JSON'
{
"order_number": "string",
"shipping_address": {
"phone_number": "09012345678"
},
"tracking_number": "string",
"estimated_delivery_date": "2024-01-15",
"actual_delivery_date": "2024-01-15",
"actual_delivery_datetime": "2024-01-15T10:00:00+09:00",
"latest_delivery_status": "荷物受付け",
"delivery_company": "string",
"logistics_centers": [
{
"center": {
"center_name": "string",
"center_address": "string",
"updated_at": "string",
"status": "string"
}
}
]
}
JSON
)
SIGNATURE="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$RECUSTOMER_WEBHOOK_SECRET" | sed 's/^.* //')"
curl -X POST 'http://localhost:3000/your_shipping_endpoint' \
-H "X-Recustomer-Signature-256: $SIGNATURE" \
-H 'X-Recustomer-Sender-ID: 1bad6b8cf97131fceab8543e81f7757195fbb1d36b376ee994ad1cf17699c464' \
-H 'Content-Type: application/json' \
--data-binary "$BODY"
// npm install express
import crypto from 'node:crypto';
import express from 'express';
const secret = process.env.RECUSTOMER_WEBHOOK_SECRET;
if (!secret) throw new Error('RECUSTOMER_WEBHOOK_SECRET is not set');
// Compare in constant time so the response time does not reveal the expected value
const safeEqual = (a, b) => {
const x = Buffer.from(a);
const y = Buffer.from(b);
return x.length === y.length && crypto.timingSafeEqual(x, y);
};
const app = express();
// Verify the signature against the raw body, before parsing it as JSON
app.post(
"/your_shipping_endpoint",
express.raw({ type: 'application/json' }),
(req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
if (!safeEqual(req.get("X-Recustomer-Signature-256") ?? '', expected)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(event);
res.status(200).json({
"results": "string"
});
},
);
app.listen(3000);
# pip install flask
import hashlib
import hmac
import os
from flask import Flask, jsonify, request
SECRET = os.environ.get("RECUSTOMER_WEBHOOK_SECRET", "")
if not SECRET:
raise RuntimeError("RECUSTOMER_WEBHOOK_SECRET is not set")
app = Flask(__name__)
@app.route("/your_shipping_endpoint", methods=["POST"])
def handle():
# Verify the signature against the raw body, before parsing it as JSON
body = request.get_data()
expected = "sha256=" + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
received = request.headers.get("X-Recustomer-Signature-256", "")
if not hmac.compare_digest(received.encode(), expected.encode()):
return "", 401
event = request.get_json()
print(event)
return jsonify({
"results": "string",
}), 200
if __name__ == "__main__":
app.run(port=3000)
<?php
// Run: php -S localhost:3000 handler.php (POST /your_shipping_endpoint)
$secret = getenv('RECUSTOMER_WEBHOOK_SECRET');
if ($secret === false || $secret === '') {
http_response_code(500);
exit('RECUSTOMER_WEBHOOK_SECRET is not set');
}
// Verify the signature against the raw body, before parsing it as JSON
$body = file_get_contents('php://input');
$headers = [];
foreach (getallheaders() as $name => $value) {
$headers[str_replace('_', '-', strtolower($name))] = $value;
}
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $headers['x-recustomer-signature-256'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);
header('Content-Type: application/json');
echo <<<'JSON'
{
"results": "string"
}
JSON;
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
)
const responseBody = `{
"results": "string"
}`
func main() {
secret := os.Getenv("RECUSTOMER_WEBHOOK_SECRET")
if secret == "" {
log.Fatal("RECUSTOMER_WEBHOOK_SECRET is not set")
}
http.HandleFunc("POST /your_shipping_endpoint", func(w http.ResponseWriter, r *http.Request) {
// Verify the signature against the raw body, before parsing it as JSON
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
received := r.Header.Get("X-Recustomer-Signature-256")
if !hmac.Equal([]byte(received), []byte(expected)) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var event map[string]any
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Println(event)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(responseBody))
})
http.ListenAndServe(":3000", nil)
}
// Cargo.toml: axum = "0.8", tokio = { version = "1", features = ["full"] },
// hmac = "0.12", sha2 = "0.10", hex = "0.4"
use axum::{
body::Bytes,
extract::State,
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::post,
Router,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
const RESPONSE_BODY: &str = r#"{
"results": "string"
}"#;
// Verify the signature against the raw body, before parsing it as JSON
async fn handle(State(secret): State<String>, headers: HeaderMap, body: Bytes) -> Response {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key length");
mac.update(&body);
let signature = headers
.get("X-Recustomer-Signature-256")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("sha256="))
.and_then(|v| hex::decode(v).ok());
if !signature.is_some_and(|s| mac.verify_slice(&s).is_ok()) {
return StatusCode::UNAUTHORIZED.into_response();
}
println!("{}", String::from_utf8_lossy(&body));
(StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], RESPONSE_BODY).into_response()
}
#[tokio::main]
async fn main() {
let state = std::env::var("RECUSTOMER_WEBHOOK_SECRET")
.ok()
.filter(|v| !v.is_empty())
.expect("RECUSTOMER_WEBHOOK_SECRET is not set");
let app = Router::new().route("/your_shipping_endpoint", post(handle)).with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}