---
title: "Autenticación"
description: "En esta guía veremos cómo funciona la autenticación. La API de Fystack usa autenticación basada en HMAC para verificar de forma segura tus solicitudes a la API."
url: https://docs.fystack.io/es/authentication
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# Autenticación

Necesitarás autenticar tus solicitudes para acceder a cualquiera de los endpoints de la API de Fystack. En esta guía veremos cómo funciona la autenticación. Fystack usa autenticación basada en HMAC para verificar de forma segura la autenticidad de tus solicitudes a la API.

## Autenticación HMAC

La API de Fystack usa un sistema de autenticación seguro basado en HMAC que requiere que firmes tus solicitudes con tu API secret. Cada solicitud necesita tres headers de autenticación:

```bash
ACCESS-API-KEY: your_api_key
ACCESS-TIMESTAMP: current_unix_timestamp
ACCESS-SIGN: base64_encoded_hmac_signature
```

La firma HMAC se genera combinando el método HTTP, la ruta de la solicitud, el timestamp y el cuerpo de la solicitud (si corresponde), y luego firmando el resultado con tu API secret.

## Ejemplo de solicitud con headers de autenticación

Este es un ejemplo de cómo hacer una solicitud autenticada a la API de Fystack:

```bash
curl https://api.fystack.io/api/v1/wallets \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Calcular el ACCESS-SIGN

Para calcular la firma HMAC, debes:

1. Crear un string canónico a partir de los parámetros de la solicitud en un orden fijo: `method`, `path`, `timestamp`, `body`
2. Firmar este string con tu API secret usando HMAC-SHA256
3. Codificar en hexadecimal el digest HMAC y luego codificar en base64 el string hexadecimal

Mantén siempre seguro tu API secret y restablécelo si sospechas que se ha visto comprometido. Así se calcula la firma en cada lenguaje:

```typescript
import CryptoJS from 'crypto-js'

function computeSignature(
  apiSecret: string,
  method: string,
  path: string,
  timestamp: string,
  body: string = ''
): string {
  // Build canonical string in exact order
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`

  // HMAC-SHA256 -> hex -> base64
  const hexDigest = CryptoJS.HmacSHA256(canonical, apiSecret)
    .toString(CryptoJS.enc.Hex)

  return btoa(hexDigest)
}

// Example usage
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = computeSignature(
  'your_api_secret', 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
)

const headers = {
  'ACCESS-API-KEY': 'your_api_key',
  'ACCESS-TIMESTAMP': timestamp,
  'ACCESS-SIGN': signature
}
```

```go
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/base64"
  "encoding/hex"
  "fmt"
  "strconv"
  "time"
)

func computeSignature(apiSecret string, method, path, timestamp, body string) string {
  // Build canonical string in fixed order
  message := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s",
    method, path, timestamp, body)

  // Create HMAC-SHA256
  h := hmac.New(sha256.New, []byte(apiSecret))
  h.Write([]byte(message))
  hexSig := hex.EncodeToString(h.Sum(nil))

  // Base64-encode the hex string
  return base64.StdEncoding.EncodeToString([]byte(hexSig))
}

// Usage
ts := strconv.FormatInt(time.Now().Unix(), 10)
signature := computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  ts, "",
)
```

```php
<?php
function computeSignature($apiSecret, $method, $path, $timestamp, $body = '') {
  // Build canonical string in fixed order
  $message = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  // Create HMAC-SHA256 hex digest
  $hexSig = hash_hmac('sha256', $message, $apiSecret);

  // Base64-encode the hex string
  return base64_encode($hexSig);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $apiSecret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp, ''
);

$headers = [
  'ACCESS-API-KEY: ' . $apiKey,
  'ACCESS-TIMESTAMP: ' . $timestamp,
  'ACCESS-SIGN: ' . $signature
];
```

```python
import hmac
import hashlib
import time
import base64

def compute_signature(api_secret: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  message = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  # Create HMAC-SHA256 hex digest
  hex_sig = hmac.new(
    api_secret.encode(),
    message.encode(),
    hashlib.sha256
  ).hexdigest()

  # Base64-encode the hex string
  return base64.b64encode(hex_sig.encode()).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  api_secret, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp, ''
)

headers = {
  'ACCESS-API-KEY': api_key,
  'ACCESS-TIMESTAMP': timestamp,
  'ACCESS-SIGN': signature
}
```

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

public static String computeSignature(String apiSecret, String method, String path, String timestamp, String body) throws Exception {
  // Build canonical string in fixed order
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  // Create HMAC-SHA256
  Mac mac = Mac.getInstance("HmacSHA256");
  mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
  byte[] digest = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));

  // Hex-encode the digest
  StringBuilder hexDigest = new StringBuilder();
  for (byte b : digest) {
    hexDigest.append(String.format("%02x", b));
  }

  // Base64-encode the hex string
  return Base64.getEncoder().encodeToString(hexDigest.toString().getBytes(StandardCharsets.UTF_8));
}

// Usage
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String signature = computeSignature(
  apiSecret, "GET",
  "/api/v1/workspaces/your_workspace_id/wallets",
  timestamp, ""
);

Map<String, String> headers = Map.of(
  "ACCESS-API-KEY", apiKey,
  "ACCESS-TIMESTAMP", timestamp,
  "ACCESS-SIGN", signature
);
```

### Proceso de firma de solicitudes

1. Construye el string canónico en un orden fijo: `method={METHOD}&path={PATH}&timestamp={TIMESTAMP}&body={BODY}`
   - `method`: método HTTP en mayúsculas (GET, POST, etc.)
   - `path`: ruta de la solicitud (por ejemplo, `/api/v1/workspaces/{id}/wallets`)
   - `timestamp`: timestamp UNIX en segundos
   - `body`: cuerpo de la solicitud serializado como JSON, o un string vacío para las solicitudes GET

2. Crea la firma HMAC-SHA256:
   - Firma el string canónico con tu API secret
   - Codifica el digest en hexadecimal
   - Codifica en base64 el string hexadecimal

3. Agrega los headers requeridos:
   - `ACCESS-API-KEY`: tu API key
   - `ACCESS-TIMESTAMP`: el timestamp usado en la firma
   - `ACCESS-SIGN`: la firma codificada en base64

> **Warning.** Mantén seguro tu API secret y nunca lo compartas. La API key se puede compartir, pero el secret debe permanecer privado.

## Construir el string del cuerpo

`body` en el string canónico debe ser **exactamente los mismos bytes que transmites**, tanto para HMAC como para Ed25519. Para una solicitud `GET`, o cualquier solicitud sin cuerpo, es un string vacío, nunca `"{}"` ni `null`.

El error que debes evitar: serializar el payload dos veces, una para firmar y otra cuando tu cliente HTTP lo envía. Si lo que firmaste no coincide byte a byte con lo que recibió el servidor, `ValidateAccessSign` falla, aunque el payload sea lógicamente el mismo JSON. Serializa una sola vez y luego reutiliza exactamente ese string tanto para construir el string canónico como para el cuerpo literal de la solicitud; no le pases a tu cliente HTTP el objeto/dict/map original confiando en que una segunda serialización independiente lo reproduzca exactamente. Según el lenguaje, esto falla de forma visible (algunos clientes HTTP rechazan o manejan mal un cuerpo que no es string) o falla de forma silenciosa como una discrepancia de firma difícil de depurar (si las dos serializaciones llegan a diferir, por una actualización de biblioteca, una opción predeterminada distinta o valores inusuales como `NaN`), así que no dependas de ninguno de los dos resultados: simplemente no serialices dos veces.

```javascript
const payload = { name: 'my-integration' }
const body = JSON.stringify(payload) // serialize once

// sign() uses `body` in the canonical string, exactly as below
const signature = computeSignature(apiSecret, 'POST', path, timestamp, body)

await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', /* ...auth headers */ },
  // `fetch` requires body to already be a string (or Blob/ArrayBuffer/etc),
  // it will not JSON-serialize `payload` for you, pass `body`, not `payload`
  body
})
```

```go
payload := map[string]string{"name": "my-integration"}
bodyBytes, _ := json.Marshal(payload) // serialize once
body := string(bodyBytes)

signature := computeSignature(apiSecret, "POST", path, timestamp, body)

req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) // same bytes
```

```python
import json
import requests

payload = {'name': 'my-integration'}
body = json.dumps(payload)  # serialize once

signature = compute_signature(api_secret, 'POST', path, timestamp, body)

# Pass data=body (the exact string you signed). Passing json=payload
# instead would have requests() serialize the dict itself, a second,
# independent call that isn't guaranteed to match what you signed
requests.post(url, data=body, headers={'Content-Type': 'application/json', **auth_headers})
```

```php
<?php
$payload = ['name' => 'my-integration'];
$body = json_encode($payload); // serialize once

$signature = computeSignature($apiSecret, 'POST', $path, $timestamp, $body);

curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // same string
```

```java
String body = objectMapper.writeValueAsString(payload); // serialize once

String signature = computeSignature(apiSecret, "POST", path, timestamp, body);

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create(url))
  .POST(HttpRequest.BodyPublishers.ofString(body)) // same string
  .build();
```

## Firma con Ed25519

Si tu API key usa un [par de claves de cliente](https://docs.fystack.io/api-keys.md#client-key-pair) en lugar de un secret HMAC, firma el mismo string canónico con tu clave privada Ed25519 en lugar de calcular un HMAC. Hay dos diferencias respecto del flujo HMAC:

- Firma directamente los bytes del string canónico; no hay un paso de hashing separado, Ed25519 lo hace internamente.
- Codifica en base64 directamente la firma raw de 64 bytes; no hay paso hexadecimal.

```
ACCESS-SIGN = base64( Ed25519_Sign(privateKey, canonical_string) )
```

Si usas `@fystack/sdk`, pasa un `signer` (`LocalPrivateKeySigner` o `AwsKmsSigner`) en `credentials` y el SDK lo hará por ti; consulta [API Keys](https://docs.fystack.io/api-keys.md#client-key-pair). Los ejemplos siguientes son para firmar sin el SDK, en otros lenguajes.

```javascript
const crypto = require('crypto');

function computeSignature(privateKeyPem, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`;

  const privateKey = crypto.createPrivateKey(privateKeyPem);
  // No digest algorithm, Ed25519 signs the message directly
  const signature = crypto.sign(null, Buffer.from(canonical), privateKey);

  return signature.toString('base64');
}

// Usage
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = computeSignature(
  process.env.PRIVATE_KEY_PEM, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
);
```

```go
import (
  "crypto/ed25519"
  "crypto/x509"
  "encoding/base64"
  "encoding/pem"
  "errors"
  "fmt"
)

func computeSignature(privateKeyPEM []byte, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  block, _ := pem.Decode(privateKeyPEM)
  if block == nil {
    return "", errors.New("invalid PEM private key")
  }
  key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  if err != nil {
    return "", err
  }
  edKey, ok := key.(ed25519.PrivateKey)
  if !ok {
    return "", errors.New("not an Ed25519 private key")
  }

  // ed25519.Sign hashes internally, sign the canonical string directly
  signature := ed25519.Sign(edKey, []byte(canonical))
  return base64.StdEncoding.EncodeToString(signature), nil
}
```

```python
import time
import base64
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def compute_signature(private_key_pem: bytes, method: str, path: str, timestamp: str, body: str = '') -> str:
  # Build canonical string in fixed order
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  private_key = load_pem_private_key(private_key_pem, password=None)
  # .sign() on an Ed25519PrivateKey signs the message directly, no digest argument
  signature = private_key.sign(canonical.encode())

  return base64.b64encode(signature).decode()

# Usage
timestamp = str(int(time.time()))
signature = compute_signature(
  private_key_pem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  timestamp
)
```

```php
<?php
// Requires PHP 8.4+ (Ed25519 key support landed in ext-openssl in 8.4)
function computeSignature($privateKeyPem, $method, $path, $timestamp, $body = '') {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $privateKey = openssl_pkey_get_private($privateKeyPem);
  // The digest argument is ignored for Ed25519 keys
  openssl_sign($canonical, $signature, $privateKey, 0);

  return base64_encode($signature);
}

// Usage
$timestamp = time();
$signature = computeSignature(
  $privateKeyPem, 'GET',
  '/api/v1/workspaces/your_workspace_id/wallets',
  $timestamp
);
```

> **Warning.** Nunca envíes tu clave privada Ed25519 a Fystack ni la incluyas en código del lado del cliente o del navegador; solo se registra la clave pública. En producción, guarda la clave en un almacén respaldado por hardware como AWS KMS en lugar de un archivo PEM; consulta [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair).

### Firma mediante AWS KMS (sin SDK)

Si usas `@fystack/sdk` en Node.js, `AwsKmsSigner` ya lo hace por ti; consulta [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair). Para cualquier otro backend, o si solo quieres ver el mecanismo, el mismo enfoque funciona con el AWS SDK de cualquier lenguaje: llama a `Sign` de KMS con `MessageType: RAW` y `SigningAlgorithm: ED25519_SHA_512` sobre el string canónico sin procesar y luego codifica en base64 los bytes de la firma devuelta. Esto es exactamente lo que `AwsKmsSigner` hace internamente; el ejemplo de Node.js a continuación es esa misma lógica escrita de forma directa.

> **Warning.** `MessageType` debe ser `RAW`, no `DIGEST`. El algoritmo `ED25519_SHA_512` de KMS espera el mensaje sin hashear y lo hashea internamente, igual que Ed25519 funciona en el resto de esta página. Pasar `DIGEST` firma un valor distinto y no interoperable.

```javascript
import { KMSClient, SignCommand } from '@aws-sdk/client-kms'

async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') {
  const canonical = `method=${method}&path=${path}&timestamp=${timestamp}&body=${body}`

  const response = await kmsClient.send(new SignCommand({
    KeyId: keyId,
    Message: Buffer.from(canonical, 'utf8'),
    MessageType: 'RAW',
    SigningAlgorithm: 'ED25519_SHA_512'
  }))

  return Buffer.from(response.Signature).toString('base64')
}
```

```go
import (
  "context"
  "encoding/base64"
  "fmt"

  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/service/kms"
  "github.com/aws/aws-sdk-go-v2/service/kms/types"
)

func computeSignature(ctx context.Context, client *kms.Client, keyId, method, path, timestamp, body string) (string, error) {
  canonical := fmt.Sprintf("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body)

  out, err := client.Sign(ctx, &kms.SignInput{
    KeyId:            aws.String(keyId),
    Message:          []byte(canonical),
    MessageType:      types.MessageTypeRaw,
    SigningAlgorithm: types.SigningAlgorithmSpecEd25519Sha512,
  })
  if err != nil {
    return "", err
  }

  return base64.StdEncoding.EncodeToString(out.Signature), nil
}
```

```java
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.MessageType;
import software.amazon.awssdk.services.kms.model.SignRequest;
import software.amazon.awssdk.services.kms.model.SignResponse;
import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;
import java.util.Base64;

public String computeSignature(KmsClient kmsClient, String keyId, String method, String path, String timestamp, String body) {
  String canonical = String.format("method=%s&path=%s&timestamp=%s&body=%s", method, path, timestamp, body);

  SignRequest request = SignRequest.builder()
    .keyId(keyId)
    .message(SdkBytes.fromUtf8String(canonical))
    .messageType(MessageType.RAW)
    .signingAlgorithm(SigningAlgorithmSpec.ED25519_SHA_512)
    .build();

  SignResponse response = kmsClient.sign(request);
  return Base64.getEncoder().encodeToString(response.signature().asByteArray());
}
```

```python
import base64

def compute_signature(kms_client, key_id: str, method: str, path: str, timestamp: str, body: str = '') -> str:
  canonical = f"method={method}&path={path}&timestamp={timestamp}&body={body}"

  response = kms_client.sign(
    KeyId=key_id,
    Message=canonical.encode(),
    MessageType='RAW',
    SigningAlgorithm='ED25519_SHA_512'
  )

  return base64.b64encode(response['Signature']).decode()
```

```php
<?php
use Aws\Kms\KmsClient;

function computeSignature(KmsClient $kms, string $keyId, string $method, string $path, string $timestamp, string $body = ''): string {
  $canonical = "method={$method}&path={$path}&timestamp={$timestamp}&body={$body}";

  $result = $kms->sign([
    'KeyId' => $keyId,
    'Message' => $canonical,
    'MessageType' => 'RAW',
    'SigningAlgorithm' => 'ED25519_SHA_512',
  ]);

  // $result['Signature'] is already raw bytes, the SDK base64-decodes it for you
  return base64_encode($result['Signature']);
}
```

`kmsClient`/`client`/`$kms` en los ejemplos anteriores es el cliente KMS que ya te proporciona el AWS SDK de tu lenguaje, configurado como lo haces normalmente. Consulta [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair) para ver el patrón general de entorno local frente a producción (en local, apúntalo a LocalStack/ministack con credenciales de prueba estáticas; en producción, elimina ambas cosas y deja que el rol de IAM proporcione las credenciales); la misma idea se aplica en cualquier lenguaje, solo que mediante las opciones de configuración de ese SDK.
