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
| Credencial | Ejemplo | ¿Es secreta? |
|---|---|---|
| Key ID | ck_live_... | No. Indica qué clave usas. |
| Secreto de firma | cs_live_... | Sí. Con él firmas. Nunca sale de tu servidor. |
| Merchant ID | M00000001 | No. 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_ycs_live_. Las claves Sandbox empiezan porck_test_ycs_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/accountmuestra"api_access_enabled": true.
#Encabezados
Envía estos cuatro encabezados en cada llamada, incluidas las llamadas GET:
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89X-Key-Ides el ID público de tu clave.X-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 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
Convierte el cuerpo de tu solicitud en bytes una sola vez. Esos mismos bytes son los que firmas y envías. Una llamada
GETtiene el cuerpo vacío.Calcula el hash del cuerpo:
BODY_HASH= SHA-256 del cuerpo, en hexadecimal y en minúsculas. Para un cuerpo vacío ese3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.Une estas cinco líneas con un salto de línea (
\n), sin salto de línea al final:<METHOD> <PATH with query string> <X-Timestamp> <X-Nonce> <BODY_HASH>METHODva en mayúsculas, por ejemploGEToPOST. La ruta incluye la query string exactamente como la envías, por ejemplo/api/v1/skus?limit=50.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.
Signing secret cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefUna llamada GET:
Method GET
Path /api/v1/skus?limit=50
BODY_HASH e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature 58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5Una llamada POST a /api/v1/orders con este cuerpo (106 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.25"}]}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)
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)
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)
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
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í:
{"detail": "Invalid HMAC signature."}| Problema | Qué hacer |
|---|---|
| Key ID desconocido o desactivado | Revisa X-Key-Id y el entorno (Live o Sandbox). |
| 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 dieciséis endpoints. |
| Falta la firma o es incorrecta | Corrige tu código de firma. Pruébalo con los vectores 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. |
| 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) 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 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.
- 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.
- 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.
- 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:
- Crea una clave nueva y guarda su secreto.
- 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.
#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.