# Reproduce the request Recustomer sends, against your local endpoint
curl -X POST 'http://localhost:3000/orders/cancel' \
-H 'api_token: X12345678' \
-H 'Content-Type: application/json' \
-d @- <<'JSON'
{
"order_id": 12345
}
JSON
// npm install express
import crypto from 'node:crypto';
import express from 'express';
const token = process.env.RECUSTOMER_API_TOKEN;
if (!token) throw new Error('RECUSTOMER_API_TOKEN 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();
app.use(express.json());
app.post("/orders/cancel", (req, res) => {
if (!safeEqual(req.get("api_token") ?? '', token)) {
return res.status(401).json({ message: 'Unauthorized' });
}
console.log(req.body);
res.status(201).json({
"id": 1234567,
"order_number": 1001,
"status": "processing",
"fulfillment_status": "fulfilled",
"financial_status": "paid",
"order_discount_amount": 100,
"order_point_price": 100,
"total_item_discount_amount": 100,
"subtotal_price": 100,
"total_shipping_price": "string",
"total_discount": 100,
"additional_fees_amount": 100,
"total_price": 100,
"total_tax": 100,
"payment_at": "2023-05-10T12:00:00+09:00",
"is_tax_included": true,
"currency": "JPY",
"quantity": 3,
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
"note": "テスト注文",
"customer": {
"customer_id": "1",
"email": "test@recustomer.co",
"last_name": "佐藤",
"first_name": "太郎",
"full_name": "佐藤 太郎",
"phone_number": "000-####-####",
"is_consent_email_marketing": false,
"is_consent_sms_marketing": false,
"purchase_times": 11,
"customer_tags": [
"test顧客",
"一般会員"
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
},
"line_items": [
{
"id": "1000",
"product_id": "product_id",
"product_name": "テスト商品",
"variant_id": "2538974832789",
"sku": "product_id_S_Red",
"item_image": "https://recustomer.me/image/12345",
"item_discount_price": 100,
"item_discount_amount": 100,
"item_price": 100,
"item_compare_at_price": 100,
"item_cost": 100,
"item_quantity": 1,
"item_price_amount": 100,
"item_tax_price": 100,
"is_tax_included": true,
"fulfillment_status": "null",
"variant_options": [
{
"name": "カラー",
"value": "レッド"
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"shipping_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"billing_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"fulfillments": [
{
"id": "1",
"status": "pending",
"tracking_company": "yamato",
"tracking_number": "4983892",
"fulfillment_line_items": [
{
"id": "1",
"order_line_item_id": 1,
"quantity": 2
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"transactions": [
{
"id": "2",
"parent_id": 1,
"amount": 1000,
"currency": "JPY",
"status": "success",
"gateway": "manual",
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
]
});
});
app.listen(3000);
# pip install flask
import hmac
import os
from flask import Flask, jsonify, request
TOKEN = os.environ.get("RECUSTOMER_API_TOKEN", "")
if not TOKEN:
raise RuntimeError("RECUSTOMER_API_TOKEN is not set")
app = Flask(__name__)
@app.route("/orders/cancel", methods=["POST"])
def handle():
received = request.headers.get("api_token", "")
if not hmac.compare_digest(received.encode(), TOKEN.encode()):
return jsonify({"message": "Unauthorized"}), 401
print(request.get_json())
return jsonify({
"id": 1234567,
"order_number": 1001,
"status": "processing",
"fulfillment_status": "fulfilled",
"financial_status": "paid",
"order_discount_amount": 100,
"order_point_price": 100,
"total_item_discount_amount": 100,
"subtotal_price": 100,
"total_shipping_price": "string",
"total_discount": 100,
"additional_fees_amount": 100,
"total_price": 100,
"total_tax": 100,
"payment_at": "2023-05-10T12:00:00+09:00",
"is_tax_included": True,
"currency": "JPY",
"quantity": 3,
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
"note": "テスト注文",
"customer": {
"customer_id": "1",
"email": "test@recustomer.co",
"last_name": "佐藤",
"first_name": "太郎",
"full_name": "佐藤 太郎",
"phone_number": "000-####-####",
"is_consent_email_marketing": False,
"is_consent_sms_marketing": False,
"purchase_times": 11,
"customer_tags": [
"test顧客",
"一般会員",
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
},
"line_items": [
{
"id": "1000",
"product_id": "product_id",
"product_name": "テスト商品",
"variant_id": "2538974832789",
"sku": "product_id_S_Red",
"item_image": "https://recustomer.me/image/12345",
"item_discount_price": 100,
"item_discount_amount": 100,
"item_price": 100,
"item_compare_at_price": 100,
"item_cost": 100,
"item_quantity": 1,
"item_price_amount": 100,
"item_tax_price": 100,
"is_tax_included": True,
"fulfillment_status": "null",
"variant_options": [
{
"name": "カラー",
"value": "レッド",
},
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
},
],
"shipping_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F",
},
},
"billing_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F",
},
},
"fulfillments": [
{
"id": "1",
"status": "pending",
"tracking_company": "yamato",
"tracking_number": "4983892",
"fulfillment_line_items": [
{
"id": "1",
"order_line_item_id": 1,
"quantity": 2,
},
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
},
],
"transactions": [
{
"id": "2",
"parent_id": 1,
"amount": 1000,
"currency": "JPY",
"status": "success",
"gateway": "manual",
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
},
],
}), 201
if __name__ == "__main__":
app.run(port=3000)
<?php
// Run: php -S localhost:3000 handler.php (POST /orders/cancel)
$token = getenv('RECUSTOMER_API_TOKEN');
if ($token === false || $token === '') {
http_response_code(500);
exit('RECUSTOMER_API_TOKEN is not set');
}
$headers = [];
foreach (getallheaders() as $name => $value) {
$headers[str_replace('_', '-', strtolower($name))] = $value;
}
if (!hash_equals($token, $headers['api-token'] ?? '')) {
http_response_code(401);
header('Content-Type: application/json');
echo '{"message":"Unauthorized"}';
exit;
}
$request = json_decode(file_get_contents('php://input'), true);
http_response_code(201);
header('Content-Type: application/json');
echo <<<'JSON'
{
"id": 1234567,
"order_number": 1001,
"status": "processing",
"fulfillment_status": "fulfilled",
"financial_status": "paid",
"order_discount_amount": 100,
"order_point_price": 100,
"total_item_discount_amount": 100,
"subtotal_price": 100,
"total_shipping_price": "string",
"total_discount": 100,
"additional_fees_amount": 100,
"total_price": 100,
"total_tax": 100,
"payment_at": "2023-05-10T12:00:00+09:00",
"is_tax_included": true,
"currency": "JPY",
"quantity": 3,
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
"note": "テスト注文",
"customer": {
"customer_id": "1",
"email": "test@recustomer.co",
"last_name": "佐藤",
"first_name": "太郎",
"full_name": "佐藤 太郎",
"phone_number": "000-####-####",
"is_consent_email_marketing": false,
"is_consent_sms_marketing": false,
"purchase_times": 11,
"customer_tags": [
"test顧客",
"一般会員"
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
},
"line_items": [
{
"id": "1000",
"product_id": "product_id",
"product_name": "テスト商品",
"variant_id": "2538974832789",
"sku": "product_id_S_Red",
"item_image": "https://recustomer.me/image/12345",
"item_discount_price": 100,
"item_discount_amount": 100,
"item_price": 100,
"item_compare_at_price": 100,
"item_cost": 100,
"item_quantity": 1,
"item_price_amount": 100,
"item_tax_price": 100,
"is_tax_included": true,
"fulfillment_status": "null",
"variant_options": [
{
"name": "カラー",
"value": "レッド"
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"shipping_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"billing_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"fulfillments": [
{
"id": "1",
"status": "pending",
"tracking_company": "yamato",
"tracking_number": "4983892",
"fulfillment_line_items": [
{
"id": "1",
"order_line_item_id": 1,
"quantity": 2
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"transactions": [
{
"id": "2",
"parent_id": 1,
"amount": 1000,
"currency": "JPY",
"status": "success",
"gateway": "manual",
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
]
}
JSON;
package main
import (
"crypto/subtle"
"io"
"log"
"net/http"
"os"
)
const responseBody = `{
"id": 1234567,
"order_number": 1001,
"status": "processing",
"fulfillment_status": "fulfilled",
"financial_status": "paid",
"order_discount_amount": 100,
"order_point_price": 100,
"total_item_discount_amount": 100,
"subtotal_price": 100,
"total_shipping_price": "string",
"total_discount": 100,
"additional_fees_amount": 100,
"total_price": 100,
"total_tax": 100,
"payment_at": "2023-05-10T12:00:00+09:00",
"is_tax_included": true,
"currency": "JPY",
"quantity": 3,
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
"note": "テスト注文",
"customer": {
"customer_id": "1",
"email": "test@recustomer.co",
"last_name": "佐藤",
"first_name": "太郎",
"full_name": "佐藤 太郎",
"phone_number": "000-####-####",
"is_consent_email_marketing": false,
"is_consent_sms_marketing": false,
"purchase_times": 11,
"customer_tags": [
"test顧客",
"一般会員"
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
},
"line_items": [
{
"id": "1000",
"product_id": "product_id",
"product_name": "テスト商品",
"variant_id": "2538974832789",
"sku": "product_id_S_Red",
"item_image": "https://recustomer.me/image/12345",
"item_discount_price": 100,
"item_discount_amount": 100,
"item_price": 100,
"item_compare_at_price": 100,
"item_cost": 100,
"item_quantity": 1,
"item_price_amount": 100,
"item_tax_price": 100,
"is_tax_included": true,
"fulfillment_status": "null",
"variant_options": [
{
"name": "カラー",
"value": "レッド"
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"shipping_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"billing_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"fulfillments": [
{
"id": "1",
"status": "pending",
"tracking_company": "yamato",
"tracking_number": "4983892",
"fulfillment_line_items": [
{
"id": "1",
"order_line_item_id": 1,
"quantity": 2
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"transactions": [
{
"id": "2",
"parent_id": 1,
"amount": 1000,
"currency": "JPY",
"status": "success",
"gateway": "manual",
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
]
}`
func main() {
token := os.Getenv("RECUSTOMER_API_TOKEN")
if token == "" {
log.Fatal("RECUSTOMER_API_TOKEN is not set")
}
http.HandleFunc("POST /orders/cancel", func(w http.ResponseWriter, r *http.Request) {
if subtle.ConstantTimeCompare([]byte(r.Header.Get("api_token")), []byte(token)) != 1 {
http.Error(w, `{"message":"Unauthorized"}`, http.StatusUnauthorized)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Println(string(body))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
w.Write([]byte(responseBody))
})
http.ListenAndServe(":3000", nil)
}
// Cargo.toml: axum = "0.8", tokio = { version = "1", features = ["full"] }, subtle = "2"
use axum::{
extract::State,
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::post,
Router,
};
use subtle::ConstantTimeEq;
const RESPONSE_BODY: &str = r#"{
"id": 1234567,
"order_number": 1001,
"status": "processing",
"fulfillment_status": "fulfilled",
"financial_status": "paid",
"order_discount_amount": 100,
"order_point_price": 100,
"total_item_discount_amount": 100,
"subtotal_price": 100,
"total_shipping_price": "string",
"total_discount": 100,
"additional_fees_amount": 100,
"total_price": 100,
"total_tax": 100,
"payment_at": "2023-05-10T12:00:00+09:00",
"is_tax_included": true,
"currency": "JPY",
"quantity": 3,
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00",
"note": "テスト注文",
"customer": {
"customer_id": "1",
"email": "test@recustomer.co",
"last_name": "佐藤",
"first_name": "太郎",
"full_name": "佐藤 太郎",
"phone_number": "000-####-####",
"is_consent_email_marketing": false,
"is_consent_sms_marketing": false,
"purchase_times": 11,
"customer_tags": [
"test顧客",
"一般会員"
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
},
"line_items": [
{
"id": "1000",
"product_id": "product_id",
"product_name": "テスト商品",
"variant_id": "2538974832789",
"sku": "product_id_S_Red",
"item_image": "https://recustomer.me/image/12345",
"item_discount_price": 100,
"item_discount_amount": 100,
"item_price": 100,
"item_compare_at_price": 100,
"item_cost": 100,
"item_quantity": 1,
"item_price_amount": 100,
"item_tax_price": 100,
"is_tax_included": true,
"fulfillment_status": "null",
"variant_options": [
{
"name": "カラー",
"value": "レッド"
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"shipping_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"billing_address": {
"first_name": "太郎",
"last_name": "佐藤",
"full_name": "佐藤 太郎",
"email": "test@recustomer.co",
"phone_number": "000-####-####",
"address": {
"country_code": "JP",
"postal_code": "1010024",
"province": "東京都",
"city": "千代田区",
"address1": "神田和泉町1-8-11",
"address2": "サン・センタービル5F"
}
},
"fulfillments": [
{
"id": "1",
"status": "pending",
"tracking_company": "yamato",
"tracking_number": "4983892",
"fulfillment_line_items": [
{
"id": "1",
"order_line_item_id": 1,
"quantity": 2
}
],
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
],
"transactions": [
{
"id": "2",
"parent_id": 1,
"amount": 1000,
"currency": "JPY",
"status": "success",
"gateway": "manual",
"created_at": "2023-05-10T12:00:00+09:00",
"updated_at": "2023-05-12T10:30:00+09:00"
}
]
}"#;
async fn handle(State(token): State<String>, headers: HeaderMap, body: String) -> Response {
let received = headers.get("api_token").map(|v| v.as_bytes()).unwrap_or_default();
// Compare in constant time so the response time does not reveal the token
if !bool::from(received.ct_eq(token.as_bytes())) {
return StatusCode::UNAUTHORIZED.into_response();
}
println!("{body}");
(StatusCode::CREATED, [(header::CONTENT_TYPE, "application/json")], RESPONSE_BODY).into_response()
}
#[tokio::main]
async fn main() {
let state = std::env::var("RECUSTOMER_API_TOKEN")
.ok()
.filter(|v| !v.is_empty())
.expect("RECUSTOMER_API_TOKEN is not set");
let app = Router::new().route("/orders/cancel", post(handle)).with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}