> Índice completo de la documentación: https://trackerintl.com/devs/llms.txt
> Esta página en HTML: https://trackerintl.com/devs/autenticacion
> Estado: Parcial, revisada el 2026-10-05. Ruta y código confirmados en producción por diff. Los errores 401 y 422 se midieron en un entorno de pruebas; la respuesta 200 del inicio de sesión se lee del código y la caducidad del hash no se midió en producción.

# Autenticación

Cómo obtener tu user_api_hash, cómo enviarlo en cada petición y qué permisos concede, con ejemplos en cURL, JavaScript, Python y PHP.

La API usa un único tipo de credencial: el **`user_api_hash`** de un usuario de la plataforma. Lo obtienes una vez con tu correo y tu contraseña y lo envías en cada petición.

## Cómo funciona

1. `POST /api/login` con `email` y `password` devuelve el `user_api_hash`.
2. En cada llamada siguiente envías ese valor en la cabecera `user-api-hash`.
3. La plataforma resuelve a qué usuario pertenece el hash y aplica sus permisos.

Si tu plataforma lo tiene habilitado, el hash también puede consultarse en la ficha del cliente dentro del panel de administración.

## Enviar la credencial

| Forma | Cómo | Recomendación |
|---|---|---|
| Cabecera | `user-api-hash: TU_API_HASH` | Recomendada |
| Parámetro | `?user_api_hash=TU_API_HASH` (query o cuerpo) | Evítala: las URL quedan en registros de acceso |

`Authorization: Bearer` **no funciona** en esta API.

**Datos y permisos del usuario**

cURL:

```bash
curl -sS "https://plataforma.trackerintl.com/api/get_user_data" \
  -H "user-api-hash: TU_API_HASH"
```

JavaScript:

```javascript
const BASE_URL = "https://plataforma.trackerintl.com";
const API_HASH = "TU_API_HASH";

const respuesta = await fetch(`${BASE_URL}/api/get_user_data`, {
  headers: { "user-api-hash": API_HASH },
});
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}: ${await respuesta.text()}`);
console.log(JSON.stringify(await respuesta.json(), null, 2));
```

Python:

```python
import json
import requests

BASE_URL = "https://plataforma.trackerintl.com"
API_HASH = "TU_API_HASH"

respuesta = requests.get(
    f"{BASE_URL}/api/get_user_data",
    headers={"user-api-hash": API_HASH},
    timeout=30,
)
respuesta.raise_for_status()
print(json.dumps(respuesta.json(), indent=2, ensure_ascii=False))
```

PHP:

```php
<?php
$baseUrl = "https://plataforma.trackerintl.com";
$apiHash = "TU_API_HASH";

$url = $baseUrl . "/api/get_user_data";
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ["user-api-hash: " . $apiHash],
]);
$cuerpo = curl_exec($ch);
$codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($cuerpo === false || $codigo !== 200) {
    fwrite(STDERR, "HTTP $codigo\n");
    exit(1);
}
echo json_encode(json_decode($cuerpo, true), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
```

## Caducidad del hash

La caducidad **no está medida en producción**. Según la lectura del código, el hash no caduca por tiempo y se regenera cuando el usuario cambia su contraseña; al regenerarse, el valor anterior deja de servir. Hasta tener una medición real, trátalo como un dato por confirmar y prepara tu integración para volver a iniciar sesión si recibes un `401`.

## Qué permisos tiene el hash

El hash concede **los mismos permisos que la contraseña del usuario**: no existen claves de solo lectura, claves por cliente ni alcances por módulo. Los permisos del usuario (ver, editar y eliminar por módulo) se aplican en cada petición, y `GET /api/get_user_data` te devuelve la matriz completa. Por eso te recomendamos:

- Crear un usuario dedicado a la integración, con permisos únicamente de "ver".
- Guardar el hash en un gestor de secretos o en variables de entorno, nunca en el código ni en el navegador de tus usuarios.
- No enviarlo en la URL.

## Errores de autenticación

| Respuesta | Causa probable | Qué hacer |
|---|---|---|
| `401` con `{"status": 0, "message": "..."}` | Hash ausente o incorrecto, usuario inactivo, suscripción vencida (también la del gerente) o API deshabilitada por falta de pago del servidor | Verifica el hash; si cambió la contraseña, vuelve a iniciar sesión; si todo está bien, contacta a quien administra tu cuenta |
| `422` en el inicio de sesión | Faltan `email` o `password` | Envía ambos campos |

Consulta el detalle en [Errores](/devs/errores) y los límites de intentos en [Límites](/devs/limites).

## Operación de inicio de sesión

## Obtener el hash de API (user_api_hash)

**POST** `/api/login` · Estado: Parcial

Canjea correo y contraseña por el `user_api_hash`, la credencial que se envía en el resto de las peticiones. Acepta cuerpo JSON o formulario. Si el usuario aún no tiene hash, la plataforma lo genera en ese momento.

Autenticación. Sin autenticación. Límite de la plataforma: 60 peticiones por minuto.

- Según el código, el hash no caduca por tiempo y cambia cuando el usuario cambia su contraseña. No se midió en producción.
- Límite de 60 peticiones por minuto (valor por defecto del código; el valor de producción no está confirmado).
- El hash concede los mismos permisos que la contraseña del usuario. No existen claves de solo lectura ni por alcance.

### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `email` | cuerpo | string | Sí |  |
| `password` | cuerpo | string | Sí |  |

### Ejemplo de solicitud

cURL:

```bash
curl -sS -X POST "https://plataforma.trackerintl.com/api/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"demo@example.com","password":"TU_CONTRASENA"}'
```

JavaScript:

```javascript
const BASE_URL = "https://plataforma.trackerintl.com";

const respuesta = await fetch(`${BASE_URL}/api/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "demo@example.com", password: "TU_CONTRASENA" }),
});
if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}: ${await respuesta.text()}`);
console.log(JSON.stringify(await respuesta.json(), null, 2));
```

Python:

```python
import json
import requests

BASE_URL = "https://plataforma.trackerintl.com"

respuesta = requests.post(
    f"{BASE_URL}/api/login",
    json={"email": "demo@example.com", "password": "TU_CONTRASENA"},
    timeout=30,
)
respuesta.raise_for_status()
print(json.dumps(respuesta.json(), indent=2, ensure_ascii=False))
```

PHP:

```php
<?php
$baseUrl = "https://plataforma.trackerintl.com";

$url = $baseUrl . "/api/login";
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(["email" => "demo@example.com", "password" => "TU_CONTRASENA"]),
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
]);
$cuerpo = curl_exec($ch);
$codigo = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($cuerpo === false || $codigo !== 200) {
    fwrite(STDERR, "HTTP $codigo\n");
    exit(1);
}
echo json_encode(json_decode($cuerpo, true), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
```

### Respuestas

| Código | Descripción |
|---|---|
| `200` | Respuesta correcta. |
| `401` | Hash ausente o incorrecto, usuario inactivo, suscripción vencida (también la del gerente) o API deshabilitada por falta de pago del servidor. HTTP 401 con `{status: 0, message}`. |
| `422` | Faltan email/password. Forma {status:0,errors:{campo:[..]}} (sin statusCode). |
| `429` | Límite de peticiones superado (inferido del comportamiento estándar; no medido). |

| Campo (respuesta 200) | Tipo | Descripción |
|---|---|---|
| `status` |  |  |
| `user_api_hash` | string | Credencial de la API. Larga vida: solo cambia si se cambia la contraseña del usuario. |
| `permissions` | object |  |

Ejemplo de respuesta 200 (datos ficticios):

```json
{
  "status": 1,
  "user_api_hash": "TU_API_HASH",
  "permissions": {
    "devices": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "alerts": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "events": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "geofences": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "routes": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "reports": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "history": {
      "view": true,
      "edit": false,
      "remove": false
    },
    "send_command": {
      "view": false,
      "edit": false,
      "remove": false
    }
  }
}
```

### Verificación

Estado: Parcial. Revisado el 5 de octubre de 2026. Ruta confirmada en producción y código idéntico al verificado. Los errores 401 y 422 se midieron en un entorno de pruebas; la respuesta 200 se lee del código porque no se hizo un inicio de sesión real.
