# 14 · La API del Gateway: primeros pasos

Esta es la **etapa 2**: sus sistemas llaman a Asixto. La etapa 1 —su servidor de herramientas— es lo que
cubren las páginas 01 a 10, y las dos son independientes: puede llamar a esta API sin haber publicado
ningún servidor.

**Solo HTTP.** No hay SDK que instalar ni versionar: la superficie es JSON sobre HTTPS y cualquier cliente
HTTP sirve. Cada muestra de estas páginas viene en **cURL, Node.js, Python, PHP y Go** —las cinco generadas
del mismo descriptor de la operación, así que describen exactamente la misma llamada— y ninguna imprime su
clave: va por variable de entorno.

## Lo que necesita antes de la primera llamada

| | |
|---|---|
| Una **clave de API** | Se la entregamos en la incorporación. Empieza por `ak_test_` o por `ak_live_` |
| La **URL base** | Se la damos con la clave. Todas las rutas de esta página van bajo `/v1` |
| Nada más | Sin registro de aplicación, sin OAuth, sin lista de direcciones IP obligatoria |

## Los dos entornos, y en qué se diferencian

El entorno va **dentro de la clave**, así que quien la ve en un registro o en un archivo de configuración
sabe contra qué está operando.

| | `ak_test_` | `ak_live_` |
|---|---|---|
| ¿Se ejecuta de verdad? | **Sí**, con los mismos proveedores | Sí |
| ¿Consume créditos? | **Sí, al mismo precio** | Sí |
| ¿Puede recibir un `402`? | **Sí**, si se queda sin saldo o llega a su tope | Sí |
| Concurrencia | Propia de este entorno | Propia de este entorno |
| Trabajos que puede leer | Solo los de pruebas | Solo los de producción |
| Número desde el que sale una llamada | El **nuestro**, compartido | El **suyo**, obligatorio |
| A quién puede llamar | Solo a sus **números de prueba verificados** | A quien usted decida |

🔴 **El entorno de pruebas no es gratis, y es lo primero que hay que dimensionar.** Ejecuta contra los mismos
proveedores que producción, así que cada prueba nos cuesta lo que cuesta la operación y se le cobra a la misma
tarifa ([página 12](./12-pricing.md)). La alternativa —devolver una respuesta simulada— haría que una
integración probada no significara nada: estaría certificando contra un resultado que su cliente final nunca
va a recibir.

**Lo que acota el gasto de sus pruebas es su tope de gasto, no el entorno.** Pruebas y producción gastan del
mismo saldo y ninguno tiene techo de créditos propio; lo que sí es propio de cada entorno es la concurrencia
([página 18](./18-api-limits.md)).

La otra diferencia real está en la telefonía: una llamada de `ak_test_` sale con **nuestro** número
compartido, y una de `ak_live_` con el de su cuenta. Comprar y mantener un número por cuenta solo para probar
es una renta que nadie paga; sacar tráfico de producción por un número ajeno le mostraría al cliente final un
número que no es el suyo.

Un trabajo no cruza de entorno. Cuando promocione de pruebas a producción, cambia la clave, y su cuenta
necesita su número propio listo (abajo).

## Llamadas: qué poner en `to` y a quién puede llamar

`to` va en **E.164**: el `+`, el indicativo de país y el número, sin espacios ni guiones (`+573001112233`). No
adivinamos el país de un número local: un valor sin esa forma recibe `400 validation_error`.

### Con `ak_test_`: solo a sus números de prueba verificados

Como la llamada de pruebas sale con nuestro número, solo puede ir a un número que usted haya demostrado que es
suyo. Se dan de alta en la consola, en **Números de prueba**:

1. Registre el número. Le enviamos un código por SMS.
2. Escriba el código en la consola. El número queda **verificado** y ya puede llamarlo.

- **Hasta 5 números por cuenta**, contando los pendientes de verificar y los verificados. Uno pendiente que
  no verifica en 30 minutos caduca y libera su hueco; quitar uno también lo libera.
- **El envío del código lo paga Asixto**, no sus créditos. Por eso tiene límites en una ventana de 24 horas:
  10 envíos por cuenta, 5 al mismo número sumando todas las cuentas que lo registren, y un presupuesto global
  de la plataforma. Cada intento cuenta contra el límite de su cuenta aunque luego lo frene el del número o el global.
- Llamar con `ak_test_` a un número que no es uno de sus verificados responde
  `409 test_destination_not_verified`, no se cobra y no consume la `Idempotency-Key`: verifíquelo y reintente
  con la misma clave.

**La transferencia a una persona también va solo a un número verificado.** Al guardar la conducta de llamada de
pruebas, un número de transferencia que no lo esté se rechaza. Si deja de estarlo después —porque lo
quitó: un número verificado no caduca—, la llamada **sale igual, sin transferencia**, y el acuse lo declara en
`warnings.transfer_disabled` ([página 15](./15-api-reference.md)).

### Con `ak_live_`: su número propio, obligatorio

En producción la llamada sale **siempre** con el número de su cuenta; ya no hay número de Asixto de reserva. Si
el suyo no está listo, la llamada no sale, no se cobra y no consume la `Idempotency-Key`:

| Lo que recibe | Cuándo | Qué hacer |
|---|---|---|
| `409 phone_number_required` | No tiene número, o su expediente de reputación falta, está incompleto o fue rechazado | Compre su número, o cree, complete o corrija el expediente en la consola |
| `409 phone_number_in_review` | Su número está en provisión, o su expediente en revisión del operador | Esperar: cuando la consola lo muestre listo, la misma petición funciona |
| `402 phone_number_suspended` | Su número está suspendido porque venció la gracia del pago de su cuota | Ponerse al día con el pago; vuelve a servicio sin cambiar de clave |

Si el fallo es nuestro —la provisión falló en el proveedor o un dato de su número es inconsistente—, recibe un
`5xx` y lo atendemos nosotros: no hay nada que cambiar en su lado ([página 16](./16-api-errors.md)).

## Su primera llamada

**cURL**

```bash
curl -sS -X POST "$ASIXTO_BASE_URL/v1/text/sentiment" \
  -H "Authorization: Bearer $ASIXTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sent-4471" \
  -d '{"text":"Llevo tres días esperando y nadie me responde.","customer_id":"cliente-12345"}'
```

**Node.js**

```js
const respuesta = await fetch(`${process.env.ASIXTO_BASE_URL}/v1/text/sentiment`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ASIXTO_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'sent-4471',
  },
  body: JSON.stringify({
    "text": "Llevo tres días esperando y nadie me responde.",
    "customer_id": "cliente-12345"
  }),
});

const sobre = await respuesta.json();
console.log(sobre.job_id, sobre.usage.credits, sobre.result);
```

**Python**

```python
import os
import requests

url = f"{os.environ['ASIXTO_BASE_URL']}/v1/text/sentiment"
cabeceras = {
    "Authorization": f"Bearer {os.environ['ASIXTO_API_KEY']}",
    "Idempotency-Key": "sent-4471",
}

cuerpo = {
  "text": "Llevo tres días esperando y nadie me responde.",
  "customer_id": "cliente-12345"
}

respuesta = requests.post(url, headers=cabeceras, json=cuerpo, timeout=120)
respuesta.raise_for_status()

sobre = respuesta.json()
print(sobre["job_id"], sobre["usage"]["credits"], sobre["result"])
```

**PHP**

```php
<?php

$url = getenv('ASIXTO_BASE_URL') . '/v1/text/sentiment';
$cabeceras = [
    'Authorization: Bearer ' . getenv('ASIXTO_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: sent-4471',
];

$cuerpo = {
  'text': 'Llevo tres días esperando y nadie me responde.',
  'customer_id': 'cliente-12345'
};

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => $cabeceras,
    CURLOPT_POSTFIELDS => json_encode($cuerpo),
]);

$respuesta = curl_exec($ch);
curl_close($ch);

$sobre = json_decode($respuesta, true);
echo $sobre['job_id'], ' ', $sobre['usage']['credits'], PHP_EOL;
```

**Go**

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)

func main() {
	cuerpo := strings.NewReader(`{"text":"Llevo tres días esperando y nadie me responde.","customer_id":"cliente-12345"}`)

	peticion, _ := http.NewRequest("POST", os.Getenv("ASIXTO_BASE_URL")+"/v1/text/sentiment", cuerpo)
	peticion.Header.Set("Authorization", "Bearer "+os.Getenv("ASIXTO_API_KEY"))
	peticion.Header.Set("Content-Type", "application/json")
	peticion.Header.Set("Idempotency-Key", "sent-4471")

	respuesta, err := http.DefaultClient.Do(peticion)
	if err != nil {
		panic(err)
	}
	defer respuesta.Body.Close()

	var sobre map[string]any
	json.NewDecoder(respuesta.Body).Decode(&sobre)
	fmt.Println(sobre["job_id"], sobre["usage"], sobre["result"])
}
```

```json
{
  "request_id": "req_ec981f75a0380ea072e42485",
  "job_id": "19f77b69-4a92-4821-ab3e-7fce38b38953",
  "usage": { "operation": "sentiment_analysis", "unit": "request", "quantity": 1, "credits": 1 },
  "result": { "sentiment": "positive", "score": 0.9, "source": "nlp" }
}
```

## El sobre: cuatro campos, siempre los mismos

Toda respuesta correcta tiene esta forma, para las ocho operaciones. Escriba su capa de transporte una vez
y reutilícela.

| Campo | Qué es |
|---|---|
| `request_id` | Correlación de **esta** petición. Es lo que citamos en un incidente |
| `job_id` | El trabajo que la atendió. Con él consulta su estado más tarde ([página 17](./17-jobs.md)) |
| `usage` | Lo que se le cobró: `operation`, `unit`, `quantity` y `credits` |
| `result` | El resultado de la operación |

**El consumo viaja en la respuesta, no solo en la factura.** Es lo que le permite repercutir el coste a su
propio cliente en el momento en vez de enterarse a fin de mes, y es el mismo dato que verá en su desglose,
así que las dos cifras se concilian.

`usage.quantity` son las unidades **cobradas**, no las autorizadas.

La única excepción a la forma es `POST /v1/audio/speech`, que devuelve el audio como cuerpo binario: ahí el
sobre viaja en cabeceras `X-Asixto-*` con el mismo significado ([página 15](./15-api-reference.md)).

## `Idempotency-Key`: la mande siempre

Es una cabecera suya, un texto que usted elige, y es lo que convierte un reintento en algo seguro.

- **Misma clave, operación terminada** → se le devuelve el mismo `job_id` y el mismo `usage`, **sin volver
  a ejecutar y sin volver a cobrar**.
- **Misma clave, operación todavía en curso** → `409 request_in_flight`. No es un error suyo: es la verdad.
- **Misma clave para otra operación, o con otras `variables`** → `409 idempotency_key_reused`.

Sin ella, un tiempo de espera agotado en su lado le deja sin saber si la operación ocurrió, y el único
camino es preguntar o pagar dos veces. Los cinco casos de una repetición están en la
[página 17](./17-jobs.md).

## Autenticación

```
Authorization: Bearer ak_live_…
```

Si falta o no tiene esa forma: `401 authentication_required`. Si la clave no existe, está revocada o
caducó: `401 invalid_api_key` — **no distinguimos entre los tres a propósito**, porque decir cuál es
ayudaría a quien está probando claves.

Cada clave lleva sus **permisos** —ocho en total, uno por capacidad más el de lectura— y puede llevar una
lista de direcciones IP autorizadas. Si su clave no tiene el permiso de la operación:
`403 insufficient_scope`, y la respuesta declara en `WWW-Authenticate` cuál hacía falta. Qué autoriza cada
uno, y cuál conviene conceder siempre, está en la [página 15](./15-api-reference.md).

Los permisos se fijan **al emitir** y no se editan después: si le falta uno, emita otra clave. Cada entorno
admite dos claves activas a la vez, así que puede desplegar la nueva y revocar la anterior sin ventana de
caída.

## Errores: programe contra el `code`

```json
{
  "error": {
    "code": "validation_error",
    "message": "agent_id debe ser el identificador de un agente de su cuenta.",
    "request_id": "req_22398948bdff3f425bdeb7da",
    "doc_url": "https://asixto.com/docs/gateway/errors#validation_error"
  }
}
```

El **`code` es lo único estable**: el estado HTTP significa cosas distintas según el cliente, el texto se
reescribe y el idioma se traduce. El catálogo completo, con qué hacer en cada caso, está en la
[página 16](./16-api-errors.md).

Dos reglas que puede dar por buenas: **un fallo de negocio nunca llega como `5xx`**, y los errores
temporales traen `Retry-After` en segundos.

## Qué hacer a continuación

| Para | Vaya a |
|---|---|
| La referencia de las ocho operaciones | [página 15](./15-api-reference.md) |
| Qué significa cada error y si conviene reintentar | [página 16](./16-api-errors.md) |
| Trabajos asíncronos, consulta de estado y reintentos | [página 17](./17-jobs.md) |
| Recibir eventos en su sistema | [página 11](./11-webhooks.md) |
| Qué guardamos de su tráfico y quién más lo ve | [página 13](./13-data.md) |
| Cuánto cuesta un turno del agente | [página 12](./12-pricing.md) |
