# Return Order Webhook

`POST https://your_server_address/{your_return_order_endpoint}`

> Webhooks — Direction: Recustomer → Merchant (notification)

Notifies you of a return status update.

## Headers

- `X-Recustomer-Signature-256` (string, required): HMAC SHA256 signature of the payload, sent in a header to guarantee data integrity and authenticate the sender. The signature has the form 'sha256=signature', where signature is a 64-character hexadecimal string.
  - Pattern: `^sha256=[a-fA-F0-9]{64}$`
- `X-Recustomer-Sender-ID` (string, required): Contains a unique identifier of what triggered the webhook, so that the receiver can identify the sender.

## Request body

- `id` (integer, required): ID of the Return Order
- `external_order_id` (string, required): ID of the original order
- `order_number` (string, required): Order number
- `request_id` (string, required): ID of the request
- `created_at` (string, date-time, required): Return created at
- `refund_reject_reason` (string): Reason for rejecting the refund
- `approval_at` (string, date-time): Return approved at
- `send_backed_at` (string, date-time): Sent back at
- `reshipmented_at` (string, date-time): Reshipped at
- `exchanged_at` (string, date-time): Exchanged at
- `refunded_at` (string, date-time): Refunded at
- `completed_at` (string, date-time): Request completed at
- `approval_status` (string): Approval status
  - Possible values: `unapproved`, `partially_unapproved`, `approved`, `partially_approved`, `rejected`, `deleted`
- `send_back_status` (string): Send back status
  - Possible values: `unarrived`, `partially_unarrived`, `accepted`, `partially_accepted`, `refused`, `unnecessary`, `expired`
- `reshipment_status` (string): Reship status
  - Possible values: `unreshiped`, `partially_unreshiped`, `reshiped`
- `exchange_status` (string): Exchange status
  - Possible values: `unexchanged`, `partially_unexchanged`, `exchanged`, `partially_exchanged`, `rejected`
- `refund_status` (string): Refund status
  - Possible values: `unrefunded`, `processing`, `refunded`, `rejected`
- `payment_status` (string): Payment status
  - Possible values: `unpaid`, `processing`, `paid`
- `exchange_slip_creation_status` (string): Exchange order issuance status
  - Possible values: `uncreated`, `partiallycreated`, `created`
- `order_items` (array of objects, required)
  - `id` (integer, required): Return item ID
  - `original_item_id` (string, required): Original item ID
  - `sku` (string, required): Return item SKU
  - `product_id` (string, required): Returned product ID
  - `image_link` (string): Returned product image
  - `is_deleted` (boolean, required): Whether the returned product has been deleted
  - `item_name` (string, required): Returned product name
  - `item_price` (number, required): Returned product price
  - `currency` (string, required): Currency
  - `item_result` (object, required)
    - `id` (integer, required): ID of the result for the returned item
    - `approval_status` (string): Approval status of the returned item
      - Possible values: `unapproved`, `manually_approved`, `automatically_approved`, `manually_rejected`, `automatically_rejected`, `deleted`
    - `send_back_status` (string): Send back status of the returned item
      - Possible values: `unarrived`, `accepted`, `refused`, `unnecessary`, `expired`
    - `reshipment_status` (string): Reship status of the returned item
      - Possible values: `unreshiped`, `reshiped`
    - `resolution_method` (string): Resolution for the returned item
      - Possible values: `exchange`, `refund`, `repair`
    - `item_status` (string): Inspection result of the returned item
    - `is_required_send_back` (boolean, required): Whether the item needs to be sent back
    - `is_customer_pay` (boolean): Whether the customer pays the return shipping
    - `approval_reject_reason` (string): Reason for rejecting approval
    - `exchange_reject_reason` (string): Reason for rejecting the exchange
    - `memo` (string): Memo
    - `send_back_reject_reason` (string): Reason for rejecting the send back
    - `arrival_date` (string, date): Date the sent-back item arrived
  - `options` (array of objects)
    - `key` (string, required): Option key of the returned item
    - `value` (string, required): Option value of the returned item
  - `exchange_item` (object)
    - `id` (integer, required): Exchange product ID
    - `item_name` (string, required): Exchange product name
    - `options` (array of objects)
      - `key` (string, required): Option key of the returned item
      - `value` (string, required): Option value of the returned item
    - `image_link` (string): Exchange product image
    - `sku` (string, required): Exchange product SKU
    - `item_price` (number, required): Exchange product price
    - `currency` (string, required): Currency
    - `exchange_status` (string): Exchange status
      - Possible values: `unexchanged`, `exchanged`, `rejected`
    - `exchange_slip_creation_status` (string): Exchange order issuance status
      - Possible values: `uncreated`, `created`
    - `external_order_code` (string): External exchange order code
- `customer` (object, required)
  - `id` (integer, required): Customer ID
  - `email` (string, required): Email address
  - `return_count` (integer, required): Number of returns
  - `cancel_count` (integer, required): Number of cancellations
  - `last_name` (string, required): Last name
  - `first_name` (string, required): First name
  - `full_name` (string, required): Full name
  - `phone_number` (string, required): Phone number
  - `is_consent_email_marketing` (boolean, required): Whether marketing email is allowed
  - `is_consent_sms_marketing` (boolean, required): Whether marketing SMS is allowed
- `shipping_address` (object)
  - `last_name` (string, required): Last name
  - `first_name` (string, required): First name
  - `full_name` (string, required): Full name
  - `phone_number` (string, required): Phone number
  - `country_code` (string, required): Country code
  - `postal_code` (string, required): Postal code
  - `province` (string, required): Prefecture
  - `city` (string, required): City
  - `address1` (string, required): Address line 1 (street and number)
  - `address2` (string, required): Address line 2 (building and room number)
- `billing_address` (object)
  - `last_name` (string, required): Last name
  - `first_name` (string, required): First name
  - `full_name` (string, required): Full name
  - `phone_number` (string, required): Phone number
  - `country_code` (string, required): Country code
  - `postal_code` (string, required): Postal code
  - `province` (string, required): Prefecture
  - `city` (string, required): City
  - `address1` (string, required): Address line 1 (street and number)
  - `address2` (string, required): Address line 2 (building and room number)
- `refund_info` (object)
  - `id` (integer, required): Refund information ID
  - `payment_method` (string): Payment method
  - `refund_method` (string): Refund
    - Possible values: `API返金`, `PSP画面`, `銀行振込`
  - `refund_sub_total` (number, required): Refund subtotal
  - `tax` (number, required): Tax
  - `suggested_amount` (number, required): Suggested refund amount
  - `fixed_amount` (number): Refunded amount
  - `discount_and_expenses` (array of objects, required)
    - `id` (integer, required)
    - `suggested_amount` (number)
    - `fixed_amount` (number)
    - `discount_expenses_name` (string, required): Name of the discount or fee setting
- `is_archive` (boolean, required)

## Responses

### 200 OK

- `results` (string)

## Endpoint implementation

### curl

```bash
# Send a signed test notification to your local endpoint
RECUSTOMER_WEBHOOK_SECRET='your_webhook_secret'
BODY=$(cat <<'JSON'
{
  "id": 1,
  "external_order_id": "A123",
  "order_number": "#1001",
  "request_id": "#1001_01",
  "created_at": "2024-01-17 20:13:41.103117+00",
  "refund_reject_reason": "2024-01-17 20:13:41.103117+00",
  "approval_at": "2024-01-17 20:13:41.103117+00",
  "send_backed_at": "2024-01-17 20:13:41.103117+00",
  "reshipmented_at": "2024-01-17 20:13:41.103117+00",
  "exchanged_at": "2024-01-17 20:13:41.103117+00",
  "refunded_at": "2024-01-17 20:13:41.103117+00",
  "completed_at": "2024-01-17 20:13:41.103117+00",
  "approval_status": "unapproved",
  "send_back_status": "unarrived",
  "reshipment_status": "unreshiped",
  "exchange_status": "unexchanged",
  "refund_status": "unrefunded",
  "payment_status": "unpaid",
  "exchange_slip_creation_status": "uncreated",
  "order_items": [
    {
      "id": 1,
      "original_item_id": "A11",
      "sku": "sku123",
      "product_id": "product1234",
      "image_link": "https://~",
      "is_deleted": false,
      "item_name": "return item A",
      "item_price": 5000,
      "currency": "JPY",
      "item_result": {
        "id": 1,
        "approval_status": "unapproved",
        "send_back_status": "unarrived",
        "reshipment_status": "unreshiped",
        "resolution_method": "exchange",
        "item_status": "A品",
        "is_required_send_back": true,
        "is_customer_pay": true,
        "approval_reject_reason": "承認条件を満たしてない",
        "exchange_reject_reason": "交換条件を満たしてない",
        "memo": "在庫処分",
        "send_back_reject_reason": "リクエスト内容と異なる",
        "arrival_date": "2024-01-17"
      },
      "options": [
        {
          "key": "color",
          "value": "black"
        }
      ],
      "exchange_item": {
        "id": 1,
        "item_name": "exchange item A",
        "options": [
          {
            "key": "color",
            "value": "black"
          }
        ],
        "image_link": "https://~",
        "sku": "sku123",
        "item_price": 5000,
        "currency": "JPY",
        "exchange_status": "unexchanged",
        "exchange_slip_creation_status": "uncreated",
        "external_order_code": "order123"
      }
    }
  ],
  "customer": {
    "id": 1,
    "email": "info@recustomer.co",
    "return_count": 2,
    "cancel_count": 2,
    "last_name": "山田",
    "first_name": "太郎",
    "full_name": "山田太郎",
    "phone_number": "08011112222",
    "is_consent_email_marketing": true,
    "is_consent_sms_marketing": true
  },
  "shipping_address": {
    "last_name": "山田",
    "first_name": "太郎",
    "full_name": "山田太郎",
    "phone_number": "09012345678",
    "country_code": "JP",
    "postal_code": "100-0024",
    "province": "東京都",
    "city": "千代田区",
    "address1": "千代田1-1-1",
    "address2": "千代田ビル501"
  },
  "billing_address": {
    "last_name": "山田",
    "first_name": "太郎",
    "full_name": "山田太郎",
    "phone_number": "09012345678",
    "country_code": "JP",
    "postal_code": "100-0024",
    "province": "東京都",
    "city": "千代田区",
    "address1": "千代田1-1-1",
    "address2": "千代田ビル501"
  },
  "refund_info": {
    "id": 1,
    "payment_method": "クレジットカード",
    "refund_method": "API返金",
    "refund_sub_total": 10000,
    "tax": 1000,
    "suggested_amount": 11000,
    "fixed_amount": 11000,
    "discount_and_expenses": [
      {
        "id": 0,
        "suggested_amount": 0,
        "fixed_amount": 0,
        "discount_expenses_name": "返品手数料"
      }
    ]
  },
  "is_archive": true
}
JSON
)
SIGNATURE="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$RECUSTOMER_WEBHOOK_SECRET" | sed 's/^.* //')"

curl -X POST 'http://localhost:3000/your_return_order_endpoint' \
  -H "X-Recustomer-Signature-256: $SIGNATURE" \
  -H 'X-Recustomer-Sender-ID: 1bad6b8cf97131fceab8543e81f7757195fbb1d36b376ee994ad1cf17699c464' \
  -H 'Content-Type: application/json' \
  --data-binary "$BODY"
```

### Node.js

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

### Python

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

```php
<?php
// Run: php -S localhost:3000 handler.php  (POST /your_return_order_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;
```

### Go

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

### Rust

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

## Response (200)

```json
{
  "results": "string"
}
```
