# Get Product Variants

`GET https://your_server_address/products/{product_id}`

> Merchant-hosted API — Direction: Recustomer → Merchant

Retrieves the product and stock information needed for an exchange

## Headers

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

## Path parameters

- `product_id` (string, required): ID of the product

## Responses

### 200 successfully.

- `product_id` (string, required): Product ID
- `product_name` (string, required): Product name
- `image_url` (string): Link to the product image
- `created_at` (string): Created at
- `updated_at` (string): Updated at
- `variants` (array of objects, required)
  - `variant_id` (string): Variant ID
  - `sku` (string, required): SKU
  - `image_url` (string): Product image
  - `name` (string, required): Product name
  - `quantity_in_stock` (integer, int64, required): Available stock
  - `created_at` (string): Created at
  - `updated_at` (string): Updated at
  - `variant_options` (array of objects): Options set on the variant
    - `name` (string, required): Option name
    - `value` (string, required): Value set for the option

## Endpoint implementation

### curl

```bash
# Reproduce the request Recustomer sends, against your local endpoint
curl -X GET 'http://localhost:3000/products/PRODUCT_ID' \
  -H 'api_token: X12345678'
```

### 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.get("/products/:product_id", (req, res) => {
  if (!safeEqual(req.get("api_token") ?? '', token)) {
    return res.status(401).json({ message: 'Unauthorized' });
  }
  const productId = req.params["product_id"];
  console.log(productId);
  res.status(200).json({
    "product_id": "product_id",
    "product_name": "テスト商品",
    "image_url": "https://recustomer.me/image/12345",
    "created_at": "2023-05-10T12:00:00+09:00",
    "updated_at": "2023-05-12T10:30:00+09:00",
    "variants": [
      {
        "variant_id": "2538974832789",
        "sku": "test-product",
        "image_url": "https://recustomer.me/image/12345",
        "name": "テスト商品",
        "quantity_in_stock": 0,
        "created_at": "2023-05-10T12:00:00+09:00",
        "updated_at": "2023-05-12T10:30:00+09:00",
        "variant_options": [
          {
            "name": "カラー",
            "value": "レッド"
          }
        ]
      }
    ]
  });
});

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("/products/<product_id>", methods=["GET"])
def handle(product_id):
    received = request.headers.get("api_token", "")
    if not hmac.compare_digest(received.encode(), TOKEN.encode()):
        return jsonify({"message": "Unauthorized"}), 401
    print(product_id)
    return jsonify({
        "product_id": "product_id",
        "product_name": "テスト商品",
        "image_url": "https://recustomer.me/image/12345",
        "created_at": "2023-05-10T12:00:00+09:00",
        "updated_at": "2023-05-12T10:30:00+09:00",
        "variants": [
            {
                "variant_id": "2538974832789",
                "sku": "test-product",
                "image_url": "https://recustomer.me/image/12345",
                "name": "テスト商品",
                "quantity_in_stock": 0,
                "created_at": "2023-05-10T12:00:00+09:00",
                "updated_at": "2023-05-12T10:30:00+09:00",
                "variant_options": [
                    {
                        "name": "カラー",
                        "value": "レッド",
                    },
                ],
            },
        ],
    }), 200

if __name__ == "__main__":
    app.run(port=3000)
```

### PHP

```php
<?php
// Run: php -S localhost:3000 handler.php  (GET /products/{product_id})
$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;
}

preg_match('#^/products/([^/]+)$#', parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), $matches);
$product_id = $matches[1] ?? null;

http_response_code(200);
header('Content-Type: application/json');
echo <<<'JSON'
{
  "product_id": "product_id",
  "product_name": "テスト商品",
  "image_url": "https://recustomer.me/image/12345",
  "created_at": "2023-05-10T12:00:00+09:00",
  "updated_at": "2023-05-12T10:30:00+09:00",
  "variants": [
    {
      "variant_id": "2538974832789",
      "sku": "test-product",
      "image_url": "https://recustomer.me/image/12345",
      "name": "テスト商品",
      "quantity_in_stock": 0,
      "created_at": "2023-05-10T12:00:00+09:00",
      "updated_at": "2023-05-12T10:30:00+09:00",
      "variant_options": [
        {
          "name": "カラー",
          "value": "レッド"
        }
      ]
    }
  ]
}
JSON;
```

### Go

```go
package main

import (
	"crypto/subtle"
	"log"
	"net/http"
	"os"
)

const responseBody = `{
  "product_id": "product_id",
  "product_name": "テスト商品",
  "image_url": "https://recustomer.me/image/12345",
  "created_at": "2023-05-10T12:00:00+09:00",
  "updated_at": "2023-05-12T10:30:00+09:00",
  "variants": [
    {
      "variant_id": "2538974832789",
      "sku": "test-product",
      "image_url": "https://recustomer.me/image/12345",
      "name": "テスト商品",
      "quantity_in_stock": 0,
      "created_at": "2023-05-10T12:00:00+09:00",
      "updated_at": "2023-05-12T10:30:00+09:00",
      "variant_options": [
        {
          "name": "カラー",
          "value": "レッド"
        }
      ]
    }
  ]
}`

func main() {
	token := os.Getenv("RECUSTOMER_API_TOKEN")
	if token == "" {
		log.Fatal("RECUSTOMER_API_TOKEN is not set")
	}
	http.HandleFunc("GET /products/{product_id}", 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
		}
		log.Println(r.PathValue("product_id"))
		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"] }, subtle = "2"
use axum::{
    extract::{Path, State},
    http::{header, HeaderMap, StatusCode},
    response::{IntoResponse, Response},
    routing::get,
    Router,
};
use subtle::ConstantTimeEq;

const RESPONSE_BODY: &str = r#"{
  "product_id": "product_id",
  "product_name": "テスト商品",
  "image_url": "https://recustomer.me/image/12345",
  "created_at": "2023-05-10T12:00:00+09:00",
  "updated_at": "2023-05-12T10:30:00+09:00",
  "variants": [
    {
      "variant_id": "2538974832789",
      "sku": "test-product",
      "image_url": "https://recustomer.me/image/12345",
      "name": "テスト商品",
      "quantity_in_stock": 0,
      "created_at": "2023-05-10T12:00:00+09:00",
      "updated_at": "2023-05-12T10:30:00+09:00",
      "variant_options": [
        {
          "name": "カラー",
          "value": "レッド"
        }
      ]
    }
  ]
}"#;

async fn handle(State(token): State<String>, Path(product_id): Path<String>, headers: HeaderMap) -> 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!("{product_id}");
    (StatusCode::OK, [(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("/products/{product_id}", get(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
{
  "product_id": "product_id",
  "product_name": "テスト商品",
  "image_url": "https://recustomer.me/image/12345",
  "created_at": "2023-05-10T12:00:00+09:00",
  "updated_at": "2023-05-12T10:30:00+09:00",
  "variants": [
    {
      "variant_id": "2538974832789",
      "sku": "test-product",
      "image_url": "https://recustomer.me/image/12345",
      "name": "テスト商品",
      "quantity_in_stock": 0,
      "created_at": "2023-05-10T12:00:00+09:00",
      "updated_at": "2023-05-12T10:30:00+09:00",
      "variant_options": [
        {
          "name": "カラー",
          "value": "レッド"
        }
      ]
    }
  ]
}
```
