# Autenticación y firma de solicitudes

> Firma las solicitudes a la API con HMAC-SHA256 usando el UUID de tu proyecto y tu API key.

Toda solicitud a la API (excepto los webhooks entrantes) debe incluir el UUID de tu proyecto y una firma de la solicitud. La firma demuestra que la solicitud proviene de ti y que nadie la modificó en el camino.

## API keys

2328.io utiliza **dos claves** que comparten el mismo algoritmo de firma pero cubren distintos endpoints:

| Clave | Se utiliza para |
|-------|-----------------|
| **API key** | Pagos, monederos estáticos, saldo, tipos de cambio y verificación de webhooks de pago / monedero estático |
| **Payout API key** | Todos los endpoints `/v1/payout/*` y la verificación de webhooks de retiro |

Ambas claves se gestionan en la configuración de tu proyecto en [2328.io](https://2328.io). Los ejemplos siguientes mencionan "API key" de forma genérica — sustitúyela por la correcta según el endpoint que llames.

> **INFO:** **Nunca** mezcles las dos claves: firmar una solicitud de retiro con la API key habitual (o una solicitud de pago con la clave de retiro) devuelve un error de firma.

## Headers requeridos

| Header | Tipo | Requerido | Descripción |
|--------|------|-----------|-------------|
| `Content-Type` | string | sí | Siempre `application/json` |
| `project` | string | sí | UUID de tu proyecto |
| `sign` | string | sí | Firma HMAC-SHA256 de la solicitud, calculada con tu API key |
| `User-Agent` | string | sí | Identifica tu aplicación (p. ej. `MyShop/1.4 (+https://myshop.example)`). Las solicitudes sin `User-Agent` pueden ser bloqueadas. |

## Cómo funciona la firma

Piensa en la firma como una huella del cuerpo de la solicitud. Se construye así:

1. Serializa el cuerpo a JSON (compacto — sin espacios en blanco adicionales).
2. Codifica ese JSON en Base64. Este paso normaliza la entrada entre lenguajes — una vez convertido a ASCII plano, todos los lenguajes producen los mismos bytes para HMAC.
3. Calcula **HMAC-SHA256** sobre la cadena Base64 usando tu API key, y luego convierte el resultado a hexadecimal en minúsculas.

Para **GET** y otros tipos de solicitud sin cuerpo, firma una cadena vacía en lugar del JSON.

> **INFO:** La firma de la cadena vacía es constante para una API key dada. Puedes cachearla si haces muchas llamadas GET.

## Implementaciones

<CodeSnippet name="apiSign" langs="php,js,ts,python,go" />

### Solicitudes sin cuerpo (GET)

Para solicitudes con cuerpo vacío (por ejemplo `GET /v1/payout/status/{uuid}`), firma una cadena vacía:

<CodeSnippet name="apiSignBodyless" langs="curl,php,js,ts,python,go" />

## Ejemplo completo de solicitud

<CodeSnippet name="fullRequestExample" langs="curl,php,js,ts,python,go" />

> **DANGER:** **Nunca expongas tu API key en código del lado del cliente.** Firma las solicitudes en tu backend. Una API key filtrada le da a cualquiera acceso completo a tu cuenta de comerciante.

## Verificación de firmas de webhook

Cuando 2328.io te envía un webhook, el mismo algoritmo se aplica a la inversa:

1. Extrae el campo `sign` del payload.
2. Codifica los campos restantes a JSON (compacto, sin espacios en blanco).
3. Codifica esa cadena en Base64.
4. Calcula `HMAC-SHA256` con la clave correspondiente.
5. Compáralo con el `sign` recibido usando una comparación de **tiempo constante** (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `OpenSSL.fixed_length_secure_compare`).

La clave de firma depende del origen del webhook:

| Webhook | Clave para verificar |
|---------|----------------------|
| Webhooks de pago / monedero estático (`/v1/payment`, `/v1/static-wallet`) | **API key** |
| Webhooks de retiro (`/v1/payout`) | **Payout API key** |

> **WARNING:** **Errores comunes de verificación.** Tu codificador JSON debe producir **exactamente los mismos bytes** que produjo el emisor — de lo contrario el Base64 difiere y la firma no coincidirá.

- **Go**: usa `json.NewEncoder` con `SetEscapeHTML(false)`. El `json.Marshal` por defecto escapa `<`, `>`, `&` a `<` y rompe la firma.
- **Python**: pasa `ensure_ascii=False` a `json.dumps`. Sin esto, los caracteres no ASCII (cirílico, chino, …) se escapan a `\uXXXX`.
- **JSON compacto**: sin espacios en blanco entre campos (`separators=(",", ":")` en Python).
- **Orden de campos** (Go): un `map[string]any` simple aleatoriza las claves al recodificar. Usa `json.RawMessage`, una struct ordenada o elimina `sign` de los bytes originales.

Si la verificación sigue fallando, ejecuta `apiSign` sobre el payload tú mismo — debe producir la misma cadena hexadecimal que el `sign` recibido.

> **INFO:** **Una firma válida no previene reenvíos.** Solo prueba que el webhook vino de 2328.io — no impide que un atacante vuelva a publicar un webhook *capturado* más tarde. Comprueba siempre la idempotencia por `uuid` (o `txid` para wallets estáticos) antes de acreditar fondos. Rechaza con HTTP `401` si la firma falta o es incorrecta.

Los ejemplos de código completos están en **[Webhook Notifications](/docs/webhooks#verifying-the-signature)**. El manejo de reintentos y las reglas de idempotencia están en [Buenas prácticas](/docs/webhooks#best-practices).