# API C2C — documentación para agentes de IA > Verificación de pagos Binance Pay C2C multi-tenant. El dinero entra directo a la cuenta de > Binance del propio comercio; esta API nunca custodia fondos, solo detecta el pago y avisa. > Referencia completa (HTML): https://apic2c.com/docs # API C2C — 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` Auth: header `Authorization: Bearer sk_live_...` (o `sk_test_...`) en cada request. Generá tu key desde el panel de tenant (`/dashboard/api-keys`). Moneda soportada: por ahora solo `USDT` (el campo `currency` queda abierto para más adelante, pero cualquier otro valor devuelve `currency_invalid`). ## 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`. ## Crear un payment intent ``` POST /api/v1/payment-intents Authorization: Bearer sk_live_xxx Content-Type: application/json Idempotency-Key: { "amount": "25.00", "currency": "USDT", "reference": "order_1234", "metadata": { "customer_email": "buyer@example.com" }, "expires_in_seconds": 900 } ``` Respuesta `201`: ```json { "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 se guarda y se devuelve tal cual en cada respuesta y en el webhook — útil para datos internos propios 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`, `matched_at` cuando `status` 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á `pending`. ## Listar ``` GET /api/v1/payment-intents?status=pending&reference=order_1234&limit=20&starting_after=pi_... ``` ```json { "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 `pending`. ## Errores Todos los errores tienen el mismo shape: ```json { "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. | | 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. | | 409 | `not_cancellable` | El intent ya no está `pending`. | | 429 | `rate_limited` | Superaste el límite de requests (ver Rate limiting arriba). | Ver también [webhooks.md](./webhooks.md) para recibir la notificación de pago confirmado sin tener que hacer polling de `GET /api/v1/payment-intents/:id`. --- # Webhooks Cuando un payment intent cambia a `completed` (o `expired`), la API hace un `POST` a la URL de webhook configurada para tu cuenta, con este body: ```json { "id": "evt_...", "type": "payment_intent.completed", "created": "2026-08-22T15:18:43.000Z", "data": { "object": { /* mismo shape que GET /api/v1/payment-intents/:id */ } } } ``` Eventos: `payment_intent.completed`, `payment_intent.expired`. ## Verificar la firma Cada request lleva el header `X-C2C-Signature: t=,v1=`, firmado con tu webhook secret (`whsec_...`, mostrado una sola vez al darte de alta). 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 ```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 ```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 ```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) ```cpp #include #include #include 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(secret.size()), reinterpret_cast(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 el timestamp 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.