開発者/はじめに

認証

APIを呼び出すときは、毎回key IDと署名を送ります。署名によって、呼び出しが貴社からのものであることを証明し、第三者が内容を書き換えたり再送したりできなくなります。signing secret自体が送信されることはありません。

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

#認証情報

認証情報例秘密情報か
key IDck_live_...いいえ。どのキーを使うかを示します。
signing secretcs_live_...はい。 署名に使います。貴社のサーバーの外に出してはいけません。
Merchant IDM00000001いいえ。APIの呼び出しでは省略できます。
  • キーは、OwnerがPortalで作成します(APIキーを参照)。
  • Portalはキーの作成時に、signing secretを1回だけ表示します。CardVを含め、誰も再表示できません。
  • Liveのキーは ck_live_ と cs_live_ で始まります。Sandboxのキーは ck_test_ と cs_test_ で始まります。一方の環境のキーをもう一方で使うことはできません。
  • APIは、CardVが貴社の審査を承認した後に使えるようになります。承認されると、GET/account に "api_access_enabled": true と表示されます。

#リクエストヘッダー

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

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

X-Merchant-Id を送ることもできます。送る場合は、キーと一致している必要があります。

#リクエストの署名

  1. リクエスト本文をバイト列に変換します。変換は1回だけです。署名するバイト列と送信するバイト列は、まったく同じものにします。 GET の呼び出しでは本文は空です。

  2. 本文をハッシュ化します。BODY_HASH は本文のSHA-256を16進数の小文字で表したものです。本文が空の場合は e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 になります。

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

    Text
    <METHOD>
    <PATH with query string>
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>

    METHOD は大文字で書きます(例:GET、POST)。パスには、送信するとおりのクエリ文字列を含めます(例:/api/v1/skus?limit=50)。

  4. 署名します。X-Signature は、signing secretを鍵にして、上の文字列のHMAC-SHA256を計算したものです。結果は16進数の小文字で書きます。

モバイルチャージの呼び出しも同じ方法で署名します。パスはそれぞれのもの(例:/api/v1/recharge/orders)を使います。

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

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

Text
Signing secret  cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp     1790000000
X-Nonce         0123456789abcdef0123456789abcdef

GET の呼び出し:

Text
Method       GET
Path         /api/v1/skus?limit=50
BODY_HASH    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature  58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5

次の本文(106バイト、1行、末尾に改行なし)で /api/v1/orders に送る POST の呼び出し:

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}
Text
BODY_HASH    5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature  e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89

#サンプルコード

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

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.25"}]}'
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_SIGNING_SECRET" -hex | sed 's/^.*= //')

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

-d @file ではなく --data-raw を使ってください。-d は改行を取り除くため、送信するバイト列が署名したものと一致しなくなります。 GET の呼び出しでは、GET、クエリ文字列を含むパス、空の本文のハッシュに署名し、本文は送りません。

Python(requests)

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

BASE_URL = "https://sandbox.cardv.net"
KEY_ID = os.environ["CARDV_KEY_ID"]
SIGNING_SECRET = os.environ["CARDV_SIGNING_SECRET"]


def signed_headers(method: str, path: str, body: bytes) -> dict:
    ts, nonce = str(int(time.time())), secrets.token_hex(16)
    body_hash = hashlib.sha256(body).hexdigest()
    text = "\n".join([method, path, ts, nonce, body_hash])
    signature = hmac.new(SIGNING_SECRET.encode(), text.encode(), hashlib.sha256).hexdigest()
    return {"X-Key-Id": KEY_ID, "X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": signature}


def cardv_get(path: str) -> requests.Response:
    # Put the query string in `path` so the signed path and the sent path are identical.
    return requests.get(BASE_URL + path, headers=signed_headers("GET", path, b""), timeout=30)


def cardv_post(path: str, payload: dict) -> requests.Response:
    body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
    headers = {**signed_headers("POST", path, body), "Content-Type": "application/json"}
    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.25"}],
})
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_KEY_ID, CARDV_SIGNING_SECRET } = process.env;

function signedHeaders(method, path, body) {
  const timestamp = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString("hex");
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const text = [method, path, timestamp, nonce, bodyHash].join("\n");
  const signature = crypto.createHmac("sha256", CARDV_SIGNING_SECRET).update(text, "utf8").digest("hex");
  return { "X-Key-Id": CARDV_KEY_ID, "X-Timestamp": timestamp, "X-Nonce": nonce, "X-Signature": signature };
}

async function cardvGet(path) {
  const res = await fetch(BASE_URL + path, { headers: signedHeaders("GET", path, Buffer.alloc(0)) });
  return { status: res.status, body: await res.json() };
}

async function cardvPost(path, payload) {
  const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
  const headers = { ...signedHeaders("POST", path, body), "Content-Type": "application/json" };
  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.25" }],
}));
console.log(await cardvGet("/api/v1/balance"));

PHP(署名部分のみ)

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

// Send exactly $body. For a GET call use $body = ''.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($signingSecret, 'POST', '/api/v1/orders', $body, $ts, $nonce);
// Headers: X-Key-Id, X-Timestamp, X-Nonce, X-Signature

#エラー

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

JSON
{"detail": "Invalid HMAC signature."}
原因対処
key IDが不明、または無効化されているX-Key-Id と、接続先の環境(LiveかSandboxか)を確認してください。
APIの利用が有効になっていないCardVの審査承認をお待ちください。
貴社サーバーのIPアドレスが許可されていないPortalのIP許可リストに追加してください。
Portal専用のエンドポイントを呼び出したPortalで操作してください。APIキーで使えるのは16のエンドポイントだけです。
署名がない、または間違っている署名のコードを修正し、テストベクターで確認してください。
タイムスタンプが古すぎる、または未来すぎるサーバーの時刻を合わせてください(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の概要にある16のエンドポイントだけです。その中には注文とコードの取得も含まれるため、signing secretはOwnerのパスワードと同じように厳重に管理してください。

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

  1. 作成:キーを作成し、名前を付けます。Portalにkey IDとsigning secretが表示されます。すぐにsecretをシークレット管理ツールにコピーしてください。表示されるのはこの1回だけです。
  2. secretの再生成:signing secretを紛失した場合は再生成します。key IDは変わりません。古いsecretはすぐに使えなくなるため、新しいsecretをただちにすべてのサーバーに反映してください。
  3. 無効化:不要になったキーは無効にしてください。無効化はすぐに反映され、元に戻せません。無効にしたキーはその後一覧から削除できます。履歴はCardVに残ります。

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

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

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

#旧形式のキー

2026年10月より前に作成されたキーは cvb2b_ で始まり、旧来のヘッダー X-Merchant-Id と X-Api-Key を使います。署名は POST の呼び出しにだけ付けます。旧形式のキーはすべてのリクエストで送信されるため、漏れやすくなります。

Live と Sandbox のどちらでも、これらのキーはすでに使えません。今すぐ新しいキーに移行してください。Portalで新しいキーを作成し、コードを上記のヘッダーに切り替えてから、古いキーを無効にします。

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