← GeoStream

07 — API REST v1 y webhooks (semántica)

ID: GS-API-001 · Versión: 0.2.0 Contrato máquina (fuente de verdad de rutas y esquemas): contracts/openapi/geostream.v1.yaml (OpenAPI 3.1)

Este documento define la semántica que OpenAPI no expresa. Autenticación y autorización: 06_IDENTITY_AUTH.md. Validación y errores: 09_VALIDATION_ERRORS.md.

1. Convenciones

Aspecto Regla
Base /v1. Salud en /health/* (sin versión).
Formato JSON UTF-8. Peticiones con cuerpo: Content-Type: application/json. Errores: application/problem+json.
Nombres de campo snake_case.
Enums Cadenas en mayúsculas: nombre Protobuf sin el prefijo del enum (SESSION_MODE_ONE_TO_ONE → ONE_TO_ONE).
Tiempos RFC 3339 UTC con Z y milisegundos en respuestas.
Enteros grandes sequence como número JSON (≤ 2^53 − 1).
Nulos Los campos opcionales ausentes en un recurso se devuelven como null cuando el esquema lo declara; nunca se omiten campos required.
Campos desconocidos en peticiones 422 (09 §2).
Caché Toda respuesta bajo /v1 lleva Cache-Control: no-store.
Correlación Toda respuesta lleva X-Request-Id: req_…; el mismo ID aparece en logs y en Problem.request_id.

2. Cabeceras

Petición: Authorization: Bearer <credencial>, Content-Type, Idempotency-Key (POST mutantes), If-Match (PATCH). Respuesta: X-Request-Id, ETag (recursos versionados: sesión, miembro), Retry-After (429, 503), Idempotent-Replayed: true (respuesta repetida por idempotencia), Deprecation / Sunset (cuando aplique, §11).

3. Autenticación

Ver 06. Resumen: API key de aplicación o token de dispositivo en Authorization: Bearer. El esquema de seguridad y los scopes de cada operación están en OpenAPI (security, x-gs-auth, x-gs-capability).

4. Códigos de estado usados

200, 201 (creación), 202 (encolado), 204 (sin cuerpo), 400, 401, 403, 404, 405, 409, 412, 413, 415, 422, 429, 500, 503. El significado de cada code está en 09 §6.

5. Problem Details

{
  "type": "https://geostream.dev/problems/session_full",
  "title": "Session is full",
  "status": 409,
  "code": "SESSION_FULL",
  "detail": "max_members reached",
  "request_id": "req_0192b6c0a1e47c3f9d2a7b5e8f1c0d3a",
  "errors": []
}

6. Idempotencia

Aplica a todo POST que crea o muta (marcado con el parámetro Idempotency-Key en OpenAPI). PUT, PATCH con If-Match y DELETE son idempotentes por semántica.

Algoritmo:

  1. Sin cabecera → procesar normalmente.
  2. Clave de almacenamiento: (tenant_id, principal_id, idempotency_key); principal_id = key_id de la API key o device_id.
  3. Huella: SHA-256 de método + " " + ruta + "\n" + cuerpo canónico (JSON con claves ordenadas y sin espacios).
  4. Si no existe: insertar registro IN_PROGRESS (insert con conflicto controlado) → procesar → guardar estado HTTP y cuerpo de la respuesta → COMPLETED.
  5. Si existe COMPLETED con la misma huella → devolver la respuesta guardada con Idempotent-Replayed: true.
  6. Si existe con otra huella → 422 IDEMPOTENCY_KEY_REUSED.
  7. Si existe IN_PROGRESS → 409 CONFLICT (el cliente reintenta tras una breve espera).
  8. Las respuestas 5xx no se guardan (el registro se elimina para permitir reintento). Las 4xx sí se guardan.
  9. Retención: GS_RETENTION_IDEMPOTENCY_H.

Las respuestas que contienen secretos que solo se muestran una vez (código de invitación, secreto de webhook) sí se guardan para la repetición, cifradas en reposo con GS_DATA_ENCRYPTION_KEY y purgadas al vencer la retención. Ver 10 §6.

7. Paginación

8. Rate limits

Principal Límite Parámetro
Dispositivo peticiones/minuto por dispositivo GS_REST_RATE_LIMIT_DEVICE_RPM
Aplicación peticiones/minuto por API key GS_REST_RATE_LIMIT_APP_RPM
Canje de invitaciones intentos/minuto por dispositivo GS_INVITE_REDEEM_RPM
Fallos de autenticación por IP GS_AUTH_FAILURE_RPM_PER_IP

Ventana fija de 60 s en el almacén efímero (memoria en nodo único). Al exceder: 429 RATE_LIMITED con Retry-After = segundos hasta el fin de la ventana (≥ 1). Las respuestas pueden incluir RateLimit-Limit y RateLimit-Remaining (informativas).

9. Semántica por recurso

9.1 Tokens y tickets

9.2 Sesiones

9.3 Miembros

9.4 Invitaciones

9.5 Subjects y dispositivos

9.6 Snapshot

9.7 Geocercas (F11)

Ver 14_GEOFENCING.md.

9.8 Ingesta HTTP (F8)

9.9 Pools de disponibilidad (E4)

Ver 23_MOBILITY.md. Todas estas operaciones exigen política de tenant pools_allowed y son aditivas: no cambian el comportamiento de /v1/sessions ni de /v1/sessions/{id}/members para los modos existentes.

Operación Scope de aplicación Scope de dispositivo Notas
POST /v1/pools pools:manage ✗ Crea una sesión con mode = POOL. No acepta owner_external_subject_id ni initial_members; los conductores entran solos.
GET /v1/pools/{id} sessions:read ✗ Devuelve la Session con mode = POOL.
GET /v1/pools/{id}/nearby pools:query ✗ §9.9.1
GET /v1/pools/{id}/rings pools:query ✗ §9.9.2
GET /v1/pools/{id}/density pools:query pools:density §9.9.3; sin identificadores
PUT /v1/pools/{id}/members/{member_id}/availability pools:manage sharing:write (solo me) AVAILABLE / BUSY / PAUSED
PUT /v1/pools/{id}/members/{member_id}/attributes pools:manage ✗ Solo aplicación; ≤ pool_max_attributes entradas (09 §5.1)
POST /v1/pools/{id}/reset-odometer members:write ✗ Reinicia el odómetro de un miembro del pool

POST /v1/pools es un alias de sugar sintáctico de POST /v1/sessions con mode = POOL forzado: no duplica reglas. Su respuesta es la misma Session con ETag.

9.9.1 nearby

GET /v1/pools/{id}/nearby?lat=&lon=&radius_m=&limit=&rank=&max_age_s=&filter.attributes[k]=v

Ordena por distance (predeterminado) o por eta. Con rank = eta se aplica el prefiltrado pool_eta_prefilter (los N candidatos más cercanos) antes de estimar el tiempo de llegada. Cada elemento incluye member_id, distance_m, age_ms, availability, attributes (si los pidió el filtro), position y, con rank = eta, eta_s y mode.

Nunca incluye a quien está PAUSED ni a quien no está conectado, salvo que max_age_s lo permita explícitamente. Límite duro: pool_query_max_radius_m.

9.9.2 rings

GET /v1/pools/{id}/rings?lat=&lon=&ring_step_m=&k=&per_ring_limit=

Devuelve anillos concéntricos desde el centro. Exactamente uno de ring_step_m o k debe enviarse. per_ring_limit acota cada anillo (1..=100); el resto se omite y la respuesta indica truncated: true en el anillo afectado. Solo para aplicación.

9.9.3 density

GET /v1/pools/{id}/density?lat=&lon=&radius_m=&resolution=

Devuelve marcadores aproximados y anónimos, sin member_id, sin subject_id, sin device_id y sin marcas de tiempo exactas (FR-POOL-006). Cada marcador lleva un conteo, un centroide de celda H3 desplazado de forma aleatoria pero acotada, rumbo cuantizado a 8 sectores y delay_ms ≥ pool_density_delay_s. Máximo pool_density_max_markers marcadores. Es la única consulta de pool accesible a un dispositivo, y existe precisamente para que un pasajero no necesite ver conductores individuales.

9.10 Herramientas de movilidad (E5)

Ver 23_MOBILITY.md §4–§7. Todos exigen tools:use y tenant.tools_allowed.

Operación Descripción
POST /v1/tools/eta Matriz de tiempos de llegada origen→destino. Requiere scope de aplicación o de dispositivo con tools:use.
POST /v1/tools/assignments Asignación por lotes de tareas a trabajadores; hungarian o min_cost_flow.
POST /v1/tools/routes Ruta sobre el grafo vial propio o por Provider Gateway (GS_GRAPH_PATHS).

Los tres aceptan source_preference ∈ {PROVIDER, LEARNED, ESTIMATED}. Con PROVIDER y el proveedor caído se devuelve 503 PROVIDER_UNAVAILABLE con Retry-After; nunca se degrada silenciosamente a la estimación propia, porque el integrador pidió explícitamente el proveedor.

GET/PUT/DELETE /v1/providers/{port} (scope providers:manage) configuran el Provider Gateway: port ∈ {ROUTE_MATRIX, ROUTE, MAP_MATCHING, GEOCODING}, URL https://, credencial BYOK del tenant cifrada en reposo. El cuerpo de la petición nunca devuelve la credencial; PUT con la misma credencial la rota y emite auditoría provider.rotated (23 §8).

9.11 Historial y analítica (E6/E7)

Ver 24_HISTORY_ANALYTICS.md. Todas exigen la política correspondiente del tenant; sin ella devuelven 403 FEATURE_DISABLED.

Operación Scope Notas
POST /v1/sessions/{id}/history dispositivo ingest:write Ingesta de muestras retroactivas (24 §2).
GET /v1/sessions/{id}/trace?from=&to=&simplify_m=&format= history:read Polilínea de la sesión; format ∈ {json, polyline, gsp1}.
GET /v1/sessions/{id}/trips, GET /v1/trips/{trip_id} history:read Viajes derivados.
GET /v1/analytics/heatmap analytics:read Agregado k-anónimo.
GET /v1/analytics/od-matrix analytics:read Pares origen-destino k-anónimos.
GET /v1/analytics/speed-profile analytics:read Percentiles por celda y franja.
GET /v1/analytics/mobility-profile analytics:read Exige consentimiento ANALYTICS del sujeto.
GET /v1/analytics/places analytics:read Lugares aprendidos, k-anónimos.
POST /v1/exports, GET /v1/exports/{id} history:read Exportación asíncrona; la URL de descarga caduca (24 §4).

Toda respuesta analítica incluye k_anonymity_applied y suppressed_cells para que el integrador sepa qué se ocultó, sin revelar los valores suprimidos.

9.12 Dispositivos edge (E8)

Ver 25_EDGE_DEVICES.md. Todas exigen edge_enabled en la política del tenant.

Operación Scope de aplicación Scope de dispositivo Notas
POST /v1/tags, GET /v1/tags, GET /v1/tags/{id}, DELETE /v1/tags/{id} tags:manage tags:manage Registro y baja. El cuerpo nunca acepta ni devuelve la EIK (FR-EDGE-001).
POST /v1/edge/reports ✗ edge:report Reporte cifrado de sightings.
POST /v1/edge/reports/lookup ✗ edge:lookup Consulta de los propios tags; el servidor entrega los blobs sin poder descifrarlos.
POST /v1/edge/lpwan/{integration_id}/webhook ✗ firma HMAC de integración Ingesta LPWAN (25 §4).

POST /v1/edge/reports/lookup responde 429 RATE_LIMITED con el bucket por (dueño, hashed_eid), porque es una operación cara y susceptible de abuso de descubrimiento (FR-EDGE-005).

9.13 Consola de desarrolladores (F15)

Contrato: contracts/openapi/geostream.v1.yaml, etiqueta Console (18 operaciones bajo /v1/console). Decisión: ADR-0033 §3–§4, ADR-0036 y ADR-0037. Autorización: 06 §8.6.

10. Webhooks

10.1 Modelo

Endpoints registrados por tenant (/v1/webhooks). Cada evento de dominio que proyecta a webhook (05 §10) se escribe en el outbox en la misma transacción que el cambio de estado, una fila por endpoint suscrito al tipo. El rol worker entrega.

10.2 Catálogo de tipos

session.created, session.updated, session.closed, session.expired, member.joined, member.updated, member.left, member.revoked, sharing.started, sharing.stopped, geofence.entered (F11), geofence.exited (F11), webhook.test. Esquemas en OpenAPI (webhooks:).

Tipos añadidos por la pista E, todos sin coordenadas (FR-WHK-005):

Tipo Cuándo Qué lleva
geofence.dwell (E5) Permanencia ≥ geofence_dwell_min_seconds geofence_id, member_id, entered_at, dwell_s
track.stop_started, track.stop_ended (E2) Entrada y salida de parada online member_id, stop_index, duration_s; sin centroide
trip.completed (E6) Cierre de viaje trip_id, started_at, ended_at, distance_m, stop_count; sin polilínea
pool.member_online, pool.member_offline (E4) Conexión y desconexión en un pool session_id, member_id
pool.member_availability_changed (E4) Cambio de availability member_id, from, to
edge.tag_registered, edge.tag_removed (E8) Alta y baja de tag tag_id, model; nunca la EIK

Cada tipo lleva en OpenAPI la extensión x-gs-phase con la fase que lo introduce (E2, E4…), de modo que un integrador pueda filtrar por fase sin consultar este documento.

Envoltorio:

{
  "id": "evt_…",
  "type": "member.revoked",
  "created_at": "2026-10-11T21:04:05.123Z",
  "tenant_id": "ten_…",
  "data": { "member": { "…": "…" } }
}

10.3 Firma

10.4 Entrega y reintentos

10.5 Contenido

11. Versionado y deprecación

Compatibilidad de la pista E (regla dura). Todas las operaciones E4–E8 son aditivas bajo /v1 y llevan x-gs-phase en OpenAPI. Ninguna modifica un contrato existente de forma incompatible:

Un cliente escrito antes de la pista E debe seguir funcionando sin cambios contra un servidor que sí la soporta (NFR-COMPAT-003).

12. CORS (F8)

Solo para orígenes en application.allowed_origins. Métodos y cabeceras permitidos: los de esta API. Access-Control-Allow-Credentials: false (la autenticación va en cabecera, no en cookies).

13. Ejemplos (ILUSTRATIVO)

# Backend del integrador: emitir token de dispositivo
curl -s https://geo.example.com/v1/tokens \
  -H "Authorization: Bearer $GS_API_KEY" -H "Content-Type: application/json" \
  -d '{"external_subject_id":"u-48213","installation_id":"3f1c2a7e-9b0d-4c1e-8f2a-6d5b4c3a2e10","platform":"ANDROID"}'

# Backend: crear sesión de viaje con conductor y pasajero
curl -s https://geo.example.com/v1/sessions \
  -H "Authorization: Bearer $GS_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: trip-9921-session" \
  -d '{"mode":"ONE_TO_ONE","expires_in_seconds":7200,
       "initial_members":[{"external_subject_id":"driver-77"},{"external_subject_id":"rider-12"}]}'

# App (token de dispositivo): empezar a compartir
curl -s -X PUT https://geo.example.com/v1/sessions/$SES/members/me/sharing \
  -H "Authorization: Bearer $DEVICE_TOKEN" -H "Content-Type: application/json" \
  -d '{"sharing":true}'