Saltar al contenido principal

Enlaces de pago

Un enlace de pago es la unidad de cobro de Binkio: un importe, una moneda y una lista de formas en que tu cliente puede pagarlo. Es lo que hay detrás de un botón «Pagar» en tu tienda, de un cobro por correo o de un QR en el mostrador.

Lo que lo hace distinto de un botón de pago corriente: el pagador puede mandarte una cosa y tú recibir otra. Él transfiere pesos por SPEI y tu cuenta se abona en dólares digitales, o al revés. Eso es lo que aquí llamamos un pago cruzado, y no tienes que hacer nada para habilitarlo — sale de la lista de métodos que aceptes.


Crear un cobro​

curl -X POST https://api.binkio.com/v1/payment_links \
-H "Authorization: Bearer bk_live_sec_…" \
-H "Idempotency-Key: pedido-4821" \
-H "Content-Type: application/json" \
-d '{
"amount": "1500.25",
"currency": "MXNB",
"description": "Pedido 4821",
"accepted_methods": ["SPEI", "MXNB", "USDC"],
"fee_bearer": "payer",
"expires_at": "2026-09-14T23:59:59Z",
"return_url": "https://tutienda.com/pedido/4821/gracias"
}'
CampoObligatorioQué es
amountsíLo que recibe tu cuenta, como cadena decimal. Hasta 8 decimales
currencysíMXNB, USDC o COP — la moneda en la que quedas abonado
descriptionsíLo que ve el pagador. Máximo 255 caracteres
accepted_methodsnoCómo puede pagarte (lista). Sin esto, se habilitan los métodos viables. Ver Si pides métodos concretos
fee_bearernopayer (por defecto) o merchant
expires_atnoISO 8601. Sin esto, el enlace no caduca
return_urlnoA dónde devolver al pagador al terminar. Tiene que ser https:
metadatanoTus pares clave-valor. Te los devolvemos tal cual, aquí y en el webhook

Si pides métodos concretos​

El campo se llama accepted_methods y es una lista con los nombres de la tabla de Métodos de cobro: ["SPEI", "MXNB"]. Cualquier otro nombre de campo (methods, payment_methods…) se rechaza con invalid_request y "property methods should not exist".

Hay dos formas de crear, y se comportan distinto si un método no se puede dejar listo en ese momento (por ejemplo, porque el banco no entregó una CLABE libre):

Cómo lo creasSi un método no queda listo
Sin accepted_methodsSe crea el enlace con los que sí quedaron. Lee accepted_methods en la respuesta para saber cuáles son
Con accepted_methodsNo se crea. Recibes 400 payment_method_unavailable con param: "accepted_methods" y el método en el mensaje. Reintentar suele bastar

Si necesitas SPEI sí o sí, pídelo explícitamente: así nunca te llega un enlace sin él creyendo que lo tiene.

La vuelta a tu tienda​

Con return_url, la página de cobro puede devolver al pagador a donde tú digas cuando termina. Sin ella se queda en la página de Binkio sin camino de vuelta.

Tiene que ser https:. No es formalismo: a esa URL se manda el navegador de tu cliente desde una pantalla de pago, así que un http: degradaría a claro esa navegación y un javascript: sería ejecutar código en el contexto del cobro. Los dos se rechazan con 400.

Ponle lo que necesites para reconocer el pedido —/pedido/4821/gracias— porque la URL se guarda tal cual y te la devolvemos en el detalle del enlace.

:::caution No la uses como confirmación de pago El pagador vuelve a tu tienda cuando él termina, no cuando el dinero está acreditado. Quien confirma es el webhook payment_link.paid, o GET /v1/payment_links/{code} si prefieres preguntar tú. :::

metadata — para no mantener una tabla aparte​

El problema que resuelve: recibes el webhook de un cobro y necesitas saber a qué pedido tuyo corresponde. Sin un sitio donde guardarlo, mantienes tu propia tabla que traduce «código de enlace» → «pedido 4821» — y el día que se te desincroniza, por un enlace creado y no guardado o una fila borrada, te llega el aviso de un cobro real y no sabes de quién es.

Cuélgalo del enlace y te vuelve solo:

curl -X POST https://api.binkio.com/v1/payment_links \
-H "Authorization: Bearer bk_live_sec_…" \
-H "Idempotency-Key: pedido-4821" \
-H "Content-Type: application/json" \
-d '{
"amount": "1500.25",
"currency": "MXNB",
"description": "Pedido 4821",
"metadata": { "order_id": "4821", "sucursal": "centro" }
}'

Te lo devolvemos sin tocar en el detalle del enlace, en el listado, y en los webhooks de payment_link.paid, payment_link.partial_deposit, payment_link.closed y payment_link.claim_reported.

Va en la representación base, no sólo en el detalle: listar 25 cobros y casarlos con tus pedidos es una llamada, no veintiséis.

Los límites, y por qué​

ReglaValor
Claves como máximo50
Longitud de una clave40 caracteres
Longitud de un valor500 caracteres
Tipo de los valorescadena, siempre

Un valor que no sea cadena se rechaza, no se convierte. Si mandas {"order_id": 4821} recibes un 400, no un "4821" silencioso. Convertir por tu cuenta significaría que descubres la conversión leyendo tu propio webhook, con la conciliación ya escrita encima.

Un solo nivel, sin objetos anidados. Un objeto anidado no tiene tope natural de tamaño, y esto viaja en cada respuesta y en cada webhook que te mandamos.

:::danger No metas datos personales Esto acaba en tus registros, en los nuestros, en correos de soporte y en consolas de gente que está depurando. Un identificador de pedido, sí. El correo, el teléfono o la dirección de tu cliente, no — para eso ya tienes tu propia base de datos, y el order_id es la llave para llegar a ella. :::

:::tip {} no es lo mismo que no mandarla Si no mandas metadata, el campo vuelve como null. Si mandas {}, vuelve como {}. La diferencia se conserva a propósito, para que puedas distinguir «nunca puse nada» de «la vacié». :::

amount va como cadena​

Y no es un capricho. JSON.parse convierte 999999999.12345678 en 999999999.1234568: la precisión se pierde antes de que el importe llegue a nosotros. Mandar un número recibe un 400, no una conversión silenciosa — porque un cobro con el importe equivocado se descubre cobrando.

Monedas​

MonedaQué esNotas
MXNBPeso mexicano digital, 1:1 con el pesoEl más común para cobrar en México
USDCDólar digitalPara precios en dólares
COPPeso colombianoNativo 1:1, sin conversión. Sólo admite el método COP

Métodos de cobro​

MétodoEl pagador mandaRiel
SPEIPesos mexicanos, por transferenciaBanca mexicana
WIREDólares, por transferencia internacionalBanca corresponsal
MXNBMXNB en cadenaArbitrum
USDCUSDC en cadenaEthereum
USDTUSDT en cadenaEthereum
COPPesos colombianosRieles de Cobre: llave Bre-B, QR, Nequi, PSE, Bancolombia

La combinación de currency y accepted_methods es lo que decide si hay conversión. Un enlace en MXNB pagado por SPEI es 1:1 y no hay nada que convertir. El mismo enlace pagado en USDC sí: el pagador manda dólares digitales y tú recibes pesos digitales.

:::caution No todos los métodos caben en todos los importes Cada método tiene un mínimo y un máximo, y cuando media una conversión esos límites dependen de la ruta. Un método que no cabe se publica como no disponible con el motivo (BELOW_MIN_THRESHOLD / ABOVE_MAX_THRESHOLD) en vez de fallar cuando el pagador ya está intentando pagar. :::

Pregunta antes de crear​

Pedir un método que el importe no soporta devuelve 400. Para no averiguarlo a base de errores, pregunta con el importe del carrito en la mano:

curl "https://api.binkio.com/v1/payment_links/payment_methods?currency=MXNB&amount=50000.00" \
-H "Authorization: Bearer bk_live_sec_…"
{
"object": "payment_method_options",
"currency": "MXNB",
"amount": "50000.00",
"available_methods": ["MXNB", "SPEI", "USDC", "WIRE", "USDT"],
"conversion_enabled": true,
"methods": [
{ "method": "SPEI", "available": true, "requires_conversion": false },
{ "method": "WIRE", "available": true, "requires_conversion": true }
]
}

No crea ni consulta ningún enlace. conversion_enabled te dice de un vistazo si ese importe alcanza para cobrar también en la otra familia de moneda: con false, sólo puedes cobrar en la moneda del enlace.

Quién paga la comisión​

fee_bearer decide de dónde sale:

  • payer (por defecto) — el pagador manda el importe más la comisión, y tú recibes exactamente lo que pusiste en amount.
  • merchant — el pagador manda amount y la comisión se descuenta de lo que recibes.

Con payer, lo que ingresa tu cuenta es el importe publicado. Ni más ni menos: si la tasa se mueve a favor durante un pago cruzado, ese sobrante no es tuyo ni del pagador — es el margen que cubre el riesgo de la conversión.


El ciclo de vida​

EstadoQué significa
activeEsperando pago. Es el único estado en el que se puede pagar
processingEl dinero llegó y se está acreditando (barrido en cadena o conversión)
paidAcreditado. paid_at y paid_with_method dicen cuándo y cómo
cancelledLo cancelaste tú
expiredSe pasó expires_at sin pagarse

processing no es un estado transitorio de milisegundos. En un pago en cadena hay que esperar confirmaciones, y en uno cruzado hay una conversión de por medio. Enseña «estamos confirmando tu pago», no «pago fallido».

Sólo un enlace active se puede cancelar:

curl -X POST https://api.binkio.com/v1/payment_links/PL-9C2X-4T7B-1KD8/cancel \
-H "Authorization: Bearer bk_live_sec_…" \
-H "Idempotency-Key: cancela-4821"

Cancelar uno ya pagado devuelve 409 payment_link_not_cancellable.

Un enlace ya cancelado o caducado se puede borrar:

curl -X DELETE https://api.binkio.com/v1/payment_links/PL-9C2X-4T7B-1KD8 \
-H "Authorization: Bearer bk_live_sec_…" \
-H "Idempotency-Key: borra-4821"

Responde 204 sin cuerpo. Un enlace active hay que cancelarlo primero, y uno paid no se borra nunca — es el respaldo de un cobro que ocurrió, y seguirá en tus movimientos. Los dos casos devuelven 409 payment_link_not_deletable.


Consultar​

# El listado, del más reciente al más antiguo, paginado por cursor
curl "https://api.binkio.com/v1/payment_links?status=paid&limit=50" \
-H "Authorization: Bearer bk_live_sec_…"

# Uno concreto, con el detalle de cada método
curl https://api.binkio.com/v1/payment_links/PL-9C2X-4T7B-1KD8 \
-H "Authorization: Bearer bk_live_sec_…"

Filtros del listado: status, currency, created_after, created_before, limit y cursor.

Para el resumen —lo que pinta un panel— no hace falta recorrer el histórico:

curl https://api.binkio.com/v1/payment_links/stats \
-H "Authorization: Bearer bk_live_sec_…"
{
"object": "payment_link_stats",
"active": 12,
"processing": 2,
"paid": 340,
"cancelled": 7,
"expired": 23,
"total": 384
}

El detalle añade lo que el listado no lleva: un objeto por cada método aceptado, con su importe a pagar, su cotización y las instrucciones de envío.

{
"object": "payment_link",
"code": "PL-9C2X-4T7B-1KD8",
"status": "active",
"amount": "1500.25000000",
"currency": "MXNB",
"methods": [
{
"object": "payment_link_method",
"method": "SPEI",
"asset": "MXN",
"requires_conversion": false,
"amount_due": "1500.25",
"quote_status": "native",
"instructions": { "clabe": "000000000000000000" }
},
{
"object": "payment_link_method",
"method": "USDC",
"asset": "USDC",
"requires_conversion": true,
"amount_due": "89.42",
"exchange_rate": "17.0234",
"quote_status": "locked",
"amount_due_expires_at": "2026-09-07T10:29:22.481Z",
"instructions": { "address": "0x…", "network": "ethereum" }
}
]
}

quote_status te dice en qué situación está el importe de cada método:

ValorQué significa
nativeSin conversión. El importe no caduca
lockedCon conversión, importe fijado hasta amount_due_expires_at
expiredLa cotización venció. Recárgala antes de enseñarla
calculatingTodavía se está cotizando

:::info La CLABE de SPEI caduca La CLABE de un enlace es un registro de un solo uso con el banco, y caduca (del orden de una semana). Si nadie paga antes:

  • En GET /v1/payment_links/{code} y en el checkout, el método SPEI deja de traer instructions.clabe: no publicamos una CLABE vencida, porque el banco puede habérsela asignado ya a otro cobro.
  • Al abrir checkout_url, la página de pago registra una CLABE nueva para el mismo enlace y el mismo importe. El pago se sigue atribuyendo a tu enlace.

Si montas tu propia pantalla de cobro con la API y el enlace tiene más de unos días, manda al pagador a checkout_url o crea un enlace nuevo. :::

:::warning Una cotización vencida no se refresca sola en el detalle GET /v1/payment_links/{code} es de sólo lectura: si el bloqueo venció te lo dice (quote_status: "expired", sin importe) pero no cotiza de nuevo. Es deliberado — un GET que regenera la cotización puede cambiarle la CLABE al pagador mientras la está copiando, y ese pago llega a una cuenta que ya nadie mira. Para una cotización fresca, manda al pagador a checkout_url. :::


El checkout del pagador​

Tu cliente no tiene —ni puede tener— una credencial nuestra. El código del enlace es su credencial: sirve para un solo cobro, no da acceso a tu cuenta, y quien lo tiene sólo puede pagarte.

Por eso estas dos rutas no piden Authorization:

curl https://api.binkio.com/v1/checkout/PL-9C2X-4T7B-1KD8
curl https://api.binkio.com/v1/checkout/PL-9C2X-4T7B-1KD8/status

Te devuelven el nombre del comercio, el importe, los métodos con sus instrucciones y —esto es lo útil— los datos para enterarte en vivo de que se pagó, sin encuestar:

{
"object": "checkout",
"code": "PL-9C2X-4T7B-1KD8",
"status": "active",
"merchant": { "object": "checkout_merchant", "name": "Tienda Ejemplo" },
"realtime": {
"object": "checkout_realtime",
"transport": "socket.io",
"namespace": "/payment-links",
"join": { "event": "join", "payload": { "code": "PL-9C2X-4T7B-1KD8" } },
"notify_events": ["payment_link:paid", "payment_link:quotes_updated"],
"status_url": "https://api.binkio.com/v1/checkout/PL-9C2X-4T7B-1KD8/status",
"poll_interval_seconds": 5
}
}

Si prefieres no montar el socket, status_url cada poll_interval_seconds hace el mismo trabajo con más latencia.

Usa esto si construyes tu propia pantalla de cobro. Si no, checkout_url ya te da una hecha, con los métodos, el QR y la actualización en vivo resueltos.