← Volver a la API para desarrolladores

API de Reconocimiento Facial

FR-API: cotejo de rostros, búsqueda de parecidos, deduplicación y conciliación de identidades entre bases de datos. Asistivo — siempre requiere verificación humana.

¿Qué es el FR-API?

El FR-API convierte cada rostro en un vector de 512 dimensiones y los compara por similitud de coseno sobre una base vectorial. Detecta si dos fotos corresponden a la misma persona, aunque provengan de plataformas distintas. Su objetivo es ayudar a cruzar y unificar reportes de personas desaparecidas tras el terremoto de Venezuela 2026.

Es asistivo: cada resultado trae un score de similitud y siempre debe confirmarse por una persona antes de tomar decisiones (fusionar, eliminar, contactar).

Modos de uso

1

Cotejo (check-duplicate)

Subes una foto y el servicio responde si ya existe una persona registrada con ese rostro. Útil al dar de alta un reporte para no crear duplicados.

2

Búsqueda (search)

Subes una foto y obtienes la lista de personas más parecidas ordenadas por score. Asistivo: siempre requiere verificación humana.

3

Deduplicación (duplicates)

Cruzas tu base contra sí misma y obtienes los pares de registros que probablemente sean la misma persona.

4

Conciliación (reconcile)

Encuentra a la misma persona reportada en bases DISTINTAS y trae sus imágenes de cada base, para unificar identidades entre plataformas.

5

Ingesta (index)

Subes tus registros (foto + metadatos) al índice vectorial para que sean buscables y comparables por rostro.

URL Base

https://fr-api.reportavnzla.com:8443
  • Todas las respuestas son JSON. OpenAPI público en https://fr-api.reportavnzla.com:8443/openapi.json.
  • Soporta fotos grandes (decenas de MB). Alias para cargas pequeñas: https://reportavnzla.com/fr-api (límite 4.5 MB/archivo).
  • Rate-limit: 120 peticiones/min por clave.

Autenticación

Las rutas /v1/* requieren API key en el header X-API-Key. Nunca la incluyas en código que corra en el navegador: llama al FR-API desde tu servidor(patrón servidor-a-servidor). Las rutas /health y /openapi.json son públicas.

X-API-Key: TU_API_KEY

Paso 0 — Valida tu configuración (evita el 90% de los problemas)

Antes de integrar, confirma que tu .env está bien. Descarga fr-doctor (script sin dependencias) y córrelo en la carpeta de tu .env — te dice en segundos si la key autentica, si tu FR_SOURCE coincide y si ya indexaste datos:

python fr-doctor.py            # lee tu .env y diagnostica

O a mano, con /v1/whoami:

# Paso 0: ¿tu key sirve? ¿qué source y cuántos indexados tienes?
curl "https://fr-api.reportavnzla.com:8443/v1/whoami" -H "X-API-Key: TU_API_KEY"
# 401 = key inválida/truncada (deben ser 48 caracteres)
# -> { "key_label":"…", "source_default":"…", "indexed_my_source":0,
#      "min_score_default":0.51, "reference_threshold":0.35 }

Importante: el FR-API NO consulta tu base de datos

El FR-API es un índice vectorial centralizado. No se conecta a tu Neon/Supabase/Postgres. Solo encuentra caras que hayas subido con POST /v1/index.

Flujo: tu BD → /v1/index → índice del FR-API → /v1/check-duplicate busca ahí. Si nunca indexaste, check-duplicate siempre dirá “sin duplicado” (no hay con qué comparar). Haz el backfill primero.

El parámetro source: entrada vs. salida

source es la etiqueta de la plataforma de origen de un registro. Significa cosas distintas según el endpoint:

EndpointsourceQué significa
POST /v1/searchsalidaEn cada resultado: de qué base viene ese parecido. No se envía.
POST /v1/check-duplicatesalidaEn cada candidato: de qué base es. No se envía.
POST /v1/indexentrada (opc.)Etiqueta de TU plataforma al subir un registro. Default = la de tu key.
GET /v1/duplicates?source=entradaQué base depurar (pares dentro de UNA base). Sin él = tu propia base.
GET /v1/reconcile?sources=entrada (opc.)Limitar la conciliación a ciertas bases.

Regla simple: comparar una foto (search / check-duplicate) → no mandas source; depurar o conciliar bases → indicas la(s) fuente(s).

Cruzar tu base de datos y detectar duplicados

Dos pasos: (1) subes tus registros al índice una vez, y (2) pides los duplicados. Re-ejecuta el paso 1 al agregar registros nuevos (es idempotente, solo añade lo nuevo).

Paso 1 — Indexar tu base

Un POST /v1/index por persona. Solo entran fotos con rostro (las demás devuelven indexed:false, sin fallar).

# Sube/indexa UN registro de tu base. Idempotente por external_id (tu id).
curl -X POST "https://fr-api.reportavnzla.com:8443/v1/index" \
  -H "X-API-Key: TU_API_KEY" \
  -F "external_id=ID_EN_TU_BD" \
  -F "person_name=Katherine Mendoza" \
  -F "last_seen_location=Catia la Mar" \
  -F "image_url=https://tu-cdn/fotos/123.jpg"   # o  -F "file=@foto.jpg"
# -> {"ok":true,"indexed":true,"record_id":"tu-fuente:ID_EN_TU_BD"}

Recorriendo tu BD (Node.js):

const FR  = "https://fr-api.reportavnzla.com:8443";
const KEY = "TU_API_KEY";
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// 1) trae TUS registros (ajusta a tu BD)
const personas = await db.query(
  "SELECT id, nombre, ubicacion, foto_url FROM personas"
);

// 2) súbelos al índice (idempotente; reanudable)
for (const p of personas) {
  if (!p.foto_url) continue;                  // sin foto no se compara
  const fd = new FormData();
  fd.append("external_id", String(p.id));     // tu id → no duplica
  fd.append("person_name", p.nombre ?? "");
  fd.append("last_seen_location", p.ubicacion ?? "");
  fd.append("image_url", p.foto_url);
  const r = await fetch(FR + "/v1/index",
    { method: "POST", headers: { "X-API-Key": KEY }, body: fd });
  console.log(p.id, (await r.json()).indexed ? "ok" : "sin-rostro");
  await sleep(550);                            // ~110/min < límite 120/min
}

Paso 2 — Pedir los duplicados

Opción rápida: /v1/duplicates te devuelve los pares (con los dos ids de tu BD, el score y las dos fotos para revisar). Pasa el mismo source con que indexaste en el paso 1.

# Cruza TU base por rostro y lista pares duplicados.
# "source" debe ser EL MISMO con que indexaste (paso 1). Por defecto es la
# etiqueta de tu API key; si indexaste con otro source, pásalo igual aquí.
curl "https://fr-api.reportavnzla.com:8443/v1/duplicates?source=TU_FUENTE&min_score=0.6&limit=500" \
  -H "X-API-Key: TU_API_KEY"
{
  "checked": 500,
  "duplicates": [
    { "a": "tu-fuente:12", "b": "tu-fuente:88", "score": 0.94,
      "a_name": "José Pérez",  "b_name": "Jose Perez",
      "a_image": "https://…",  "b_image": "https://…" }
  ]
}

⚠️ /v1/duplicates revisa hasta 500 registros por llamada. Si tu base es mayor y quieres cobertura total con un solo endpoint, escríbenos para subir el límite/añadir paginación — o usa la opción de abajo.

Opción de cobertura total (bases > 500): una búsqueda por registro; de paso detecta si la persona está también en otras plataformas.

// Cobertura TOTAL (bases > 500): una búsqueda por registro.
const UMBRAL = 0.6, vistos = new Set(), duplicados = [];

for (const p of personas) {
  if (!p.foto_url) continue;
  const fd = new FormData();
  fd.append("file", await (await fetch(p.foto_url)).blob());
  const { results = [] } = await (await fetch(FR + "/v1/search",
    { method: "POST", headers: { "X-API-Key": KEY }, body: fd })).json();

  for (const m of results) {
    const yoMismo = m.record_id === "tu-fuente:" + p.id;
    if (yoMismo || m.score < UMBRAL) continue;
    if (m.source === "tu-fuente") {            // duplicado DENTRO de tu base
      const k = [String(p.id), m.record_id].sort().join("|");
      if (!vistos.has(k)) { vistos.add(k); duplicados.push({ a: p.id, b: m.record_id, score: m.score }); }
    }
    // m.source !== "tu-fuente"  → la misma persona en OTRA plataforma
  }
  await sleep(550);
}
console.table(duplicados);   // pares para revisión humana

Paso 3 — Revisar (humano) y limpiar

Empieza por los pares de banda alta. Muestra las dos fotos a un revisor; si confirma, fusionas/eliminas en tu BD. El FR no borra nada por ti.

Endpoints

MétodoRutaAuthParámetrosDescripción
GET/healthpúblicaEstado del servicio y de la colección vectorial.
GET/openapi.jsonpúblicaEsquema OpenAPI público (también en /api/fr/openapi).
GET/v1/whoamiAPI keyAutodiagnóstico: valida tu key y devuelve tu source, cuántos registros tienes indexados y el min_score. Empieza por aquí.
POST/v1/check-duplicateAPI keyfile | image_url, min_score?Foto → ¿ya hay una persona registrada con ese rostro? Acepta file, URL http o data-URI base64; cualquier formato.
POST/v1/searchAPI keyfile | image_url, min_score?Foto → personas más parecidas (top-10) con score y source. Acepta file, URL http o data-URI. Busca en TODAS las bases.
POST/v1/indexAPI keyexternal_id, file|image_url, person_name?, last_seen_location?, age?, contact_phone?, source?Sube/indexa UN registro de tu base. Idempotente por external_id.
POST/v1/index/commitAPI keyCierre de lote (no-op con el backend actual; devuelve el total).
GET/v1/duplicatesAPI keysource?, min_score=0.6, limit=500Cruza UNA base contra sí misma → pares duplicados. source = el mismo con que indexaste (default = etiqueta de tu key).
GET/v1/reconcileAPI keymin_score=0.55, limit=800, sources?Misma persona entre bases DISTINTAS → grupos con las imágenes de cada base.
GET/v1/groupsAPI keylimit=20, offset=0, q?Lista de grupos/clusters de identidad.
GET/v1/groups/{id}/clusterAPI keyMiembros de un grupo/cluster concreto.

Ejemplos (curl)

Cotejo — /v1/check-duplicate

Foto como multipart/form-data en el campo file. 422 = sin rostro (deja registrar igual).

curl -X POST "https://fr-api.reportavnzla.com:8443/v1/check-duplicate" \
  -H "X-API-Key: TU_API_KEY" \
  -F "file=@foto.jpg"

Respuesta:

{
  "ok": true, "faces_detected": 1,
  "possible_duplicate": true, "best_score": 0.97,
  "candidates": [
    { "record_id": "tu-fuente:123", "person_name": "Katherine Mendoza",
      "last_seen_location": "Catia la Mar", "image_url": "https://…",
      "score": 0.97, "band": "alta", "source": "reportavnzla" }
  ]
}

Conciliación — /v1/reconcile

Encuentra a la misma persona reportada en bases distintas y devuelve sus imágenes de cada base. Ajusta min_score y limit; opcional sources=A,B.

# Misma persona en bases distintas (azure ↔ reportavnzla ↔ …)
curl "https://fr-api.reportavnzla.com:8443/v1/reconcile?min_score=0.55&limit=800" \
  -H "X-API-Key: TU_API_KEY"
# opcional: limita a ciertas fuentes con &sources=azure,tu-fuente

Score y bandas

Cada coincidencia trae un score (0–1, similitud de coseno) y una band:

alta ≥ 0.50media 0.35 – 0.50baja < 0.35

Piso de 0.51: /v1/check-duplicate y /v1/search solo devuelven coincidencias con score ≥ 0.51 (trae solo lo más cercano). Puedes ajustarlo por petición con ?min_score=.

Multi-rostro y recuadros (verde / amarillo)

Si la imagen tiene varias personas, se coteja cada rostro. La respuesta de /v1/check-duplicate y /v1/search incluye un arreglo faces con un elemento por rostro detectado, listo para dibujar recuadros: verde si coincide (≥ 0.51), amarillo si no.

{
  "ok": true, "faces_detected": 2, "min_score": 0.51,
  "possible_duplicate": true,
  "faces": [
    { "bbox": [67,117,132,215], "matched": true,  "color": "green",
      "best_score": 0.99, "band": "alta", "candidates": [ /* … */ ] },
    { "bbox": [240,110,300,205], "matched": false, "color": "yellow",
      "best_score": 0.0, "band": null, "candidates": [] }
  ],
  "candidates": [ /* coincidencias agregadas de todos los rostros, score >= 0.51 */ ]
}

bbox = [x1,y1,x2,y2] en píxeles de la imagen enviada. matched/color indican el recuadro; best_score el mejor parecido de ese rostro; candidates sus coincidencias.

Solución de problemas

Registro / búsqueda sin error ni respuesta

Casi siempre la API key está mal copiada (truncada). Corre fr-doctor o GET /v1/whoami: un 401 confirma la key. Las claves son de 48 caracteres. Los proxies no rompen el registro, así que un 401 se ve como “sin coincidencias”.

Siempre da possible_duplicate:false / 0 resultados

Probablemente no has indexado tu base. GET /v1/whoami → indexed_my_source. Si es 0, haz el backfill con POST /v1/index. El FR-API no lee tu BD.

"Me sigue diciendo 0.35"

Es el campo threshold (referencia histórica) — NO controla nada. El piso real es min_score (0.51). Para cambiarlo, pasa ?min_score= en la petición; FR_MIN_SCORE en tu .env no aplica por sí solo.

/v1/duplicates devuelve vacío

Tu source no coincide con el de indexación. Usa el mismo source con que indexaste (= source_default de /v1/whoami = la etiqueta de tu key), o pásalo explícito con ?source=.

HTTP 429

Límite de 120 peticiones/min por clave. En backfills, separa ~550 ms entre llamadas.

Documentación descargable

Importa el esquema OpenAPI en tu cliente (Postman, Insomnia, openapi-generator), lee la guía completa, o entrega la guía para agentes de IA a tu equipo: es un archivo único con código de referencia (proxies, registro, backfill) para que un agente de IA replique la integración. Guárdalo en tu repo como AGENTS.md o docs/FR-API.md.