# Manual de integración — API EnviosChile

API REST para cotizar envíos dentro de Chile desde tu ecommerce. Envías origen, destino, peso y dimensiones; recibes la tarifa en pesos chilenos (CLP) para entrega en agencia y a domicilio.

- **URL base:** `https://api.envioschile.net`
- **Formato:** JSON (UTF-8) en request y response
- **Moneda:** CLP, montos enteros

> **Nota legal:** EnviosChile no es Starken ni está afiliado, patrocinado ni autorizado por Starken u otro courier; "Starken" se menciona solo con fines descriptivos de compatibilidad. Las tarifas son **estimaciones referenciales** que pueden diferir del cobro oficial y cambiar en el tiempo.

---

## 1. Autenticación

Toda consulta a la API pública requiere una **API key**. Se obtiene en el panel: [Crear cuenta](https://envioschile.net/register) → **API keys** → **Crear key**.

La key completa (`sk_live_…`) se muestra **una sola vez** al crearla. Guárdala en un lugar seguro (variable de entorno, secreto del servidor). En la base de datos solo se almacena su hash.

Envíala en cada request con cualquiera de estos headers:

```
X-API-Key: sk_live_abc123...
```

o

```
Authorization: Bearer sk_live_abc123...
```

**Importante:** llama a la API siempre **desde tu servidor** (PHP, Node, etc.), nunca desde el navegador — expondrías tu key.

---

## 2. Cotizar un envío — `POST /api/v1/quote`

### Request

```json
{
  "origen": "SANTIAGO",
  "destino": "PUERTO MONTT",
  "bultos": [
    { "alto": 20, "largo": 30, "ancho": 15, "peso": 2.5 }
  ],
  "valor_declarado": 10000,
  "tipo_entrega": "AMBOS"
}
```

| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| `origen` | string | sí | Nombre de ciudad, nombre de comuna o `ciud_codigo` (ej: `"SANTIAGO"`, `"PROVIDENCIA"`, `"1"`) |
| `destino` | string | sí | Igual que `origen` |
| `bultos` | array | sí | 1 a 20 bultos. Cada uno se tarifica por separado y se suman |
| `bultos[].alto/largo/ancho` | number | sí | Centímetros. Máximo 650 cm por lado |
| `bultos[].peso` | number | sí | Kilogramos. Mayor que 0 |
| `valor_declarado` | integer | no | CLP, > 0 y ≤ 1.000.000. No afecta el precio (seguro incluido hasta $50.000) |
| `tipo_entrega` | string | no | `AGENCIA`, `DOMICILIO` o `AMBOS` (default `AMBOS`) |

### Response `200 OK`

```json
{
  "origen":  { "ciudad": "SANTIAGO", "ciud_codigo": 1, "comuna": "SANTIAGO" },
  "destino": { "ciudad": "PUERTO MONTT", "ciud_codigo": 74, "comuna": "PUERTO MONTT" },
  "origen_zona": "CENTRO",
  "destino_zona": "SUR",
  "peso_facturable": 2.5,
  "volumen_cm3": 9000,
  "bultos": [
    { "peso": 2.5, "volumen_cm3": 9000, "peso_facturable": 2.5 }
  ],
  "opciones": [
    { "tipo_entrega": "AGENCIA",   "tarifa_base": 8220, "recargo": 2000, "tarifa": 10220, "dias_entrega": "2-4" },
    { "tipo_entrega": "DOMICILIO", "tarifa_base": 8650, "recargo": 2000, "tarifa": 10650, "dias_entrega": "3-5" }
  ],
  "moneda": "CLP",
  "exactitud": "exacta",
  "nota": "Tarifa referencial. Incluye recargo configurado por el comercio."
}
```

- `peso_facturable` = `max(peso_real, alto×largo×ancho / 4000)` por bulto.
- Desglose de cada opción: `tarifa_base` es el valor del modelo, `recargo` es lo que configuraste en el panel (monto fijo + porcentaje) y `tarifa` es el total para mostrar al comprador final. Puedes exponer solo `tarifa` en tu checkout.
- `exactitud`: `"exacta"` cuando el par origen→destino está calibrado con datos muestreados; `"estimada"` cuando se resolvió por zona o por el par inverso en pesos altos.
- `dias_entrega` es una estimación referencial en días hábiles.

### Códigos de error

| Código | Significado | Qué hacer |
|---|---|---|
| `400` | Validación: dimensiones fuera de rango, peso ≤ 0, body malformado | Corrige el request. **No cuenta contra tu cuota** |
| `401` | API key faltante, inválida o revocada | Revisa el header y el estado de la key en el panel |
| `402` | Plan vencido o pago pendiente | Regulariza el pago en el panel |
| `422` | Ciudad de origen o destino no encontrada | Usa el catálogo (`GET /api/v1/cities`) para validar nombres |
| `429` | Cuota mensual agotada, ráfaga (>10 req/s) o >5 consultas simultáneas | Ver sección 3 |
| `5xx` | Error interno | Reintenta con backoff exponencial |

Formato de error:

```json
{ "statusCode": 429, "message": "Cuota mensual agotada, actualiza a PRO" }
```

---

## 3. Límites y cuotas

| Plan | Cuota mensual | Precio |
|---|---|---|
| **FREE** | 500 cotizaciones exitosas / mes calendario | $0 |
| **PRO** | Ilimitadas | USD 15/mes |

Límites técnicos (ambos planes):

- **10 requests/segundo** por API key
- **5 consultas simultáneas** por API key
- Bloqueo temporal de 15 minutos ante abuso sostenido del límite de ráfaga
- 1 API key con acceso total por cuenta (para rotarla, crea una nueva y revoca la anterior)

Headers en cada respuesta:

```
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 342
Retry-After: 60          (solo en respuestas 429)
```

**Política de reintentos recomendada:** ante un `429`, espera lo que indique `Retry-After` (o usa backoff exponencial: 1s, 2s, 4s… máximo 3 intentos). Ante `5xx`, backoff exponencial. Nunca reintentes un `400` o `422` sin corregir el request.

**Consejo:** las tarifas cambian poco. Cachea la respuesta por par (origen, destino, peso, dimensiones) durante algunas horas en tu servidor y ahorrarás la mayor parte de tu cuota.

---

## 4. Catálogo de ciudades — `GET /api/v1/cities`

El catálogo maestro (2.167 localidades, 16 regiones) vive en la API. Tu integración debe descargarlo y cachearlo (recomendado: refrescar cada 30 días).

```
GET /api/v1/cities?q=temuco&page=1&page_size=100
GET /api/v1/cities?region=13
```

Respuesta:

```json
{
  "page": 1,
  "page_size": 100,
  "results": [
    {
      "ciudad": "TEMUCO",
      "ciud_codigo": 94,
      "comuna": "TEMUCO",
      "region_num": 9,
      "region": "La Araucanía",
      "zona": "CENTRO_SUR"
    }
  ]
}
```

### Resolución rápida — `GET /api/v1/cities/resolve?q=...`

Convierte una comuna/ciudad de tu checkout al registro canónico:

```
GET /api/v1/cities/resolve?q=PROVIDENCIA
→ { "ciudad": "SANTIAGO", "ciud_codigo": 1, "comuna": "PROVIDENCIA", "zona": "CENTRO", ... }
```

Formatos aceptados en `origen`/`destino` y en `resolve`: nombre de ciudad (`"PUERTO MONTT"`), nombre de comuna (`"LAS CONDES"`) o código (`"74"`). Mayúsculas/minúsculas y acentos son indiferentes.

### Nota técnica: herencia por central de carga

La macrozona de una localidad se hereda de la **central de carga que la atiende**, no de su región administrativa. Ejemplo real: Angol está en La Araucanía (región 9) pero tarifica como Biobío (`CENTRO_SUR`) porque la sirve la central de Los Ángeles. El catálogo ya trae la zona resuelta — usa siempre el campo `zona` del catálogo, nunca deduzcas la zona desde la región.

### Macrozonas

| Zona | Referencias |
|---|---|
| `EXTREMO_NORTE` | Arica, Iquique, Antofagasta, Calama |
| `NORTE_CHICO` | Copiapó, La Serena, Coquimbo |
| `CENTRO` | Valparaíso, Santiago, Rancagua |
| `CENTRO_SUR` | Curicó, Talca, Chillán, Concepción, Temuco |
| `SUR` | Valdivia, Osorno, Puerto Montt |
| `EXTREMO_AUSTRAL` | Coyhaique, Punta Arenas |

Ciudades principales con código: Santiago=1, Concepción=2, La Serena=4, Iquique=5, Coyhaique=6, Calama=10, Chillán=19, Copiapó=26, Coquimbo=27, Curicó=32, Arica=39, Los Ángeles=53, Osorno=63, Puerto Montt=74, Rancagua=80, Antofagasta=84, Angol=88, Talca=91, Temuco=94, Valdivia=98, Valparaíso=100, Viña del Mar=104, Punta Arenas=121, Castro=388, Ancud=389.

---

## 5. Ejemplos de código

### cURL

```bash
curl -X POST https://api.envioschile.net/api/v1/quote \
  -H "X-API-Key: $ENVIOSCHILE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "origen": "SANTIAGO",
    "destino": "PUERTO MONTT",
    "bultos": [{ "alto": 20, "largo": 30, "ancho": 15, "peso": 2.5 }],
    "tipo_entrega": "AMBOS"
  }'
```

### PHP (WordPress / WooCommerce)

```php
<?php
function envioschile_cotizar( $origen, $destino, $bultos ) {
    $response = wp_remote_post( 'https://api.envioschile.net/api/v1/quote', array(
        'headers' => array(
            'X-API-Key'    => get_option( 'envioschile_api_key' ),
            'Content-Type' => 'application/json',
        ),
        'body'    => wp_json_encode( array(
            'origen'       => $origen,
            'destino'      => $destino,
            'bultos'       => $bultos,
            'tipo_entrega' => 'AMBOS',
        ) ),
        'timeout' => 10,
    ) );

    if ( is_wp_error( $response ) ) {
        return null;
    }
    $code = wp_remote_retrieve_response_code( $response );
    if ( 200 !== $code ) {
        error_log( 'EnviosChile error ' . $code . ': ' . wp_remote_retrieve_body( $response ) );
        return null;
    }
    return json_decode( wp_remote_retrieve_body( $response ), true );
}
```

### Node.js

```js
const API_KEY = process.env.ENVIOSCHILE_API_KEY;

async function cotizar(origen, destino, bultos) {
  const res = await fetch('https://api.envioschile.net/api/v1/quote', {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ origen, destino, bultos, tipo_entrega: 'AMBOS' }),
  });
  if (res.status === 429) {
    const retryAfter = Number(res.headers.get('retry-after') ?? 1);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return cotizar(origen, destino, bultos); // un solo reintento
  }
  if (!res.ok) throw new Error(`EnviosChile ${res.status}: ${await res.text()}`);
  return res.json();
}
```

---

## 6. Guía: plugin de WooCommerce

El plugin solo necesita: (1) leer la comuna de destino del checkout, (2) armar los bultos desde el carrito, (3) llamar a la API **desde el servidor** y (4) mostrar las opciones como métodos de envío. Toda la inteligencia de zonas y tarifas queda en la API.

Esqueleto de un shipping method:

```php
<?php
/**
 * Plugin Name: EnviosChile Shipping
 * Description: Cotización de envíos en tiempo real vía API EnviosChile.
 */

add_action( 'woocommerce_shipping_init', function () {

    class WC_EnviosChile_Shipping extends WC_Shipping_Method {

        public function __construct( $instance_id = 0 ) {
            $this->id                 = 'envioschile';
            $this->instance_id        = absint( $instance_id );
            $this->method_title       = 'EnviosChile';
            $this->method_description = 'Las tarifas mostradas son estimaciones referenciales para envíos por Starken. Puedes añadir un recargo fijo o porcentual desde tu panel de EnviosChile.';
            $this->supports           = array( 'shipping-zones', 'instance-settings' );
            $this->init_form_fields();
            // El comerciante puede renombrar el método como quiera bajo su responsabilidad.
            $this->title = $this->get_option( 'title', 'Envío por Starken (referencial)' );
        }

        public function init_form_fields() {
            $this->instance_form_fields = array(
                'api_key' => array(
                    'title' => 'API Key',
                    'type'  => 'password',
                ),
                'origen'  => array(
                    'title'   => 'Comuna de origen (tu bodega)',
                    'type'    => 'text',
                    'default' => 'SANTIAGO',
                ),
            );
        }

        public function calculate_shipping( $package = array() ) {
            // 1. Comuna de destino desde el checkout
            $destino = strtoupper( $package['destination']['city'] );
            if ( empty( $destino ) ) {
                return;
            }

            // 2. Bultos desde el carrito (dimensiones en cm, peso en kg)
            $bultos = array();
            foreach ( $package['contents'] as $item ) {
                $p = $item['data'];
                for ( $i = 0; $i < $item['quantity']; $i++ ) {
                    $bultos[] = array(
                        'alto'  => (float) $p->get_height() ?: 10,
                        'largo' => (float) $p->get_length() ?: 10,
                        'ancho' => (float) $p->get_width()  ?: 10,
                        'peso'  => (float) $p->get_weight() ?: 0.5,
                    );
                }
            }

            // 3. Cotizar (con caché transitorio de 6 horas)
            $cache_key = 'ec_quote_' . md5( $destino . wp_json_encode( $bultos ) );
            $quote     = get_transient( $cache_key );
            if ( false === $quote ) {
                $quote = envioschile_cotizar( $this->get_option( 'origen' ), $destino, $bultos );
                if ( $quote ) {
                    set_transient( $cache_key, $quote, 6 * HOUR_IN_SECONDS );
                }
            }
            if ( ! $quote ) {
                return;
            }

            // 4. Una rate por opción de entrega
            foreach ( $quote['opciones'] as $opcion ) {
                $label = 'AGENCIA' === $opcion['tipo_entrega']
                    ? 'Retiro en agencia'
                    : 'Despacho a domicilio';
                $this->add_rate( array(
                    'id'    => $this->id . '_' . strtolower( $opcion['tipo_entrega'] ),
                    'label' => $label . ' (' . $opcion['dias_entrega'] . ' días hábiles)',
                    'cost'  => $opcion['tarifa'],
                ) );
            }
        }
    }
} );

add_filter( 'woocommerce_shipping_methods', function ( $methods ) {
    $methods['envioschile'] = 'WC_EnviosChile_Shipping';
    return $methods;
} );
```

Recomendaciones:

- Descarga el catálogo (`GET /api/v1/cities`) en la activación del plugin y guárdalo como opción/tabla local para poblar el selector de comunas; refréscalo cada 30 días.
- Usa `GET /api/v1/cities/resolve?q=` si la comuna escrita por el cliente no calza exacta.
- Cachea cotizaciones con transients (como arriba): ahorra cuota y acelera el checkout.
- Maneja el `429` degradando con elegancia: muestra un costo fijo de respaldo o "envío a coordinar".

---

## 7. Notas legales

- EnviosChile **no es Starken** ni está afiliado, patrocinado ni autorizado por Starken u otro courier. "Starken" es una marca de su titular y se menciona solo con fines descriptivos de compatibilidad.
- Las tarifas entregadas son **estimaciones referenciales** que pueden diferir del cobro oficial y cambiar en el tiempo. Verifica siempre el valor final con el courier.
- No incluyas el logo ni los colores corporativos de Starken en tu tienda o plugin al usar este servicio.
- El servicio se entrega "tal cual", con los límites de cuota y disponibilidad del plan contratado.
