---
title: "API de oportunidades"
description: "Lista de navegación pública más búsqueda y CRUD con sesión para SerializedOpportunity, incluidos los campos de autor y espectador del feed"
locale: "es"
---
# API de oportunidades

REST para el marketplace de doble naturaleza. La lista de navegación está abierta a los visitantes; todo lo demás requiere sesión. La **lista de navegación** (`GET /api/opportunities`) es una paginación por cursor consciente del rol más enriquecimiento por espectador. La **búsqueda** es una ruta aparte. Like / Save / Hide / Contact son Server Actions, no recursos HTTP.

> **Info**
> Usa las pestañas **Founder** / **Developer** para filtrar esta página. Ciclo de producto y UX de las tarjetas: [Oportunidades](/docs/features/opportunities.md).

  Textos antiguos de la API enumeraban unos "7 tipos" fijos. Los tipos en tiempo de ejecución son la unión `OpportunityType` en `features/opportunities/types/index.ts`, **limitada** por `ring-config.json` → `opportunities.enabledTypes`. No documentes un conjunto cerrado de siete.

## Superficies (compartido)

| Método | Ruta | Autenticación | Notas |
|--------|------|---------------|-------|
| `GET` | `/api/opportunities` | Opcional | `limit`, `startAfter`. Los parámetros de filtro `types`/`type`, `q`/`query`, `categories`/`category`, `location`, `priority` cambian al servicio de búsqueda. Pasa `viewerUserId` cuando hay sesión. Devuelve `{ opportunities, items, lastVisible, cursor, hasMore }` |
| `POST` | `/api/opportunities` | Sesión | Cuerpo `createOpportunityBodySchema` → `{ opportunity }` **201** |
| `GET` | `/api/opportunities/{id}` | Sesión | Un anuncio |
| `PUT` / `PATCH` | `/api/opportunities/{id}` | Sesión | Actualizar |
| `DELETE` | `/api/opportunities/{id}` | Sesión | Eliminar |
| `GET` | `/api/opportunities/search` | Sesión | Requiere `q`. Opcionales `types`, `categories`, `location`, `limit` |
| `POST` | `/api/opportunities/search` | Sesión | Cuerpo JSON `SearchOpportunitiesParams` |
| `POST` | `/api/opportunities/{id}/sign` | Sesión | Firma consultiva de una petición, una por usuario. Devuelve `signatureCount`, `signatureTarget` |
| `GET` | `/api/opportunities/my` | Sesión | Lista del propietario; `view=` `all` \| `drafts` \| `pending` \| `active` \| `archived` |
| `POST` | `/api/opportunities/upload` | Sesión | Adjuntos |
| `GET` / `POST` | `/api/mcp/v1/opportunities` | MCP guard | La lista hoy **no** pasa `viewerUserId` |

Desde 2026-10-17, `GET /api/opportunities` sin autenticar devuelve filas públicas no confidenciales para el rol `visitor`. El filtro de visitante se aplica después de obtener la página, así que una página de invitado puede tener menos de `limit` filas. Detalle, búsqueda y cambios siguen devolviendo **401** sin sesión.

### For founders

## Lo que deben saber los operadores

- **Navegar frente a buscar.** La página `/opportunities` carga `GET /api/opportunities` (una página de anuncios). Los filtros del panel derecho se envían como parámetros de consulta y se aplican en el servidor. Los visitantes pueden navegar sin cuenta.
- **Anuncios ocultos.** Cuando un miembro toca Hide, la siguiente navegación o búsqueda de **esa cuenta** omite el id. Los demás miembros lo siguen viendo.
- **Nombres en las tarjetas.** Las respuestas de lista y búsqueda adjuntan una instantánea `creator` segura para el público, así que la UI nunca muestra un UUID en bruto.
- **Crear en el producto.** El formulario para añadir oportunidades sigue prefiriendo el Server Action `createOpportunity` (`app/_actions/opportunities.ts`). El `POST` REST es para clientes de API y MCP; ambos llaman al mismo servicio.

  
- **[Ciclo de la funcionalidad](/docs/features/opportunities.md)** — Matcher, tipos habilitados, acciones de las tarjetas del feed, My Opportunities.

  
- **[Sincronización del descubrimiento](/docs/architecture/discovery-mutation-sync.md)** — Etiquetas de caché, revalidatePath, Tunnel `opportunity:*` tras crear, actualizar o eliminar.

  
- **[Ejemplos de API](/docs/examples/api-integration.md)** — curl con cookies de sesión en un clon local.

### For developers

## Enriquecimiento de la lista

`GET /api/opportunities` (`app/api/opportunities/route.ts`):

1. `auth()`. Sin sesión el rol es `visitor`, sin enriquecimiento de espectador.
2. Con parámetros de filtro: `searchOpportunities`. Si no, `getOpportunitiesForRole({ userRole, limit, startAfter, viewerUserId })`.
3. Si `viewerUserId` está definido: `getViewerHiddenOpportunityIds` y luego el filtro de DatabaseService `{ field: 'id', operator: 'not-in', value: hiddenIds }`.
4. `attachOpportunityFeedFields` añade `creator`, `viewer`, `likes`.

`searchOpportunities` lee internamente `session.user.id` (o el id del actor MCP); los llamantes **no** pasan `viewerUserId`. La exclusión de ocultos y el adjunto se ejecutan cuando ese id existe.

MCP `GET /api/mcp/v1/opportunities` llama a `getOpportunitiesForRole` **sin** `viewerUserId` (sin filtro de ocultos, sin creator/viewer en esa lista).

### Extras de `SerializedOpportunity` (lista/búsqueda)

{`interface OpportunityCreatorSummary {
  id: string
  name: string
  username?: string  // only when publicProfile
  avatar?: string
}

interface OpportunityViewerFlags {
  liked: boolean
  saved: boolean
  hidden: boolean
}

// on SerializedOpportunity:
creator?: OpportunityCreatorSummary
viewer?: OpportunityViewerFlags
likes?: number           // denormalized count on the JSONB row
createdBy: string        // user id (always)
metadata?: { source?: { kind: 'chat_task'; messageId?: string; conversationId?: string } }`}

Archivo de tipos canónico: `features/opportunities/types/index.ts`. Las fechas son cadenas ISO. Las formas `Timestamp` de Firestore solo existen para `DB_BACKEND_MODE=firebase-full`.

### Cuerpo de creación (obligatorio)

`createOpportunityBodySchema` (`features/opportunities/lib/create-opportunity-schema.ts`): `title`, `briefDescription`, `organizationId`, `category`, `location`, `visibility`, `contactInfo.linkedEntity`, `contactInfo.contactAccount`.

`POST` devuelve **201** `{ opportunity }`. Después, la creación llama a `syncOpportunityDiscovery` con un fragmento que incluye `creator { id, name, avatar }` de la sesión (tarjetas insertadas en vivo).

### Interacciones (no REST)

| Acción | Módulo |
|--------|--------|
| Like | `toggleOpportunityLike` → `like-service` / `likes` |
| Save | `toggleOpportunitySave` → `user_content_interactions` |
| Hide | `markOpportunityNotInterested` → `user_content_interactions` |
| Comentario | `createOpportunityComment` |
| Intención de contacto | `recordOpportunityContactIntent` (Matcher; la UI sigue haciendo `router.push` a `/messages?user=`) |

Archivo: `app/_actions/opportunity-interactions.ts`.

### Mapeo y `not-in`

`mapDbQueryRowToSerializedOpportunity()` en `features/opportunities/lib/opportunity-db-mapper.ts`. **No** uses `lib/converters/opportunity-converter.ts` en Postgres.

El operador `'not-in'` está en `IDatabaseService` `DatabaseFilterOperator`. PostgreSQL: `NOT (field = ANY($n))`. El adaptador de Firebase filtra después.

### Sincronización del descubrimiento tras cambios

Las creaciones, actualizaciones, eliminaciones y cambios de estado llaman a `syncOpportunityDiscovery()`, que:

1. **`revalidateTag`**: invalida las listas de oportunidades de `unstable_cache` (`lib/cached-data.ts`)
2. **`revalidatePath`**: `/[locale]/opportunities`, `/[locale]/opportunities/[id]`, `/[locale]/opportunities/my`
3. **Tunnel**: `opportunity:created|updated|deleted` en el canal `opportunities`

No hay un comando aparte de reindexación de búsqueda. Ver [Discovery mutation sync](/docs/architecture/discovery-mutation-sync.md). La navegación del cliente sigue usando `useCursorFeed` (24 h); `revalidatePath` por sí solo no limpia esa caché.

### Ejemplos

{`const response = await fetch('/api/opportunities?limit=10', { credentials: 'include' })
const { opportunities, lastVisible, hasMore } = await response.json()
// opportunities[i].creator?.name
// opportunities[i].viewer?.saved`}

{`const response = await fetch('/api/opportunities/search?q=react&limit=20', {
  credentials: 'include',
})
const { opportunities, totalCount, searchMetadata } = await response.json()`}

Creación en la UI: `useActionState` + `createOpportunity` de `app/_actions/opportunities.ts`, ver [API integration](/docs/examples/api-integration.md).

## Control de acceso

- **Visibilidad** en el documento: `public` \| `subscriber` \| `member` \| `confidential` \| `entity`. Las filas `entity` solo las pueden leer administradores, el autor y los miembros de la entidad vinculada. Las consultas de lista aplican filtros de visibilidad según el rol (`buildOpportunityVisibilityFilters`).
- Los anuncios **confidenciales** requieren roles con acceso confidencial.
- **Postularse** ocurre dentro de la app (no hay una ruta REST `/apply` dedicada en alpha).

## Preguntas frecuentes

### Impacto

#### ¿Puedo listar oportunidades sin sesión?
Sí, solo filas públicas no confidenciales. `GET /api/opportunities/{id}` y la búsqueda siguen requiriendo sesión.

#### ¿Respeta la búsqueda Hide?
Sí, cuando `searchOpportunities` tiene el id de usuario de la sesión (o del actor MCP). Las rutas HTTP de búsqueda GET/POST requieren sesión.

### Migración

#### ¿Ha desaparecido `createdBy`?
No. Sigue siendo el id de usuario. `creator` es una instantánea pública opcional para la UI.

### Operaciones

#### ¿Por qué las listas MCP siguen mostrando UUID en las tarjetas?
La lista MCP omite `viewerUserId`, así que `attachOpportunityFeedFields` no se aplica en esa ruta.

## Documentación relacionada

  
- [features/opportunities](/docs/features/opportunities.md) — Requisito previo: ciclo de producto, acciones de las tarjetas del feed y límites de tipos habilitados.

  
- [architecture/discovery-mutation-sync](/docs/architecture/discovery-mutation-sync.md) — Depende de: etiquetas de caché, revalidatePath y Tunnel opportunity:* tras los cambios.

  
- [features/tasks](/docs/features/tasks.md) — Mismo flujo: convertir en solicitud escribe metadata.source, que leen las tarjetas de la lista.

  
- [api/entities](/docs/api/entities.md) — Mismo flujo: el vínculo con la organización es obligatorio al crear (`organizationId` / linkedEntity).

  
- [examples/api-integration](/docs/examples/api-integration.md) — Siguiente paso: curl con una cookie de sesión local.

  
- [api/authentication](/docs/api/authentication.md) — Depende de: cookie de sesión requerida para cada ruta de oportunidades.
