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

CredentialExampleSecret?
Merchant IDM00000001No. It says who you are.
API keycvb2b_...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/account then shows "api_access_enabled": true.

#Headers

Send these two headers on every call:

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

On POST/orders, also send these three:

HTTP
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed
  • X-Timestamp is the current Unix time in seconds. It must be within 5 minutes (300 seconds) of CardV's clock.
  • X-Nonce is a random value that you never use twice. Use 32 random hex characters.
  • X-Signature is explained in Signing an order. It must be lowercase hex.

GET calls do not need a signature.

#Signing an order

  1. Turn your order into JSON text once. You will sign and send these exact bytes.

  2. Hash the body: BODY_HASH = SHA-256 of the body, as lowercase hex.

  3. Join these five lines with a newline (\n), with no newline at the end:

    Text
    POST
    /api/v1/orders
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>
  4. 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.

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

Body (108 bytes, one line, no newline at the end):

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

Expected results:

Text
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)

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"

Use --data-raw, not -d @file. -d removes newlines, so the sent bytes would not match.

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+ (built-in 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 (signing only)

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);

#Errors

All of these return HTTP 403 (not 401), except the rate limit, which returns 429. The body looks like this:

JSON
{"detail": "Invalid HMAC signature."}
ProblemWhat to do
Missing or wrong Merchant ID or API keyCheck both headers and the environment.
API access not enabledWait for CardV to approve your business.
Your server's IP is not allowedAdd it to the IP allowlist in the Portal.
Endpoint is Portal onlyUse the Portal. Only seven endpoints work with a key.
Missing or wrong signatureFix your signing code. Test it with the test vector.
Timestamp too old or newSync your server clock (NTP).
Nonce already usedUse 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) or 2001: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.

  1. Create a key and give it a name. The Portal does not show the key yet.
  2. 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.
  3. Disable a key when you no longer need it. This is immediate and permanent.

To change keys without downtime:

  1. Create and reveal a new key.
  2. Deploy it to all your servers.
  3. Check in the Portal that the old key is no longer used.
  4. 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.