Ir al contenido

Para desarrolladoresGuía

Recibir pagos con criptomonedas en PHP

Recibe Bitcoin y USDT en un sitio PHP: firma la solicitud a la API con hash_hmac, crea un pago, envía al cliente a la página de pago y verifica el webhook.

Un sitio en PHP recibe cripto con CryptoLike con tres piezas de código: una función que firma las solicitudes a la API con hash_hmac, una llamada que crea el pago de un pedido y un manejador de webhooks que marca el pedido como pagado. No hace falta ninguna extensión aparte de cURL, y el mismo código corre en PHP 7.4 y 8.x, en Laravel, Symfony o PHP puro.

Esta guía es para un sitio o una aplicación propios. Una tienda en WooCommerce, OpenCart, PrestaShop o Magento 2 no necesita código: instala el plugin de WooCommerce o el módulo de tu plataforma.

Qué necesitas

Una cuenta de CryptoLike, un servidor PHP que pueda hacer solicitudes HTTPS salientes y una URL pública https:// para el webhook. Guarda el secreto de la API y el secreto del webhook en variables de entorno: los ejemplos leen CRYPTOLIKE_API_KEY_ID, CRYPTOLIKE_API_SECRET y CRYPTOLIKE_WEBHOOK_SECRET, además de CRYPTOLIKE_API_URL para la URL de la API de prueba (si no está definida, se usa producción). Prueba todo primero con claves de prueba y redes de prueba, así no se mueve dinero real.

1. Crea una clave API de prueba

En el panel abre Desarrolladores → Claves API → Crear clave y dale a la clave los permisos create_invoice y read. Las claves que empiezan con ck_test_ funcionan con redes de prueba. Las claves de prueba se crean en el entorno de prueba: el soporte de CryptoLike te da acceso a su panel, donde creas la misma clave, y la URL de su API (CRYPTOLIKE_API_URL en los ejemplos). El panel de producción emite solo claves reales, y en la red principal una clave de prueba se rechaza con 401 auth.test_key; cambia a una clave real solo cuando el flujo funcione de punta a punta. El secreto se muestra una sola vez: ponlo directo en el entorno de tu servidor, nunca en un repositorio ni en una página.

2. Firma las solicitudes a la API con hash_hmac

Cada solicitud lleva tres encabezados: el ID de la clave, la hora Unix en segundos y una firma HMAC-SHA256 de cuatro líneas: la marca de tiempo, el método, la ruta con su query y el SHA-256 del cuerpo. Esta función lo hace con cURL:

php
function cryptolike(string $method, string $path, ?array $payload = null, ?string $idempotencyKey = null): array
{
    $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES);
    $ts = (string) time();
    $canonical = implode("\n", [$ts, strtoupper($method), $path, hash('sha256', $body)]);
    $headers = [
        'X-API-Key: ' . getenv('CRYPTOLIKE_API_KEY_ID'),
        'X-Timestamp: ' . $ts,
        'X-Signature: ' . hash_hmac('sha256', $canonical, getenv('CRYPTOLIKE_API_SECRET')),
        'Content-Type: application/json',
    ];
    if ($idempotencyKey !== null) {
        $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
    }
    $ch = curl_init((getenv('CRYPTOLIKE_API_URL') ?: 'https://api.cryptolike.net') . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $body === '' ? null : $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $data = json_decode((string) $response, true) ?? [];
    if ($status >= 400) {
        throw new RuntimeException(($data['error']['code'] ?? 'http_' . $status) . ': ' . ($data['error']['message'] ?? ''));
    }
    return $data;
}

El cuerpo del que calculas el hash debe ser exactamente la cadena que envías; por eso la función lo codifica una sola vez. Antes de la primera llamada real, compara tu firma con el vector de referencia de autenticación de la API: un 401 auth.invalid_signature casi siempre significa otra cadena de cuerpo o la hora en milisegundos.

3. Crea el pago de un pedido

Crea un pago por pedido desde el servidor. Ponle precio en tu moneda y deja que el cliente elija la moneda cripto, o pasa un coin_id de GET /v1/currencies para fijarla. Sin coin_id, la página de pago muestra las monedas disponibles para facturas, cada una con un monto aproximado marcado con ≈; para ofrecer solo algunas en este pedido, agrega allowed_coin_ids con los id que tienen invoice_enabled: true en GET /v1/currencies firmado. Con el ID del pedido como clave de idempotencia, un doble clic o un reintento devuelve el mismo pago:

php
$invoice = cryptolike('POST', '/v1/invoices', [
    'amount' => '49.90',
    'currency' => 'USD',
    'order_id' => $order->id,
    'idempotency_by_order_id' => true,
    'return_url' => 'https://example.com/orders/' . $order->id,
], 'order-' . $order->id);

$order->saveInvoiceId($invoice['id']);

Los montos son cadenas decimales, nunca números de punto flotante: '49.90', no 49.9. La respuesta trae id, status (new) y checkout_url.

4. Envía al cliente a la página de pago

Redirige a checkout_url: la página muestra el monto, la red, la dirección con código QR, el tiempo restante y el estado en vivo, y después del pago devuelve al cliente a return_url. Para que el cliente se quede en tu sitio, incrusta la misma página con el widget de pago. Tu página solo recibe el ID de la factura; el secreto de la API se queda en el servidor.

php
header('Location: ' . $invoice['checkout_url'], true, 303);
exit;

5. Verifica el webhook y marca el pedido como pagado

Agrega un endpoint en el panel, en Desarrolladores → Webhooks, con los eventos invoice.* y guarda su secreto. El manejador verifica la firma sobre el cuerpo sin procesar, rechaza marcas de tiempo antiguas, omite los eventos que ya procesó y responde 200 rápido:

php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = (string) getenv('CRYPTOLIKE_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);

if ($secret === '' || !ctype_digit($ts) || abs(time() - (int) $ts) > 300 || !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
if (!alreadyProcessed($event['event_id'])) {
    if ($event['type'] === 'invoice.paid') {
        markOrderPaid($event['data']['invoice']['order_id']);
    }
    rememberEvent($event['event_id']);
}
http_response_code(200);

Lee el cuerpo con php://input, no con $_POST, y no lo decodifiques ni lo vuelvas a codificar antes de la verificación. Cada evento se entrega al menos una vez y puede llegar en desorden: confía en el estado dentro de data, no en el orden de llegada. Más detalles y el vector de prueba: cómo verificar los webhooks de pagos cripto.

6. Prueba en una red de prueba y pasa a producción

Crea un pago con la clave de prueba, págalo con monedas de prueba en la red que indica la página y comprueba que llegan invoice.pending y después invoice.paid. Luego maneja los demás resultados: invoice.underpaid (el cliente puede completar el pago a la misma dirección hasta el vencimiento), invoice.expired (ofrece un pago nuevo) e invoice.reverted (detén la entrega del pedido). Reemplaza la clave de prueba por una real y recorre la lista de verificación para producción de la guía para desarrolladores.

Errores comunes y cómo corregirlos

Error Qué pasó Qué hacer
401 auth.invalid_signature La cadena firmada no coincide con la solicitud Calcula el hash del cuerpo exacto que envías; firma la ruta con su query; usa segundos, no milisegundos
401 auth.expired_timestamp El reloj del servidor tiene un desfase de más de 5 minutos Sincroniza el reloj con NTP
403 auth.permission_denied A la clave le falta un permiso Agrega create_invoice o read a la clave en el panel
409 conflict El pedido ya tiene un pago por otro monto o ya recibió dinero Lee details.invoice_id; no crees un segundo pago
422 idempotency.mismatch El mismo Idempotency-Key llegó con otro cuerpo Usa una clave por pedido y por solicitud
422 no_allowed_coins Ninguna moneda de allowed_coin_ids está disponible para facturas en esta divisa Envía otros id o quita el campo para ofrecer todas las monedas

¿Prefieres un SDK?

El SDK oficial de PHP envuelve las mismas llamadas para PHP 7.4 – 8.x sin dependencias en tiempo de ejecución: solicitudes firmadas, reintentos ante 429 y 5xx, una factura abierta por pedido y Webhook::verify(). El código de esta guía es lo que el SDK hace por dentro, así que puedes empezar con uno u otro.

Preguntas frecuentes

¿Necesito Composer o una librería para recibir cripto en PHP?

No. Para firmar una solicitud a CryptoLike bastan hash() y hash_hmac() de la biblioteca estándar de PHP, y las solicitudes pasan por cURL. La función del paso 2 alcanza para toda la integración. El SDK oficial de PHP agrega reintentos, paginación y utilidades para tiendas, y también funciona sin Composer: copias sus archivos en tu proyecto.

¿Puedo mostrar la página de pago en mi propio sitio?

Sí. Crea el pago en tu servidor como en el paso 3 y, en lugar de redirigir, incrusta la misma página de pago con el widget de pago. Antes agrega la dirección de tu sitio a los orígenes permitidos: la tarjeta Orígenes permitidos en la página Configuración del panel. El widget solo lee datos públicos del pago, así que el secreto de la API nunca llega al navegador.

¿Cómo sé a qué pedido corresponde un webhook?

El objeto invoice del webhook trae el order_id que enviaste al crear el pago, y metadata si lo enviaste. Busca el pedido por ese valor, revisa el estado dentro de data y guarda el event_id: así una entrega repetida del mismo evento se confirma sin modificar el pedido dos veces.

Términos usados aquí

Sigue leyendo

Empieza con un pago de prueba

Crea una cuenta, pide a soporte el entorno de prueba y mira cómo un pago pasa de pending a paid en una red de prueba.