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

Source: https://cryptolike.net/es/guides/accept-crypto-payments-php
Published: 2026-09-25 · Updated: 2026-10-04 · Author: CryptoLike team

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](https://cryptolike.net/es/integrations/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](https://cryptolike.net/docs/auth): 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](https://cryptolike.net/docs/widget). 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](https://cryptolike.net/guides/verify-crypto-payment-webhooks).

## 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](https://cryptolike.net/docs).

## 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](https://cryptolike.net/integrations/sdk-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.
