開発者/はじめに
認証
APIを呼び出すときは、毎回Merchant IDとAPIキーを送ります。注文(POST/orders)のときはさらに署名も送ります。これにより、第三者が注文内容を書き換えたり、同じ注文を再送したりできなくなります。
関連ドキュメント:商品と注文 · セキュリティ · README
#認証情報
| 認証情報 | 例 | 秘密情報か |
|---|---|---|
| Merchant ID | M00000001 | いいえ。貴社を識別するためのIDです。 |
| APIキー | cvb2b_... | はい。 パスワードであり、署名用の鍵でもあります。 |
- Merchant IDは、アカウントが承認されたときにCardVから発行されます。
- APIキーは、OwnerがPortalで作成します(APIキーを参照)。
- LiveとSandboxではキーが別々です。一方のキーをもう一方で使うことはできません。
- APIは、CardVが貴社の審査を承認した後に使えるようになります。承認されると、
GET/accountに"api_access_enabled": trueと表示されます。
#リクエストヘッダー
すべての呼び出しで、次の2つのヘッダーを送ります。
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPOST/orders では、さらに次の3つも送ります。
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestampは現在のUnix時間(秒)です。CardVのサーバー時刻との差が5分(300秒)以内である必要があります。X-Nonceは、一度しか使わないランダムな値です。ランダムな16進数32文字を使ってください。X-Signatureの作り方は注文の署名で説明します。16進数の小文字で書いてください。
GET の呼び出しには署名は不要です。
#注文の署名
注文内容をJSON文字列に変換します。変換は1回だけです。署名するバイト列と送信するバイト列は、まったく同じものにします。
本文をハッシュ化します。
BODY_HASHは本文のSHA-256を16進数の小文字で表したものです。次の5行を改行(
\n)でつなぎます。最後の行の後には改行を入れません。POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>署名します。
X-Signatureは、APIキー全体を鍵にして、上の文字列のHMAC-SHA256を計算したものです。結果は16進数の小文字で書きます。
最も多い間違いは、署名したJSONと送信したJSONが違っていることです。たとえば {"a":1} と {"a": 1} は別のバイト列です。署名した後に、HTTPライブラリが本文を書き換えないように注意してください。
テストベクター:次の値でコードが正しいか確認できます。キーはダミーです。
API key cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method POST
Path /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdef本文(108バイト、1行、末尾に改行なし):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}正しい結果:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#サンプルコード
どのサンプルも、テストベクターと同じ結果になります。キーはコードに直接書かず、シークレット管理ツールから読み込んでください。
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.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)
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)
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
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を返します。レスポンス本文は次のような形です。
{"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でそれぞれ別に設定してください。
- 作成:キーを作成し、名前を付けます。この時点ではまだキーは表示されません。
- 表示(Reveal):CardVから6桁の確認コードがメールで届きます。有効期限は10分、入力できるのは5回までです。コードを入力するとキー全体が表示されるので、すぐに保存してください。表示するたびに記録が残ります。
- 無効化:不要になったキーは無効にしてください。無効化はすぐに反映され、元に戻せません。
サービスを止めずにキーを切り替える手順:
- 新しいキーを作成して表示します。
- 新しいキーをすべてのサーバーに反映します。
- 古いキーが使われていないことをPortalで確認します。
- 古いキーを無効にします。
キーは少なくとも年に1回、またキーにアクセスできる担当者が退職・異動したときにも切り替えてください。キーが漏れた可能性がある場合は、まずキーを無効にしてから調査してください。
連携についてご不明な点は、Merchant ID と注文 ID またはリクエスト ID を添えて [email protected] までお問い合わせください。