Desarrolladores/Primeros pasos
Autenticación
Cada llamada a la API lleva tu Merchant ID y tu clave de API.
Al hacer un pedido (POST/orders) también envías una firma, para que nadie pueda modificarlo ni repetirlo.
Ver también: Catálogo y pedidos · Seguridad · README
#Credenciales
| Credencial | Ejemplo | ¿Es secreta? |
|---|---|---|
| Merchant ID | M00000001 | No. Solo indica quién eres. |
| Clave de API | cvb2b_... | Sí. Es tu contraseña y también tu clave para firmar. |
- CardV te da el Merchant ID cuando aprueba tu cuenta.
- Un Owner crea la clave de API en el Portal (ver Claves de API).
- Live y Sandbox usan claves distintas. Una clave de un entorno nunca funciona en el otro.
- La API solo funciona cuando CardV ya aprobó tu empresa.
A partir de ahí,
GET/accountmuestra"api_access_enabled": true.
#Encabezados
Envía estos dos encabezados en cada llamada:
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEn POST/orders, envía además estos tres:
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestampes la hora actual en formato Unix, en segundos. No puede diferir más de 5 minutos (300 segundos) del reloj de CardV.X-Noncees un valor aleatorio que nunca repites. Usa 32 caracteres hexadecimales aleatorios.X-Signaturese explica en Cómo firmar un pedido. Debe ir en hexadecimal y en minúsculas.
Las llamadas GET no necesitan firma.
#Cómo firmar un pedido
Convierte tu pedido en texto JSON una sola vez. Esos mismos bytes son los que firmas y envías.
Calcula el hash del cuerpo:
BODY_HASH= SHA-256 del cuerpo, en hexadecimal y en minúsculas.Une estas cinco líneas con un salto de línea (
\n), sin salto de línea al final:POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>Fírmalo:
X-Signature= HMAC-SHA256 de ese texto, usando tu clave de API completa como clave. Escribe el resultado en hexadecimal y en minúsculas.
El error más común es firmar una versión del JSON y enviar otra.
Por ejemplo, {"a":1} y {"a": 1} son bytes distintos.
Asegúrate de que tu librería HTTP no modifique el cuerpo después de firmarlo.
Vector de prueba. Comprueba tu código con estos valores. La clave es falsa.
API key cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method POST
Path /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefCuerpo (108 bytes, en una sola línea, sin salto de línea al final):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}Resultados esperados:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#Ejemplos de código
Todos los ejemplos dan el resultado del vector de prueba. Carga la clave desde tu gestor de secretos, no la pongas en el código.
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"Usa --data-raw, no -d @file. -d elimina los saltos de línea, así que los bytes enviados ya no coincidirían con los firmados.
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 integrado)
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 (solo la firma)
<?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);#Errores
Todos estos casos devuelven HTTP 403 (no 401), salvo el límite de solicitudes, que devuelve 429. El cuerpo de la respuesta se ve así:
{"detail": "Invalid HMAC signature."}| Problema | Qué hacer |
|---|---|
| Falta el Merchant ID o la clave de API, o son incorrectos | Revisa los dos encabezados y el entorno. |
| El acceso a la API no está activado | Espera a que CardV apruebe tu empresa. |
| La IP de tu servidor no está permitida | Agrégala a la lista de IPs permitidas en el Portal. |
| El endpoint solo funciona en el Portal | Usa el Portal. Con una clave solo funcionan siete endpoints. |
| Falta la firma o es incorrecta | Corrige tu código de firma. Pruébalo con el vector de prueba. |
| La marca de tiempo es demasiado antigua o futura | Sincroniza el reloj de tu servidor (NTP). |
| El nonce ya se usó | Usa un nonce aleatorio nuevo en cada solicitud de pedido. |
| Demasiadas solicitudes (429) | Espera los segundos que indica Retry-After y vuelve a intentar. |
Importante:
- Cuando reenvíes un pedido, genera una marca de tiempo, un nonce y una firma nuevos. El cuerpo no cambia. Consulta cómo reintentar sin riesgo.
- Un nonce queda usado aunque la firma haya sido incorrecta.
- CardV registra los intentos fallidos y alerta a su equipo si son muchos.
#Lista de IPs permitidas
La lista de IPs permitidas hace que solo tus servidores puedan usar tu clave. La administras en el Portal (Owner).
- Es opcional. Sin reglas, se aceptan llamadas desde cualquier IP.
- Con al menos una regla, las llamadas desde otras IPs reciben HTTP 403.
- Las reglas tienen este formato:
203.0.113.10/32(IPv4) o2001:db8::/48(IPv6). - Solo se aplica a las llamadas con clave de API, no al inicio de sesión en el Portal.
- Agrega todas las IPs de tus servidores antes de activarla, incluidos gateways NAT y regiones de respaldo.
#Claves de API
Una clave de API puede usar exactamente los siete endpoints de La API en resumen. Eso incluye hacer pedidos y leer códigos, así que cuídala como la contraseña de un Owner.
Solo un Owner puede administrar las claves, en el Portal, en Integrations → API keys. Hazlo por separado en Live y en Sandbox.
- Crea una clave y ponle un nombre. El Portal todavía no muestra la clave.
- Muéstrala con Reveal. CardV te envía por correo un código de 6 dígitos, válido por 10 minutos (5 intentos). Ingrésalo para ver la clave completa. Guárdala de inmediato. Cada vez que se muestra una clave, queda registrado.
- Desactiva una clave cuando ya no la necesites. El cambio es inmediato y definitivo.
Para cambiar de clave sin interrumpir el servicio:
- Crea una clave nueva y muéstrala.
- Instálala en todos tus servidores.
- Comprueba en el Portal que la clave anterior ya no se usa.
- Desactiva la clave anterior.
Cambia tus claves al menos una vez al año y cada vez que se vaya alguien que tenía acceso a ellas. Si crees que una clave pudo filtrarse, primero desactívala y después investiga.
¿Dudas sobre tu integración? Escribe a [email protected] con tu Merchant ID y el ID del pedido o de la solicitud.