開発者/はじめに

認証

APIを呼び出すときは、毎回Merchant IDとAPIキーを送ります。注文(POST/orders)のときはさらに署名も送ります。これにより、第三者が注文内容を書き換えたり、同じ注文を再送したりできなくなります。

関連ドキュメント:商品と注文 · セキュリティ · README

#認証情報

認証情報例秘密情報か
Merchant IDM00000001いいえ。貴社を識別するためのIDです。
APIキーcvb2b_...はい。 パスワードであり、署名用の鍵でもあります。
  • Merchant IDは、アカウントが承認されたときにCardVから発行されます。
  • APIキーは、OwnerがPortalで作成します(APIキーを参照)。
  • LiveとSandboxではキーが別々です。一方のキーをもう一方で使うことはできません。
  • APIは、CardVが貴社の審査を承認した後に使えるようになります。承認されると、GET/account に "api_access_enabled": true と表示されます。

#リクエストヘッダー

すべての呼び出しで、次の2つのヘッダーを送ります。

HTTP
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

POST/orders では、さらに次の3つも送ります。

HTTP
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed
  • X-Timestamp は現在のUnix時間(秒)です。CardVのサーバー時刻との差が5分(300秒)以内である必要があります。
  • X-Nonce は、一度しか使わないランダムな値です。ランダムな16進数32文字を使ってください。
  • X-Signature の作り方は注文の署名で説明します。16進数の小文字で書いてください。

GET の呼び出しには署名は不要です。

#注文の署名

  1. 注文内容をJSON文字列に変換します。変換は1回だけです。署名するバイト列と送信するバイト列は、まったく同じものにします。

  2. 本文をハッシュ化します。BODY_HASH は本文のSHA-256を16進数の小文字で表したものです。

  3. 次の5行を改行(\n)でつなぎます。最後の行の後には改行を入れません。

    Text
    POST
    /api/v1/orders
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>
  4. 署名します。X-Signature は、APIキー全体を鍵にして、上の文字列のHMAC-SHA256を計算したものです。結果は16進数の小文字で書きます。

最も多い間違いは、署名したJSONと送信したJSONが違っていることです。たとえば {"a":1} と {"a": 1} は別のバイト列です。署名した後に、HTTPライブラリが本文を書き換えないように注意してください。

テストベクター:次の値でコードが正しいか確認できます。キーはダミーです。

Text
API key      cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method       POST
Path         /api/v1/orders
X-Timestamp  1790000000
X-Nonce      0123456789abcdef0123456789abcdef

本文(108バイト、1行、末尾に改行なし):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}

正しい結果:

Text
BODY_HASH    b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature  fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed

#サンプルコード

どのサンプルも、テストベクターと同じ結果になります。キーはコードに直接書かず、シークレット管理ツールから読み込んでください。

cURL(bash + OpenSSL)

Shell
BASE_URL="https://sandbox.cardv.net"
REQ_PATH="/api/v1/orders"      # do not call this variable PATH
BODY='{"external_order_id":"SHOP-10001","items":'
BODY+='[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$REQ_PATH" "$TS" "$NONCE" "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$CARDV_API_KEY" -hex | sed 's/^.*= //')

curl -sS -X POST "$BASE_URL$REQ_PATH" \
  -H "Content-Type: application/json" \
  -H "X-Merchant-Id: $CARDV_MERCHANT_ID" \
  -H "X-Api-Key: $CARDV_API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Nonce: $NONCE" \
  -H "X-Signature: $SIG" \
  --data-raw "$BODY"

-d @file ではなく --data-raw を使ってください。-d は改行を取り除くため、送信するバイト列が署名したものと一致しなくなります。

Python(requests)

Python
import hashlib, hmac, json, os, secrets, time
import requests

BASE_URL = "https://sandbox.cardv.net"
MERCHANT_ID = os.environ["CARDV_MERCHANT_ID"]
API_KEY = os.environ["CARDV_API_KEY"]
AUTH = {"X-Merchant-Id": MERCHANT_ID, "X-Api-Key": API_KEY}


def sign(path: str, body: bytes, ts: str, nonce: str) -> str:
    body_hash = hashlib.sha256(body).hexdigest()
    text = "\n".join(["POST", path, ts, nonce, body_hash])
    return hmac.new(API_KEY.encode(), text.encode(), hashlib.sha256).hexdigest()


def cardv_get(path: str, params=None) -> requests.Response:
    return requests.get(BASE_URL + path, params=params, headers=AUTH, timeout=30)


def cardv_post(path: str, payload: dict) -> requests.Response:
    body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
    ts, nonce = str(int(time.time())), secrets.token_hex(16)
    headers = {
        **AUTH,
        "Content-Type": "application/json",
        "X-Timestamp": ts,
        "X-Nonce": nonce,
        "X-Signature": sign(path, body, ts, nonce),
    }
    return requests.post(BASE_URL + path, data=body, headers=headers, timeout=30)


resp = cardv_post("/api/v1/orders", {
    "external_order_id": "SHOP-10001",
    "items": [{"sku_id": "S000001", "quantity": 1, "expected_unit_price": "9.2500"}],
})
print(resp.status_code, resp.json())

order_id = resp.json()["order"]["order_id"]
print(cardv_get(f"/api/v1/orders/{order_id}").json()["status"])

Node.js 18以降(標準のfetch)

Node.js
import crypto from "node:crypto";

const BASE_URL = "https://sandbox.cardv.net";
const { CARDV_MERCHANT_ID, CARDV_API_KEY } = process.env;
const AUTH = { "X-Merchant-Id": CARDV_MERCHANT_ID, "X-Api-Key": CARDV_API_KEY };

function sign(path, body, timestamp, nonce) {
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const text = ["POST", path, timestamp, nonce, bodyHash].join("\n");
  return crypto.createHmac("sha256", CARDV_API_KEY).update(text, "utf8").digest("hex");
}

async function cardvPost(path, payload) {
  const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
  const timestamp = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString("hex");
  const headers = {
    ...AUTH,
    "Content-Type": "application/json",
    "X-Timestamp": timestamp,
    "X-Nonce": nonce,
    "X-Signature": sign(path, body, timestamp, nonce),
  };
  const res = await fetch(BASE_URL + path, { method: "POST", headers, body });
  return { status: res.status, body: await res.json() };
}

console.log(await cardvPost("/api/v1/orders", {
  external_order_id: "SHOP-10001",
  items: [{ sku_id: "S000001", quantity: 1, expected_unit_price: "9.2500" }],
}));

PHP(署名部分のみ)

PHP
<?php
function cardv_sign(
    string $apiKey, string $path, string $body, string $ts, string $nonce
): string {
    $text = implode("\n", ['POST', $path, $ts, $nonce, hash('sha256', $body)]);
    return hash_hmac('sha256', $text, $apiKey); // lowercase hex
}

// Send exactly $body.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($apiKey, '/api/v1/orders', $body, $ts, $nonce);

#エラー

以下のエラーはすべてHTTP 403(401ではありません)を返します。ただし、レート制限だけは429を返します。レスポンス本文は次のような形です。

JSON
{"detail": "Invalid HMAC signature."}
原因対処
Merchant IDまたはAPIキーがない、または間違っている2つのヘッダーと、接続先の環境を確認してください。
APIの利用が有効になっていないCardVの審査承認をお待ちください。
貴社サーバーのIPアドレスが許可されていないPortalのIP許可リストに追加してください。
Portal専用のエンドポイントを呼び出したPortalで操作してください。APIキーで使えるのは7つのエンドポイントだけです。
署名がない、または間違っている署名のコードを修正し、テストベクターで確認してください。
タイムスタンプが古すぎる、または未来すぎるサーバーの時刻を合わせてください(NTP)。
nonceが使用済み注文リクエストのたびに、新しいランダムなnonceを使ってください。
リクエストが多すぎる(429)Retry-After の秒数だけ待ってから再試行してください。

注意点:

  • 注文を再送するときは、タイムスタンプ、nonce、署名を新しく作り直してください。本文は変えないでください。詳しくは安全な再送をご覧ください。
  • 署名が間違っていた場合でも、そのnonceは使用済みになります。
  • CardVは失敗したリクエストを記録しており、失敗が多い場合は担当チームに通知が届きます。

#IP許可リスト

IP許可リストを使うと、貴社のサーバーからしかAPIキーを使えないように制限できます。設定はPortalで行います(Owner)。

  • 設定は任意です。ルールがなければ、どのIPアドレスからの呼び出しも受け付けます。
  • ルールを1つでも登録すると、それ以外のIPアドレスからの呼び出しにはHTTP 403が返ります。
  • ルールは 203.0.113.10/32(IPv4)や 2001:db8::/48(IPv6)のように書きます。
  • 対象はAPIキーによる呼び出しだけです。Portalへのサインインには影響しません。
  • 有効にする前に、NATゲートウェイや予備リージョンも含め、すべてのサーバーのIPアドレスを登録してください。

#APIキー

APIキーで使えるのは、APIの概要にある7つのエンドポイントだけです。その中には注文とコードの取得も含まれるため、Ownerのパスワードと同じように厳重に管理してください。

キーを管理できるのはOwnerだけです。Portalの Integrations → API keys で操作します。LiveとSandboxでそれぞれ別に設定してください。

  1. 作成:キーを作成し、名前を付けます。この時点ではまだキーは表示されません。
  2. 表示(Reveal):CardVから6桁の確認コードがメールで届きます。有効期限は10分、入力できるのは5回までです。コードを入力するとキー全体が表示されるので、すぐに保存してください。表示するたびに記録が残ります。
  3. 無効化:不要になったキーは無効にしてください。無効化はすぐに反映され、元に戻せません。

サービスを止めずにキーを切り替える手順:

  1. 新しいキーを作成して表示します。
  2. 新しいキーをすべてのサーバーに反映します。
  3. 古いキーが使われていないことをPortalで確認します。
  4. 古いキーを無効にします。

キーは少なくとも年に1回、またキーにアクセスできる担当者が退職・異動したときにも切り替えてください。キーが漏れた可能性がある場合は、まずキーを無効にしてから調査してください。

連携についてご不明な点は、Merchant ID と注文 ID またはリクエスト ID を添えて [email protected] までお問い合わせください。