開発者/はじめに
認証
APIを呼び出すときは、毎回key IDと署名を送ります。署名によって、呼び出しが貴社からのものであることを証明し、第三者が内容を書き換えたり再送したりできなくなります。signing secret自体が送信されることはありません。
関連ドキュメント:商品と注文 · セキュリティ · README
#認証情報
| 認証情報 | 例 | 秘密情報か |
|---|---|---|
| key ID | ck_live_... | いいえ。どのキーを使うかを示します。 |
| signing secret | cs_live_... | はい。 署名に使います。貴社のサーバーの外に出してはいけません。 |
| Merchant ID | M00000001 | いいえ。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つのヘッダーを送ります。
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89X-Key-Idはキーの公開IDです。X-Timestampは現在のUnix時間(秒)です。CardVのサーバー時刻との差が5分(300秒)以内である必要があります。X-Nonceは、一度しか使わないランダムな値です。ランダムな16進数32文字を使ってください。X-Signatureの作り方はリクエストの署名で説明します。16進数の小文字で書いてください。
X-Merchant-Id を送ることもできます。送る場合は、キーと一致している必要があります。
#リクエストの署名
リクエスト本文をバイト列に変換します。変換は1回だけです。署名するバイト列と送信するバイト列は、まったく同じものにします。
GETの呼び出しでは本文は空です。本文をハッシュ化します。
BODY_HASHは本文のSHA-256を16進数の小文字で表したものです。本文が空の場合はe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855になります。次の5行を改行(
\n)でつなぎます。最後の行の後には改行を入れません。<METHOD> <PATH with query string> <X-Timestamp> <X-Nonce> <BODY_HASH>METHODは大文字で書きます(例:GET、POST)。パスには、送信するとおりのクエリ文字列を含めます(例:/api/v1/skus?limit=50)。署名します。
X-Signatureは、signing secretを鍵にして、上の文字列のHMAC-SHA256を計算したものです。結果は16進数の小文字で書きます。
モバイルチャージの呼び出しも同じ方法で署名します。パスはそれぞれのもの(例:/api/v1/recharge/orders)を使います。
最も多い間違いは、署名したJSONと送信したJSONが違っていることです。たとえば {"a":1} と {"a": 1} は別のバイト列です。署名した後に、HTTPライブラリが本文を書き換えたり、クエリ文字列の順序を変えたりしないように注意してください。
テストベクター:次の値でコードが正しいか確認できます。secretはダミーです。
Signing secret cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefGET の呼び出し:
Method GET
Path /api/v1/skus?limit=50
BODY_HASH e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature 58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5次の本文(106バイト、1行、末尾に改行なし)で /api/v1/orders に送る POST の呼び出し:
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}BODY_HASH 5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89#サンプルコード
どのサンプルも、テストベクターと同じ結果になります。secretはコードに直接書かず、シークレット管理ツールから読み込んでください。
cURL(bash + OpenSSL)
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)
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)
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
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を返します。レスポンス本文は次のような形です。
{"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でそれぞれ別に設定してください。
- 作成:キーを作成し、名前を付けます。Portalにkey IDとsigning secretが表示されます。すぐにsecretをシークレット管理ツールにコピーしてください。表示されるのはこの1回だけです。
- secretの再生成:signing secretを紛失した場合は再生成します。key IDは変わりません。古いsecretはすぐに使えなくなるため、新しいsecretをただちにすべてのサーバーに反映してください。
- 無効化:不要になったキーは無効にしてください。無効化はすぐに反映され、元に戻せません。無効にしたキーはその後一覧から削除できます。履歴はCardVに残ります。
サービスを止めずにキーを切り替える手順:
- 新しいキーを作成し、そのsecretを保存します。
- 新しいキーをすべてのサーバーに反映します。
- 古いキーが使われていないことをPortalで確認します。
- 古いキーを無効にします。
キーは少なくとも年に1回、またキーにアクセスできる担当者が退職・異動したときにも切り替えてください。キーが漏れた可能性がある場合は、まずキーを無効にしてから調査してください。
#旧形式のキー
2026年10月より前に作成されたキーは cvb2b_ で始まり、旧来のヘッダー
X-Merchant-Id と X-Api-Key を使います。署名は POST の呼び出しにだけ付けます。旧形式のキーはすべてのリクエストで送信されるため、漏れやすくなります。
Live と Sandbox のどちらでも、これらのキーはすでに使えません。今すぐ新しいキーに移行してください。Portalで新しいキーを作成し、コードを上記のヘッダーに切り替えてから、古いキーを無効にします。
連携についてご不明な点は、Merchant ID と注文 ID またはリクエスト ID を添えて [email protected] までお問い合わせください。