# Cancel Order

`POST https://your_server_address/orders/cancel`

> Merchant-hosted API — Direction: Recustomer → Merchant

Cancels an order

## Headers

- `api_token` (string, required): Authentication token

## Request body

- `order_id` (string, required): ID of the order to cancel

## Responses

### 201 successfully.

- `id` (string, required): Order ID
- `order_number` (string, required): Order number
- `status` (string, required): Order status. processing: in progress, fulfilled: shipped, partial_fulfilled: partially shipped, canceled: cancelled
  - Possible values: `processing`, `fulfilled`, `partial_fulfilled`, `canceled`
- `fulfillment_status` (string, required): Fulfillment status. fulfilled: shipped, null: not shipped, partial: partially shipped, canceled: cancelled
  - Possible values: `fulfilled`, `null`, `partial`, `canceled`
- `financial_status` (string, required): Payment status. pending: pending, authorized: authorized, partially_paid: partially paid, paid: paid, partially_refunded: partially refunded, refunded: refunded, voided: voided
  - Possible values: `pending`, `authorized`, `partially_paid`, `paid`, `partially_refunded`, `refunded`, `voided`
- `order_discount_amount` (string): Total discount on the order (for marketplaces, marketplace-funded coupons plus store-funded coupons)
- `order_point_price` (string, required): Points used on the order
- `total_item_discount_amount` (string, required): Total item discounts
- `subtotal_price` (string, required): Item subtotal
- `total_shipping_price` (string, required): Shipping fee at the time of the order
- `total_discount` (string, required): Total discount (order_discount_amount + order_point_price + total_item_discount_amount)
- `additional_fees_amount` (string): Total of other fees
- `total_price` (string, required): Total price
- `total_tax` (string, required): Tax
- `payment_at` (string): Paid at
- `is_tax_included` (boolean, required): Whether total_price includes tax
- `currency` (string, required): Currency
- `quantity` (integer, required): Total quantity of ordered items
- `created_at` (string, required): Created at
- `updated_at` (string): Updated at
- `note` (string): Note on the order
- `customer` (object, required): Customer information
  - `customer_id` (string): Customer ID
  - `email` (string, required): Email address
  - `last_name` (string, nullable): Last name
  - `first_name` (string, nullable): First name
  - `full_name` (string, required): Full name
  - `phone_number` (string): Phone number
  - `is_consent_email_marketing` (boolean): Whether the customer accepts email marketing
  - `is_consent_sms_marketing` (boolean): Whether the customer accepts SMS marketing
  - `purchase_times` (integer): Number of orders
  - `customer_tags` (array of strings): Customer tags
  - `created_at` (string, required): Created at
  - `updated_at` (string, required): Updated at
- `line_items` (array of objects, required): Ordered items
  - `id` (string, required): Line item ID
  - `product_id` (string, required): Product ID
  - `product_name` (string): Product name
  - `variant_id` (string): Variant ID
  - `sku` (string, required): SKU
  - `item_image` (string): Product image
  - `item_discount_price` (string, required): Discount on the item
  - `item_discount_amount` (string, required): Total discount on the item
  - `item_price` (string, required): Selling price of the item
  - `item_compare_at_price` (string, required): List price of the item
  - `item_cost` (string): Cost of the item at the time of the order
  - `item_quantity` (string, required): Quantity of the item
  - `item_price_amount` (string, required): Total selling price of the item
  - `item_tax_price` (string, required): Tax
  - `is_tax_included` (boolean, required): Whether item_price_amount includes tax
  - `fulfillment_status` (string, required): Fulfillment status. null: not shipped, fulfilled: shipped, partial: partially shipped, not_eligible: not subject to shipping
    - Possible values: `null`, `fulfilled`, `partial`, `not_eligible`
  - `variant_options` (array of objects): Variant options
    - `name` (string, required): Option name
    - `value` (string, required): Value set for the option
  - `created_at` (string, required): Created at
  - `updated_at` (string, required): Updated at
- `shipping_address` (object, required): Shipping address
  - `first_name` (string): First name
  - `last_name` (string): Last name
  - `full_name` (string, required): Full name
  - `email` (string): Email address
  - `phone_number` (string): Phone number
  - `address` (object, required): Address
    - `country_code` (string): Country code, "JP" for Japan. See ISO-3166-2
    - `postal_code` (string, required): Postal code
    - `province` (string, required): Prefecture
    - `city` (string, required): City
    - `address1` (string): Address line 1
    - `address2` (string): Address line 2
- `billing_address` (object, required): Billing address
  - `first_name` (string): First name
  - `last_name` (string): Last name
  - `full_name` (string, required): Full name
  - `email` (string): Email address
  - `phone_number` (string): Phone number
  - `address` (object, required): Address
    - `country_code` (string): Country code, "JP" for Japan. See ISO-3166-2
    - `postal_code` (string, required): Postal code
    - `province` (string, required): Prefecture
    - `city` (string, required): City
    - `address1` (string): Address line 1
    - `address2` (string): Address line 2
- `fulfillments` (array of objects, required): Fulfillments
  - `id` (string, required): Fulfillment ID
  - `status` (string, required): Fulfillment status. pending: pending, open: in progress, success: succeeded, cancelled: cancelled, failure: failed
    - Possible values: `pending`, `open`, `success`, `cancelled`, `failure`
  - `tracking_company` (string): Carrier. yamato: Yamato Transport, sagawa: Sagawa Express, jp: Japan Post, askullogist: ASKUL LOGIST, seino_unyu: Seino Transportation, eco_hai: Ecohai, global: international express, others: other
    - Possible values: `yamato`, `sagawa`, `jp`, `askullogist`, `seino_unyu`, `eco_hai`, `global`, `others`
  - `tracking_number` (string): Tracking number
  - `fulfillment_line_items` (array of objects): Fulfilled items
    - `id` (string, required): Fulfilled item ID
    - `order_line_item_id` (integer, required): line_item_id of the shipped item
    - `quantity` (integer, required): Number of items shipped
  - `created_at` (string): Shipped at
  - `updated_at` (string): Updated at
- `transactions` (array of objects): Payment information (required if payment is complete)
  - `id` (string, required): Transaction ID
  - `parent_id` (integer): ID of the parent transaction
  - `amount` (string, required): Transaction amount
  - `currency` (string, required): Currency. See https://en.wikipedia.org/wiki/ISO_4217
  - `status` (string, required): Transaction status. success: succeeded, failure: failed, pending: pending
    - Possible values: `success`, `failure`, `pending`
  - `gateway` (string, required): Payment gateway
  - `created_at` (string): Created at
  - `updated_at` (string): Updated at

### 400 Bad request

- `message` (string): Error details

### 401 Unauthorized

- `message` (string): Error details

### 422 Unprocessable Entity

- `message` (string): Error details

### 500 Internal Server Error

- `message` (string): Error details (an error may have occurred in Recustomer. Please contact us)

## Endpoint implementation

### curl

```bash
# 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
```

### Node.js

```javascript
// 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);
```

### Python

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

```php
<?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;
```

### Go

```go
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)
}
```

### Rust

```rust
// 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();
}
```

## Response (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"
    }
  ]
}
```
