API de Chupilot

Versión 1 · la usa la app Android; no es una API pública.

Los reportes solo se aceptan si vienen de la app original y como máximo uno por hora por instalación y por IP. Para eso cada reporte pasa cuatro filtros, del más barato al más caro:

  1. Reto de un solo uso. La app pide un nonce que vence en 5 minutos y solo sirve para su propia instalación. Un request copiado no se puede repetir.
  2. Firma HMAC. La app firma el método, la ruta, la hora, el nonce, su ID y el hash del cuerpo con una llave compartida. Frena scripts caseros, pero la llave vive dentro del APK: no basta sola.
  3. Límite por hora. Se revisa la última vez que reportaron esa instalación y esa IP (IPv6 agrupada por /64). Se hace antes de llamar a Google para no gastar verificaciones en requests que se van a rechazar.
  4. Play Integrity. La app pide a Google Play un token amarrado a este request (requestHash = SHA-256 de la cadena firmada). El servidor lo descifra con Google y exige app reconocida por Play y dispositivo íntegro.

Al final, una transacción de Firestore quema el nonce, vuelve a revisar el límite y guarda el reporte. Así el límite vale aunque Cloud Run tenga varias instancias o lleguen dos requests al mismo tiempo.

GET /api/v1/challenge

Encabezado X-Chupilot-Install: <uuid v4>. Máximo 30 por IP por hora.

{ "nonce": "…", "expires_in": 300, "integrity": "required" }

POST /api/v1/margarita/report

{
  "tequila": "g4",
  "citric": "alma",
  "limon": "fresco",
  "presentacion": "cristal",
  "lat": 19.4326,
  "lng": -99.1332,
  "place_id": "ChIJ...",        // opcional
  "place_name": "La Pasita"     // opcional
}

Los ingredientes son los IDs de la app (por ejemplo tres_tonos, cointreau_marnier, botella_plastico). El servidor recalcula el puntaje con su propia tabla; no acepta uno del cliente. Respuesta 201: { "ok": true, "score": 100, "verdict": "Gurmee" }.

POST /api/v1/ai/report

{ "text": "respuesta ofensiva…", "screen": "games" }

Mismo protocolo y mismo límite de uno por hora, contado aparte del de margaritas.

POST /api/v1/favorito/report

{ "type": "tequila", "brand": "fortaleza", "aroma": 5, "sabor": 5, "suavidad": 4, "final": 5, "valor": 4 }

Solo se aceptan marcas del catálogo de la app (favoritos.json) y del tipo elegido, y cada cualidad tiene que ser un paso de su slider: un entero del 1 al 5. El servidor calcula el score de 0 a 100. Mismo protocolo y límite de uno por hora, contado aparte.

GET /api/v1/favorito/ranking?type=tequila|mezcal

Público. Marcas ordenadas por promedio bayesiano, con el promedio y la etiqueta de cada cualidad.

GET /api/v1/margarita/bars

Público, sin firma. Parámetros opcionales min_reports y limit. Devuelve los bares ordenados por promedio bayesiano (rank_score): con pocos reportes el promedio se acerca al promedio general, así que un solo 100 no gana. Se cachea 60 segundos.

Encabezados de los reportes

EncabezadoValor
X-Chupilot-InstallUUID v4 generado por la app al instalarse.
X-Chupilot-TimestampSegundos Unix. Se acepta ±5 minutos.
X-Chupilot-NonceEl de /challenge.
X-Chupilot-SignatureHMAC-SHA256 en hex de la cadena canónica.
X-Chupilot-IntegrityToken estándar de Play Integrity.

Cadena canónica, una línea por campo:

POST
/api/v1/margarita/report
1790000000
<nonce>
<install-id>
<sha256 hex del cuerpo>

Errores

Todos responden { "error": "<código>", "message": "…", "retry_after": null }.

HTTPCódigoCuándo
400bad_install_idFalta X-Chupilot-Install o no es un UUID v4.
401unsigned · stale · bad_signature · bad_nonceSin firma, hora desfasada más de 5 min, firma inválida o reto usado o vencido.
403integrityPlay Integrity rechazó el request (solo con INTEGRITY_MODE=required).
413too_largeCuerpo de más de 8 KB.
415json_onlyEl cuerpo no es application/json.
422invalidCampo desconocido, ingrediente o marca que no existe, valor fuera de los sliders o coordenadas fuera de rango.
429rate_limitedYa hubo un reporte en la última hora desde esa instalación o esa IP. Trae Retry-After.