Developers/Get started
Authentication
Every API call carries your Merchant ID and your API key.
Placing an order (POST/orders) also carries a signature, so nobody can change or repeat it.
Related: Catalog and orders · Security · README
#Credentials
| Credential | Example | Secret? |
|---|---|---|
| Merchant ID | M00000001 | No. It says who you are. |
| API key | cvb2b_... | Yes. It is your password and your signing key. |
- CardV gives you the Merchant ID when your account is approved.
- An Owner creates the API key in the Portal (see API keys).
- Live and Sandbox have different keys. A key from one never works in the other.
- The API works only after CardV approves your business.
GET/accountthen shows"api_access_enabled": true.
#Headers
Send these two headers on every call:
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxOn POST/orders, also send these three:
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestampis the current Unix time in seconds. It must be within 5 minutes (300 seconds) of CardV's clock.X-Nonceis a random value that you never use twice. Use 32 random hex characters.X-Signatureis explained in Signing an order. It must be lowercase hex.
GET calls do not need a signature.
#Signing an order
Turn your order into JSON text once. You will sign and send these exact bytes.
Hash the body:
BODY_HASH= SHA-256 of the body, as lowercase hex.Join these five lines with a newline (
\n), with no newline at the end:POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>Sign it:
X-Signature= HMAC-SHA256 of that text, using your full API key as the key. Write the result as lowercase hex.
The most common mistake is signing one version of the JSON and sending another.
For example, {"a":1} and {"a": 1} are different bytes.
Make sure your HTTP library does not change the body after you sign it.
Test vector. Check your code with these values. The key is fake.
API key cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method POST
Path /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefBody (108 bytes, one line, no newline at the end):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}Expected results:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#Code samples
All samples produce the test vector result. Load the key from your secret store, not from code.
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"Use --data-raw, not -d @file. -d removes newlines, so the sent bytes would not match.
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+ (built-in 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 (signing only)
<?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);#Errors
All of these return HTTP 403 (not 401), except the rate limit, which returns 429. The body looks like this:
{"detail": "Invalid HMAC signature."}| Problem | What to do |
|---|---|
| Missing or wrong Merchant ID or API key | Check both headers and the environment. |
| API access not enabled | Wait for CardV to approve your business. |
| Your server's IP is not allowed | Add it to the IP allowlist in the Portal. |
| Endpoint is Portal only | Use the Portal. Only seven endpoints work with a key. |
| Missing or wrong signature | Fix your signing code. Test it with the test vector. |
| Timestamp too old or new | Sync your server clock (NTP). |
| Nonce already used | Use a new random nonce on every order request. |
| Too many requests (429) | Wait the seconds in Retry-After, then try again. |
Important:
- When you send an order again, create a new timestamp, nonce and signature. Keep the body the same. See safe retries.
- A nonce is used up even if the signature was wrong.
- CardV records failed attempts and alerts its team if there are many.
#IP allowlist
The IP allowlist lets only your servers use your key. You manage it in the Portal (Owner).
- It is optional. With no rules, calls from any IP are accepted.
- With at least one rule, calls from other IPs get HTTP 403.
- Rules look like
203.0.113.10/32(IPv4) or2001:db8::/48(IPv6). - It applies only to API key calls, not to Portal sign-in.
- Add every server IP before you turn it on, including NAT gateways and backup regions.
#API keys
An API key can use exactly the seven endpoints in The API at a glance. That includes placing orders and reading codes, so guard it like an Owner password.
Only an Owner can manage keys, in the Portal under Integrations → API keys. Do this separately in Live and Sandbox.
- Create a key and give it a name. The Portal does not show the key yet.
- Reveal it. CardV emails you a 6-digit code, valid for 10 minutes (5 tries). Enter it to see the full key. Save it right away. Every reveal is logged.
- Disable a key when you no longer need it. This is immediate and permanent.
To change keys without downtime:
- Create and reveal a new key.
- Deploy it to all your servers.
- Check in the Portal that the old key is no longer used.
- Disable the old key.
Change keys at least once a year, and whenever someone with access leaves. If a key may have leaked, disable it first, then investigate.
Questions about your integration? Email [email protected] with your Merchant ID and the order or request ID.