1Cómo funciona
- Con el saldo a 0,
/emitirresponde402 SIN_SALDOy no se crea nada. - Si anulas un cupón antes de que se canjee, vuelve a tu saldo. Uno ya canjeado no se puede anular.
- El cupón es al portador: quien tenga el código puede canjearlo. Entrégalo por un canal seguro.
Tu marca en la app
Si lo acordáis con Waycard, tus cupones pueden llevar tu marca: los colores y las imágenes de tu empresa. Cuando alguien canjea uno de tus cupones, la app cambia a tu diseño al momento y la marca queda guardada en su cuenta.
- No tienes que enviar nada: la marca va asociada a tus credenciales y la asigna Waycard.
- Cada cupón guarda la marca que tenías al emitirlo. Si cambia, los ya vendidos siguen con la anterior.
- Las respuestas incluyen el campo
marcacon el identificador de tu marca, onull.
2Credenciales
| Dato | Ejemplo | Para qué |
|---|---|---|
X-Cupon-Key | wck_9f2a… | Identifica tu plataforma. Va en la cabecera de cada petición. |
| Secreto | wcs_4b1c… | Firma las peticiones. Nunca viaja en ninguna petición ni se comparte. |
El secreto se muestra una sola vez. Si lo pierdes, pide una rotación: las credenciales anteriores dejan de funcionar al instante.
Opcionalmente puedes darnos las IPs desde las que llamarás (lista blanca) y una URL de webhook.
3Firma de las peticiones
Todas las peticiones llevan tres cabeceras:
X-Cupon-Key: <key> X-Cupon-Ts: <unix epoch, seconds, UTC> X-Cupon-Sign: HMAC_SHA256(secret, rawBody + X-Cupon-Ts) // hex
rawBodyes exactamente el texto JSON que envías (los mismos bytes). Serializa una vez, firma ese texto y envía ese mismo texto.- En
GETno hay cuerpo: se firma solo el timestamp. - Se admiten ±5 minutos de desvío. Si recibes «X-Cupon-Ts fuera de ventana», revisa el reloj de tu servidor (NTP).
Content-Type: application/jsonen las peticiones POST.
Ejemplos
import crypto from 'crypto';
const BASE = 'https://api.waycard.info/v2/cupon';
const KEY = process.env.WAYCARD_CUPON_KEY;
const SECRET = process.env.WAYCARD_CUPON_SECRET;
async function call(method, path, body) {
const raw = body ? JSON.stringify(body) : '';
const ts = String(Math.floor(Date.now() / 1000));
const sign = crypto.createHmac('sha256', SECRET).update(raw + ts).digest('hex');
const res = await fetch(BASE + path, {
method,
headers: {
'Content-Type': 'application/json',
'X-Cupon-Key': KEY, 'X-Cupon-Ts': ts, 'X-Cupon-Sign': sign,
},
body: body ? raw : undefined,
});
return { status: res.status, ...(await res.json()) };
}
// Issue a coupon after charging order 12345
const r = await call('POST', '/emitir', { origen_ref: 'order-12345' });
if (r.ok) console.log(r.codigo_visible, r.caduca, r.cupo_restante);
else if (r.status === 402) console.log('No balance left: top up with Waycard');function waycardCoupon(string $method, string $path, ?array $body = null): array {
$raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_UNICODE);
$ts = (string)time();
$sign = hash_hmac('sha256', $raw . $ts, getenv('WAYCARD_CUPON_SECRET'));
$ch = curl_init('https://api.waycard.info/v2/cupon' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Cupon-Key: ' . getenv('WAYCARD_CUPON_KEY'),
'X-Cupon-Ts: ' . $ts,
'X-Cupon-Sign: ' . $sign,
],
CURLOPT_POSTFIELDS => $body === null ? null : $raw,
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $code] + (json_decode($res, true) ?: []);
}
$r = waycardCoupon('POST', '/emitir', ['origen_ref' => 'order-12345']);import hmac, hashlib, json, os, time, requests
BASE = 'https://api.waycard.info/v2/cupon'
KEY, SECRET = os.environ['WAYCARD_CUPON_KEY'], os.environ['WAYCARD_CUPON_SECRET']
def call(method, path, body=None, params=None):
raw = json.dumps(body, separators=(',', ':')) if body is not None else ''
ts = str(int(time.time()))
sign = hmac.new(SECRET.encode(), (raw + ts).encode(), hashlib.sha256).hexdigest()
r = requests.request(method, BASE + path, params=params, data=raw or None, timeout=15,
headers={'Content-Type': 'application/json',
'X-Cupon-Key': KEY, 'X-Cupon-Ts': ts, 'X-Cupon-Sign': sign})
return r.status_code, r.json()
status, r = call('POST', '/emitir', {'origen_ref': 'order-12345'})4Endpoints
Llámalo después de haber cobrado a tu cliente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
origen_ref | string ≤ 80 | Sí | Tu identificador del cobro o pedido. Es la clave de idempotencia: repetir la llamada con el mismo valor devuelve el mismo cupón y no descuenta otro. |
producto | string ≤ 20 | No | Por defecto CARD (tarjeta). |
caduca_dias | int 0-3650 | No | Días de validez. 0 = sin caducidad. Por defecto, el acordado con Waycard. |
nota | string ≤ 190 | No | Texto libre para tu referencia (cliente, campaña…). |
{
"ok": true,
"codigo": "WAY9WZJA3K7M2P5R8T",
"codigo_visible": "WAY-9WZJ-A3K7-M2P5-R8T",
"caduca": "2027-09-28",
"estado": "EMITIDO",
"reemitido": false,
"cupo_restante": 41,
"marca": "acme"
}codigoes lo que se guarda (18 caracteres, sin guiones);codigo_visiblees el mismo con guiones, para enseñárselo al cliente. La app acepta los dos.reemitido: truesignifica que eseorigen_refya tenía cupón y se te devuelve el existente.cupo_restante: los cupones que te quedan.marca: la marca que lleva el cupón, onull.
| HTTP | error | Qué hacer |
|---|---|---|
| 400 | FALTA_ORIGEN_REF | Falta origen_ref. |
| 400 | ORIGEN_AJENO | Has enviado un campo origen que no es el tuyo. No hace falta enviarlo. |
| 400 | CAMPO_LARGO | Algún campo es demasiado largo. |
| 401 | — | Revisa las credenciales, la firma sobre el cuerpo crudo y el reloj. |
| 402 | SIN_SALDO | No quedan cupones prepagados. No se ha creado nada: contacta con Waycard para recargar. |
| 429 | — | Más de 120 peticiones por minuto, o demasiadas rechazadas desde tu IP. Espera un minuto. |
Para cuando devuelves el dinero a tu cliente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
origen_ref | string | Sí | Tu identificador del cobro o pedido. |
motivo | string ≤ 160 | No | Motivo de la anulación, para tu referencia. |
{ "ok": true, "anulado": true, "cupo_restante": 42 }- Si no se puede anular:
anulado: falseymotivoNO_EXISTE,YA_CANJEADOoYA_ANULADO. ConYA_CANJEADOno se devuelve saldo: la tarjeta puede estar emitida.
Cuántos cupones te quedan y el resumen de los emitidos.
{
"ok": true,
"origen": "acme",
"prepago": true,
"cupo_restante": 42,
"caduca_dias_defecto": 365,
"marca": "acme",
"cupones": { "total": 58, "sin_usar": 20, "canjeados": 35, "anulados": 3, "caducados": 0 }
}Para saber si se ha canjeado sin depender del webhook.
{
"ok": true, "existe": true, "origen_ref": "order-12345",
"codigo": "WAY9WZJA3K7M2P5R8T", "codigo_visible": "WAY-9WZJ-A3K7-M2P5-R8T",
"estado": "CANJEADO",
"caduca": "2027-09-28",
"canjeado_el": "2026-10-02 18:41:07",
"creado": "2026-09-28 12:00:00",
"marca": "acme"
}estado:EMITIDO,CANJEADO,ANULADOoCADUCADO. Si elorigen_refno es tuyo o no existe:existe: false.
5Webhook de canje
Si nos das una URL https, te avisamos cuando se canjee uno de tus cupones, firmado con tus credenciales:
POST <your webhook>
X-Cupon-Key: <your key>
X-Cupon-Ts: <epoch>
X-Cupon-Sign: HMAC_SHA256(your secret, body + ts)
{ "codigo": "WAY9WZJA3K7M2P5R8T", "canjeado_el": "2026-10-02 18:41:07" }Verifica la firma igual que nosotros verificamos la tuya. Se envía una vez, sin reintentos: si necesitas certeza, consulta /estado.
6Buenas prácticas
- Idempotencia: usa siempre el mismo
origen_refpara el mismo cobro. Ante un timeout, repite la llamada tal cual. - Secreto: en variables de entorno o en un gestor de secretos, nunca en el código ni en el front.
- Saldo: consulta
/saldoantes de una campaña grande y avisa a Waycard con antelación para recargar. - Reloj: mantén NTP en tu servidor; la firma caduca a los 5 minutos.
- Códigos: no los envíes por canales abiertos ni los guardes en logs. Son al portador.
¿Quieres vender tarjetas Waycard?
Escríbenos y te damos de alta, con tus credenciales y, si quieres, tu marca en la app.
Solicitar acceso