Cancelar Link de Pago
Cancela un link de pago para que ya no se pueda pagar. Se pueden cancelar los links en estado pending (incluidos los que ya pasaron su expiresAt) y expired.
Endpoint
PATCH /v1/payment-links/{hashUrl}/cancelParámetros de ruta
| Parámetro | Tipo | Descripción |
|---|---|---|
hashUrl | string | Hash de 24 caracteres del link: el último segmento de paymentLinkUrl. El id del link no sirve aquí |
Autenticación
Requiere el header X-API-Key. Consulta la .
Una API key de la cuenta puede cancelar cualquier link de pago de la cuenta. Una API key de proyecto solo puede cancelar los links de su proyecto (los creados con la API key de ese proyecto); cualquier otro link responde el mismo 404 con ERR_PAYMENT_LINK_NOT_FOUND que un link que no existe, y el link no se modifica. Consulta el .
Ejemplos de solicitud
curl -X PATCH "https://api-pay.zelta.dev/v1/payment-links/Xk3v9Q_aB7mN2pL8rT4wYz1c/cancel" \
-H "X-API-Key: tu-api-key-aqui" \
-H "Content-Type: application/json"Formato de respuesta
La cancelación devuelve una respuesta compacta con los campos esenciales del link:
{
"success": true,
"data": {
"paymentLink": {
"hashUrl": "Xk3v9Q_aB7mN2pL8rT4wYz1c",
"customerName": "Maria Garcia",
"concept": "Pago de servicio mensual",
"amount": 5000,
"status": "cancelled",
"createdAt": "2026-03-13T10:30:00.000Z",
"cancelledAt": "2026-03-13T14:00:00.000Z"
}
},
"message": "Payment link cancelled successfully",
"timestamp": "2026-03-13T14:00:00.000Z"
}Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
hashUrl | string | Hash del link de pago (último segmento de paymentLinkUrl) |
customerName | string | Nombre del cliente |
concept | string | Concepto o descripción del cobro |
amount | integer | Monto en centavos |
status | string | Estado del link. Siempre "cancelled" después de cancelar |
createdAt | string | Fecha de creación original (ISO 8601) |
cancelledAt | string | Fecha y hora de la cancelación (ISO 8601) |
La cancelación no envía ningún webhook.
Códigos de estado
| Código | Descripción |
|---|---|
200 | Link cancelado exitosamente |
400 | El link ya está completed o cancelled (ERR_INVALID_STATE) |
401 | Falta el header X-API-Key |
403 | Cuenta suspendida o proyecto inactivo |
404 | API key desconocida (ERR_ACCOUNT_API_KEY_NOT_FOUND), o el link no existe o la API key no lo alcanza (ERR_PAYMENT_LINK_NOT_FOUND): es de otra cuenta o, con una API key de proyecto, no es de ese proyecto |
409 | Hay un pago del link en confirmación (ERR_PAYMENT_IN_PROGRESS) |
429 | Rate limit excedido (cuerpo en texto plano; espera Retry-After segundos) |
Respuestas de error
Link no encontrado
{
"success": false,
"message": "Payment link not found",
"error": {
"code": "ERR_PAYMENT_LINK_NOT_FOUND"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Estado inválido para cancelación
Se devuelve para un link completed o cancelled. El mensaje indica el estado actual:
{
"success": false,
"message": "Cannot cancel a payment link with status 'completed'",
"error": {
"code": "ERR_INVALID_STATE"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Pago en confirmación
Se devuelve mientras el link está en processing: el pago de un cliente se está confirmando (por ejemplo, un pago que PayPal todavía está revisando, lo que puede tardar varios días). Cancelar en ese momento podría dejar a un cliente cobrado con el link cancelado, así que la solicitud se rechaza.
{
"success": false,
"message": "A payment for this link is being confirmed",
"error": {
"code": "ERR_PAYMENT_IN_PROGRESS"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}No reintentes en un bucle: espera el resultado. El webhook payment.success te indica que el link se pagó (ya no se puede cancelar); si el pago se rechaza o se abandona, el link vuelve a pending y se puede cancelar.
API key faltante
{
"success": false,
"message": "Missing X-API-Key header",
"error": {
"code": "ERR_API_KEY_MISSING"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Reglas de cancelación
Estados que permiten cancelación
| Estado actual | ¿Se puede cancelar? |
|---|---|
pending | Sí (incluido un link que ya pasó su expiresAt y sigue guardado como pending) |
expired | Sí |
processing | No — hay un pago en confirmación (409 ERR_PAYMENT_IN_PROGRESS) |
completed | No — el pago ya fue procesado (400 ERR_INVALID_STATE) |
cancelled | No — ya está cancelado (400 ERR_INVALID_STATE) |
Un link que lleva más de 10 minutos en processing sin ningún pago que todavía pueda completarlo se considera abandonado y sí se puede cancelar.
La cancelación es atómica: si el pago de un cliente empieza en el mismo instante, el pago tiene prioridad y la cancelación devuelve 409.
Los links completados no se pueden cancelar
Los links con estado completed no pueden cancelarse porque el pago ya fue procesado. Cancelar un link no reembolsa ningún pago.
Ejemplos de implementación
Cancelación básica con manejo de errores
async function cancelPaymentLink(hashUrl) {
try {
const response = await fetch(
`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}/cancel`,
{
method: 'PATCH',
headers: {
'X-API-Key': process.env.ZELTA_API_KEY,
'Content-Type': 'application/json'
}
}
);
if (response.status === 429) {
return { success: false, error: { code: 'RATE_LIMITED', retryAfter: response.headers.get('Retry-After') } };
}
const result = await response.json();
if (!result.success) {
console.error('Error:', result.error.code, result.message);
return { success: false, error: { code: result.error.code, message: result.message } };
}
return { success: true, paymentLink: result.data.paymentLink };
} catch (error) {
console.error('Error de red:', error.message);
return { success: false, error: { code: 'NETWORK_ERROR', message: error.message } };
}
}Cancelación con manejo de cada estado
async function safeCancelPaymentLink(hashUrl) {
const result = await cancelPaymentLink(hashUrl);
if (result.success) {
return { success: true, paymentLink: result.paymentLink };
}
switch (result.error.code) {
case 'ERR_INVALID_STATE':
// Ya está completed o cancelled: consulta el estado para saber cuál
return { success: false, error: 'El link ya está pagado o cancelado' };
case 'ERR_PAYMENT_IN_PROGRESS':
// Hay un pago en confirmación: espera el webhook o vuelve a intentarlo más tarde
return { success: false, error: 'El link tiene un pago en confirmación', retryLater: true };
case 'ERR_PAYMENT_LINK_NOT_FOUND':
return { success: false, error: 'Link de pago no encontrado' };
default:
return { success: false, error: result.error.message ?? result.error.code };
}
}Casos de uso
Cancelación de orden
Cuando un cliente cancela una orden, cancela también el link de pago asociado:
async function cancelOrder(orderId, paymentLinkHashUrl) {
const result = await safeCancelPaymentLink(paymentLinkHashUrl);
if (result.success) {
console.log(`Link de pago cancelado para la orden ${orderId}`);
// Actualizar el estado de la orden en tu sistema
} else {
console.error(`No se pudo cancelar el link: ${result.error}`);
}
}Limpieza de links vencidos
Cancela los links vencidos que quedaron sin pagar. Recorre tus links sin filtro de status (un link vencido puede seguir apareciendo como pending) y obtén el hashUrl del final de paymentLinkUrl, porque la lista no trae ese campo:
async function cleanupExpiredLinks() {
const links = await getAllPaymentLinks(); // ver "Listar Links de Pago"
const now = new Date();
const expired = links.filter(l =>
l.status === 'expired' || (l.status === 'pending' && new Date(l.expiresAt) <= now)
);
let cancelled = 0;
for (const link of expired) {
const hashUrl = new URL(link.paymentLinkUrl).pathname.split('/').pop();
const result = await cancelPaymentLink(hashUrl);
if (result.success) {
cancelled++;
}
// Respetar el rate limit (60 solicitudes por ventana de 60 segundos)
await new Promise(resolve => setTimeout(resolve, 1100));
}
console.log(`${cancelled} links vencidos cancelados`);
}Cancelación por solicitud del cliente
app.post('/ordenes/:orderId/cancelar', async (req, res) => {
const order = await getOrder(req.params.orderId);
if (!order || !order.paymentLinkHashUrl) {
return res.status(404).json({ error: 'Orden no encontrada' });
}
const result = await safeCancelPaymentLink(order.paymentLinkHashUrl);
if (result.success) {
await updateOrderStatus(order.id, 'cancelled');
res.json({
message: 'Orden y link de pago cancelados exitosamente',
cancelledAt: result.paymentLink.cancelledAt
});
} else {
res.status(result.retryLater ? 409 : 400).json({ error: result.error });
}
});Buenas prácticas
- Maneja cada código:
ERR_INVALID_STATE(ya pagado o cancelado),ERR_PAYMENT_IN_PROGRESS(pago en confirmación) yERR_PAYMENT_LINK_NOT_FOUNDrequieren respuestas distintas en tu sistema. - Guarda el
hashUrl: al crear el link, guarda el último segmento depaymentLinkUrljunto a tu orden; lo necesitas para cancelar. - Registra las cancelaciones: guarda un log con el
hashUrl, la fecha y el motivo para tener trazabilidad. - Rate limiting en lote: si necesitas cancelar muchos links, espacia las solicitudes para no exceder las 60 por ventana de 60 segundos.
Siguientes pasos
- — Crea un nuevo link de pago
- — Consulta el estado de un link
- — Obtén todos tus links
- — Recibe notificaciones de pago