Referencia
API REST para verificar pagos Binance Pay C2C. El dinero entra directo a tu propia cuenta de Binance; esta API solo detecta el pago y te avisa (respuesta de estado + webhook).
Base URL: https://apic2c.com/api/v1
Moneda soportada: por ahora solo USDT (el campo currency queda abierto para más adelante, pero cualquier otro valor devuelve currency_invalid).
Lenguajes: la API es REST + JSON puro, así que funciona desde cualquier lenguaje que pueda hacer un request HTTP — PHP, Node.js, Python, C++, Java, Go, Ruby, .NET, lo que uses. Para verificar la firma del webhook (la única parte con algo de código específico) dejamos ejemplos listos en Node.js, PHP, Python y C++.
Cada request lleva el header Authorization: Bearer sk_live_... (o sk_test_...). Generá tu API key desde tu panel.
Máximo 60 requests por minuto por IP y 120 requests por minuto por cuenta. Al superarlo, la respuesta es 429 con código rate_limited. Si tu integración hace polling en vez de usar webhooks, espaciá los requests — no hace falta consultar más seguido que cada pocos segundos.
POST /api/v1/payment-intents
Authorization: Bearer sk_live_xxx
Content-Type: application/json
Idempotency-Key: <opcional, cualquier string único por request>
{
"amount": "25.00",
"currency": "USDT",
"reference": "order_1234",
"metadata": { "customer_email": "buyer@example.com" },
"expires_in_seconds": 900
}Respuesta:
201
{
"id": "pi_...",
"status": "pending",
"currency": "USDT",
"amount_expected": "25.00",
"amount_to_pay": "25.03",
"reference": "order_1234",
"metadata": { "customer_email": "buyer@example.com" },
"binance_pay_id": "123456789",
"expires_at": "2026-08-22T15:30:00.000Z",
"created_at": "2026-08-22T15:15:00.000Z"
}Importante: muéstrale al pagador el monto exacto de amount_to_pay (no amount_expected) y tu Binance Pay ID. El monto puede traer unos centavos extra sobre lo que pediste — es lo único que usamos para identificar tu pago entre todos los que estén pendientes; ese pago te llega completo (con los centavos incluidos) a tu cuenta de Binance, tú decides qué hacer con la diferencia.
metadata es un objeto libre (hasta 64KB) que guardamos y te devolvemos tal cual en cada respuesta y en el webhook — útil para tus propios datos internos (id de orden, cliente, etc.) sin depender de reference.
GET /api/v1/payment-intents/:id
Mismo shape que la creación, más amount_matched, matched_tx_id y matched_at cuando el estado pasa a completed.
POST /api/v1/payment-intents/:id/cancel
200 con el intent en estado cancelled, o 409 not_cancellable si ya no está pendiente.
GET /api/v1/payment-intents?status=pending&reference=order_1234&limit=20&starting_after=pi_...
{ "data": [ /* payment intents */ ], "has_more": false }limit tiene un máximo de 100 (default 20). Paginación con cursor: pasá el id del último resultado como starting_after para la página siguiente.
pending → completed | expired | cancelled. Son terminales: un intent nunca vuelve a pendiente.
Todos los errores tienen el mismo shape:
{ "error": { "type": "invalid_request_error", "code": "amount_invalid", "message": "..." } }| Status | Code | Cuándo pasa |
|---|---|---|
| 400 | amount_invalid | amount falta, no es numérico, o es <= 0. |
| 400 | currency_invalid | currency no está en la lista soportada (ver abajo). |
| 400 | expires_in_seconds_invalid | fuera del rango 60–86400. |
| 400 | reference_invalid | reference no es string, o supera 191 caracteres. |
| 401 | authentication_required | Falta el header Authorization o no tiene el formato Bearer sk_... |
| 401 | invalid_api_key | La API key no existe. |
| 401 | revoked_api_key | La API key fue revocada desde el panel. |
| 403 | tenant_suspended | La cuenta está suspendida. |
| 404 | payment_intent_not_found | No existe ese id para tu cuenta. |
| 409 | binance_not_configured | Todavía no cargaste credenciales de Binance en el panel — no se puede crear el intent. |
| 409 | not_cancellable | El intent ya no está pending (completed/expired/cancelled). |
| 429 | rate_limited | Superaste el límite de requests (ver Rate limiting abajo). |
Cuando un payment intent pasa a completed (o expired), hacemos un POST a la URL configurada en tu panel, con este body:
{
"id": "evt_...",
"type": "payment_intent.completed",
"created": "2026-08-22T15:18:43.000Z",
"data": { "object": { /* mismo shape que GET /api/v1/payment-intents/:id */ } }
}Cada request lleva el header X-C2C-Signature: t=<unix_ts>,v1=<hex_hmac>, firmado con tu webhook secret. Verifícalo siempre antes de confiar en el body — cualquiera puede mandarte un POST a esa URL si no validas la firma.
HMAC_SHA256(secret, "${t}.${rawBody}") sobre el body crudo, sin parsear.v1.t tiene más de 5 minutos de diferencia con la hora actual (anti-replay).Node.js
const crypto = require('crypto')
function verify(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
const t = parseInt(parts.t, 10)
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const a = Buffer.from(parts.v1, 'hex')
const b = Buffer.from(expected, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}PHP
function verify(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {
parse_str(str_replace(',', '&', $header), $parts);
$t = (int)($parts['t'] ?? 0);
if (!$t || abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', "$t.$rawBody", $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: str, tolerance: int = 300) -> bool:
parts = dict(p.split('=') for p in header.split(','))
t = int(parts.get('t', 0))
if not t or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get('v1', ''))C++ (OpenSSL)
#include <openssl/hmac.h>
#include <string>
#include <cstring>
bool verify(const std::string& secret, const std::string& t, const std::string& v1,
const std::string& rawBody) {
std::string signed_payload = t + "." + rawBody;
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLen = 0;
HMAC(EVP_sha256(),
secret.data(), static_cast<int>(secret.size()),
reinterpret_cast<const unsigned char*>(signed_payload.data()), signed_payload.size(),
digest, &digestLen);
char hex[EVP_MAX_MD_SIZE * 2 + 1];
for (unsigned int i = 0; i < digestLen; i++) sprintf(hex + i * 2, "%02x", digest[i]);
hex[digestLen * 2] = '\0';
// Comparar hex == v1 en tiempo constante (y validar "t" con tolerancia de 300s antes de
// llamar a esta función, igual que en los otros lenguajes).
if (std::strlen(hex) != v1.size()) return false;
unsigned char diff = 0;
for (size_t i = 0; i < v1.size(); i++) diff |= hex[i] ^ v1[i];
return diff == 0;
}Si tu endpoint no responde 2xx, reintentamos con backoff: inmediato, +30s, +2m, +10m, +30m, +2h, +6h. Después de 8 intentos (o 24h desde el primero) dejamos de reintentar. Tu endpoint debe responder rápido (idealmente <10s) y ser idempotente — puedes recibir el mismo evento más de una vez si un intento anterior tardó en responder.