Desarrolladores/Primeros pasos

Autenticación

Cada llamada a la API lleva tu key ID y una firma. La firma demuestra que la llamada viene de ti, y nadie puede modificarla ni repetirla. Tu secreto de firma nunca se envía.

Ver también: Catálogo y pedidos · Seguridad · README

#Credenciales

CredencialEjemplo¿Es secreta?
Key IDck_live_...No. Indica qué clave usas.
Secreto de firmacs_live_...Sí. Con él firmas. Nunca sale de tu servidor.
Merchant IDM00000001No. Es opcional en las llamadas a la API.
  • Un Owner crea las claves en el Portal (ver Claves de API).
  • El Portal muestra el secreto de firma una sola vez, al crear la clave. Nadie puede volver a mostrarlo, ni siquiera CardV.
  • Las claves Live empiezan por ck_live_ y cs_live_. Las claves Sandbox empiezan por ck_test_ y cs_test_. 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/account muestra "api_access_enabled": true.

#Encabezados

Envía estos cuatro encabezados en cada llamada, incluidas las llamadas GET:

HTTP
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89
  • X-Key-Id es el ID público de tu clave.
  • X-Timestamp es la hora actual en formato Unix, en segundos. No puede diferir más de 5 minutos (300 segundos) del reloj de CardV.
  • X-Nonce es un valor aleatorio que nunca repites. Usa 32 caracteres hexadecimales aleatorios.
  • X-Signature se explica en Cómo firmar una solicitud. Debe ir en hexadecimal y en minúsculas.

También puedes enviar X-Merchant-Id. Si lo haces, debe coincidir con la clave.

#Cómo firmar una solicitud

  1. Convierte el cuerpo de tu solicitud en bytes una sola vez. Esos mismos bytes son los que firmas y envías. Una llamada GET tiene el cuerpo vacío.

  2. Calcula el hash del cuerpo: BODY_HASH = SHA-256 del cuerpo, en hexadecimal y en minúsculas. Para un cuerpo vacío es e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

  3. Une estas cinco líneas con un salto de línea (\n), sin salto de línea al final:

    Text
    <METHOD>
    <PATH with query string>
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>

    METHOD va en mayúsculas, por ejemplo GET o POST. La ruta incluye la query string exactamente como la envías, por ejemplo /api/v1/skus?limit=50.

  4. Fírmalo: X-Signature = HMAC-SHA256 de ese texto, usando tu secreto de firma como clave. Escribe el resultado en hexadecimal y en minúsculas.

Las llamadas de recargas de celular se firman igual, con su propia ruta, por ejemplo /api/v1/recharge/orders.

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 ni reordene la query string después de firmarlos.

Vectores de prueba. Comprueba tu código con estos valores. El secreto es falso.

Text
Signing secret  cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp     1790000000
X-Nonce         0123456789abcdef0123456789abcdef

Una llamada GET:

Text
Method       GET
Path         /api/v1/skus?limit=50
BODY_HASH    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature  58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5

Una llamada POST a /api/v1/orders con este cuerpo (106 bytes, en una sola línea, sin salto de línea al final):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}
Text
BODY_HASH    5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature  e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89

#Ejemplos de código

Todos los ejemplos dan los resultados de los vectores de prueba. Carga el secreto desde tu gestor de secretos, no lo pongas en el código.

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.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"

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. Para una llamada GET, firma GET, la ruta con su query string y el hash del cuerpo vacío, y no envíes cuerpo.

Python (requests)

Python
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 integrado)

Node.js
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 (solo la firma)

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

#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í:

JSON
{"detail": "Invalid HMAC signature."}
ProblemaQué hacer
Key ID desconocido o desactivadoRevisa X-Key-Id y el entorno (Live o Sandbox).
El acceso a la API no está activadoEspera a que CardV apruebe tu empresa.
La IP de tu servidor no está permitidaAgrégala a la lista de IPs permitidas en el Portal.
El endpoint solo funciona en el PortalUsa el Portal. Con una clave solo funcionan dieciséis endpoints.
Falta la firma o es incorrectaCorrige tu código de firma. Pruébalo con los vectores de prueba.
La marca de tiempo es demasiado antigua o futuraSincroniza el reloj de tu servidor (NTP).
El nonce ya se usóUsa un nonce aleatorio nuevo en cada solicitud.
Demasiadas solicitudes (429)Espera los segundos que indica Retry-After y vuelve a intentar.

Importante:

  • Cuando reenvíes una solicitud, 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) o 2001: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 puede usar exactamente los dieciséis endpoints de La API en resumen. Eso incluye hacer pedidos y leer códigos, así que cuida el secreto de firma 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.

  1. Crea una clave y ponle un nombre. El Portal muestra el key ID y el secreto de firma. Copia ahora mismo el secreto en tu gestor de secretos. Solo se muestra esta vez.
  2. Regenera el secreto si lo pierdes. El key ID no cambia. El secreto anterior deja de funcionar al instante, así que instala el nuevo en todos tus servidores de inmediato.
  3. Desactiva una clave cuando ya no la necesites. El cambio es inmediato y definitivo. Después puedes quitar la clave desactivada de la lista. CardV conserva su historial.

Para cambiar de clave sin interrumpir el servicio:

  1. Crea una clave nueva y guarda su secreto.
  2. Instálala en todos tus servidores.
  3. Comprueba en el Portal que la clave anterior ya no se usa.
  4. 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.

#Claves antiguas

Las claves creadas antes de octubre de 2026 empiezan por cvb2b_ y usan los encabezados antiguos X-Merchant-Id y X-Api-Key, con firma solo en las llamadas POST. La clave antigua viaja en cada solicitud, así que es más fácil que se filtre.

Live y Sandbox ya no aceptan estas claves. Cámbiate ya a una clave nueva: crea una en el Portal, adapta tu código a los encabezados de arriba y luego desactiva la clave antigua.

¿Dudas sobre tu integración? Escribe a [email protected] con tu Merchant ID y el ID del pedido o de la solicitud.