USDT
API C2C
Cómo funcionaDocumentaciónIniciar sesión
Crear cuenta
AutenticaciónRate limitingCrear un payment intentConsultar estadoCancelarListarEstados posiblesErroresWebhooksReintentos

Referencia

API C2C

o abrir /llms.txt directo

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++.

Autenticación

Cada request lleva el header Authorization: Bearer sk_live_... (o sk_test_...). Generá tu API key desde tu panel.

Rate limiting

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.

Crear un payment intent

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.

Consultar estado

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.

Cancelar

POST /api/v1/payment-intents/:id/cancel

200 con el intent en estado cancelled, o 409 not_cancellable si ya no está pendiente.

Listar

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.

Estados posibles

pending → completed | expired | cancelled. Son terminales: un intent nunca vuelve a pendiente.

Errores

Todos los errores tienen el mismo shape:

{ "error": { "type": "invalid_request_error", "code": "amount_invalid", "message": "..." } }
StatusCodeCuándo pasa
400amount_invalidamount falta, no es numérico, o es <= 0.
400currency_invalidcurrency no está en la lista soportada (ver abajo).
400expires_in_seconds_invalidfuera del rango 60–86400.
400reference_invalidreference no es string, o supera 191 caracteres.
401authentication_requiredFalta el header Authorization o no tiene el formato Bearer sk_...
401invalid_api_keyLa API key no existe.
401revoked_api_keyLa API key fue revocada desde el panel.
403tenant_suspendedLa cuenta está suspendida.
404payment_intent_not_foundNo existe ese id para tu cuenta.
409binance_not_configuredTodavía no cargaste credenciales de Binance en el panel — no se puede crear el intent.
409not_cancellableEl intent ya no está pending (completed/expired/cancelled).
429rate_limitedSuperaste el límite de requests (ver Rate limiting abajo).

Webhooks

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 */ } }
}

Verificar la firma

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.

  • Recalcula HMAC_SHA256(secret, "${t}.${rawBody}") sobre el body crudo, sin parsear.
  • Compáralo en tiempo constante contra v1.
  • Rechaza si 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;
}

Reintentos

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.