Headless Salesforce Feedback Management: cómo usar Salesforce como motor de encuesta y construir el frontend por fuera
Cómo pilotar una encuesta definida en Feedback Management — con páginas, ramificación, lógica condicional, preguntas requeridas e invitaciones — desde un frontend externo en React, Next.js o móvil, sin reimplementar el motor y sin bajar cada SObject a mano. Documento escrito para usted, desde la mirada de un arquitecto técnico en IA y Agentforce.

Resumen ejecutivo
Salesforce Feedback Management expone un juego oficial de APIs REST (Connect REST API para participantes autenticados y una familia unAuth separada sobre el dominio de Omni-Channel Engagement) que permiten iniciar una respuesta, enviar respuestas por página y dejar que Salesforce decida server-side cuál es la siguiente página según las reglas de branching configuradas en Survey Builder. Ese contrato hace posible construir una experiencia visual completamente custom — React, Next.js, móvil o portal — mientras Salesforce sigue siendo dueño de la definición de encuesta, la ramificación, la persistencia en SurveyResponse / SurveyQuestionResponse y las invitaciones. Este documento revisa la superficie oficial de la API, verifica qué está soportado y qué no, compara reconstruir el motor a mano contra usar el API oficial, propone una arquitectura con BFF, y cierra con un POC concreto de cuatro páginas.
Statement técnico
La tesis en una página
Sí — Salesforce Feedback Management puede operarse como un motor de encuesta headless. Existe una superficie oficial de APIs REST (Connect REST API para participantes autenticados, y una familia unAuth Response API separada para participantes públicos) que permite iniciar una respuesta, enviar las respuestas de la página actual, pedir la siguiente página y dejar que Salesforce decida server-side cuál toca según la ramificación configurada en Survey Builder. Esa separación es real y está documentada. Lo que no debe hacer es reconstruir el motor bajando SurveyQuestion, SurveyQuestionChoice y SurveyPage vía SOQL: pierde branching, display logic, versionamiento y persistencia oficial en un solo movimiento.
Este documento está escrito desde la mirada de un arquitecto técnico especializado en integración Salesforce y frontends externos. La audiencia son directores de plataforma, arquitectos de integración y líderes de producto digital que necesitan encuestas dentro de una experiencia propia (web, móvil, portal) sin renunciar a que Salesforce siga siendo el sistema de registro de la voz del cliente. No es una postura comercial: es una postura de ingeniería con verificación explícita contra la documentación oficial vigente.
Parte 0 · Para todos
Sin tecnicismos: la analogía del restaurante
Salesforce mantiene la cocina; usted pone el comedor. La receta, los ingredientes y el chef siguen siendo de Salesforce. Lo que cambia es el envase — no el contenido.
Piense en Feedback Management como un restaurante que normalmente le sienta en su propio comedor: los formularios prefabricados que Salesforce arma por usted. En modo headless, el restaurante cierra el comedor al público pero deja la cocina funcionando. Usted pone su propio comedor — su web, su app móvil, un bot de WhatsApp, un kiosco, lo que sea — y le pide platillos a la cocina a través de una ventanilla (la API). La cocina cocina, valida, guarda y decide qué sigue en el menú. El cliente final se sienta en su espacio, pero come lo que salió de la misma cocina de siempre.
Esa separación es la que hace posible que Salesforce siga siendo el sistema de registro de la voz del cliente aunque la persona nunca vea una pantalla de Salesforce. Reportes, dashboards, automatizaciones y trazabilidad siguen funcionando exactamente igual.
Lo que se queda del lado servidor
El cerebro de la encuesta: reglas de 'si contesta X, salta a Y', preguntas que solo aparecen bajo cierta condición, campos obligatorios y traducciones. La memoria: cada respuesta se guarda en los mismos objetos de siempre. La sesión: Salesforce recuerda en qué página va cada persona. El vínculo con el contexto: la encuesta se puede amarrar al caso, la sesión de WhatsApp, la llamada de voz, la cuenta.
Lo que le toca a usted
Pintar las preguntas con su diseño propio en su canal (web, móvil, WhatsApp, voz). Un intermediario ligero (BFF) que traduzca entre su frontend y Salesforce — no es opcional, es la puerta segura a la cocina. Y dos botones: 'siguiente' y 'anterior'. La navegación real la decide el servidor.
Las limitantes reales — sin endulzar
- Solo dos verbos de navegación: 'Next' y 'Back'. Nada de saltar tres páginas, guardar borrador y volver mañana con la vida resuelta, ni editar una respuesta anterior una vez que avanzó.
- La API oficial no expone todos los tipos de pregunta con contrato público confirmado. Ranking, Slider, Date/DateTime, Picklist y Scoring existen en la plataforma pero no todos tienen esquema publicado en la Business API — hay que validarlos org por org.
- El modo público-anónimo (unAuth) requiere licencia Feedback Management Growth. Sin esa licencia, solo puede encuestar a usuarios ya identificados.
- El navegador no puede llamar directo a Salesforce en modo unAuth por reglas de seguridad de dominio (CORS). Por eso el BFF es obligatorio — no es un lujo arquitectónico, es la puerta.
- La lógica vive del lado servidor. Si Salesforce no le devuelve la pregunta 4, su frontend no la puede adivinar ni forzar. Está atado al orden en que la cocina despacha.
- El momento en que la encuesta termina lo decide Salesforce, no usted. La 'página de gracias' se puede estilizar libremente, pero el punto final del flujo lo marca el servidor.
Parte 1 · Marco
Qué significa 'headless' aquí — y por qué importa
Headless, aplicado a Feedback Management, significa una cosa concreta: Salesforce sigue siendo dueño de la definición y ejecución de la encuesta — encuesta, versión, páginas, preguntas, opciones, ramificación, invitaciones, respuestas — y la aplicación externa es dueña únicamente de la presentación y la experiencia. La aplicación no toma decisiones sobre el flujo de la encuesta; pregunta a Salesforce qué mostrar y le envía lo que el usuario respondió.
Dueño de la lógica y del dato
Survey y SurveyVersion. Pages, Questions y Choices. Page branching logic. Question display logic. Required flags. Merge fields. SurveyInvitation (creación y validación). SurveyResponse y SurveyQuestionResponse (persistencia). Idioma y traducciones. Reglas de fatiga si están configuradas en Feedback Management.
Dueño de la UX y del canal
Look & feel completo. Componentes por tipo de pregunta. Responsive y accesibilidad. Animaciones y transiciones. Persistencia visual de progreso. Analítica de interacción (dwell time, abandono por página). Canal: web propia, app móvil, portal white-label, WebView embebida en app de partner.
Parte 2 · Superficie oficial de la API
Qué expone Salesforce hoy — endpoints, verbos y campos clave
La Salesforce Feedback Management Developer Guide agrupa las APIs de respuesta bajo el nombre 'Business APIs'. Existen tres familias de endpoints y una utilidad de token para el caso público. Todas las URIs, verbos y nombres de campo listados en esta sección están tomados textualmente de la guía oficial vigente.
Familia 1 · Participantes autenticados (Bearer OAuth)
| Recurso Connect REST | Verbos | Uso |
|---|---|---|
| /connect/surveys/{surveyId}/survey-response | POST · PATCH | POST inicia una respuesta creando la invitación server-side con la configuración enviada en el body. PATCH envía las respuestas de la página actual y navega. |
| /connect/surveys/{surveyId}/invitation/{surveyInvitationId}/survey-response | POST · PATCH | Variante que usa una SurveyInvitation ya existente (creada previamente por Flow, invocable o Apex). Útil cuando la invitación se genera en un journey de negocio y el frontend solo la 'consume'. |
| /connect/surveys/{surveyId}/survey-invitation-emails | POST | Envío masivo de invitaciones por email a hasta 300 leads, contacts o users. No es parte del flujo de rendering, pero es la contraparte natural cuando la invitación llega por email. |
Familia 2 · Participantes no autenticados (unAuth Response API)
Esta familia no vive en el dominio estándar de Salesforce. Vive en el dominio de Omni-Channel Engagement — un host separado terminado en salesforce-scrt.com — bajo el prefijo /surveys/v1. Requiere Feedback Management Growth y habilitar explícitamente 'Unauthenticated Survey Participation' en Setup > Survey Settings.
| Recurso unAuth | Verbo | Uso |
|---|---|---|
| <MyDomain>.salesforce-scrt.com/surveys/v1/accessToken | POST | Emite un access token per-org (TTL 3600s) a partir del Organization Id de 18 caracteres. No es OAuth — es un token de aplicación propio de la unAuth API. |
| <MyDomain>.salesforce-scrt.com/surveys/v1/survey-response | POST · PATCH | Mismo patrón POST-inicia / PATCH-navega que la familia autenticada. El body requiere adicionalmente surveyDeveloperName e invitationUuid para reforzar la seguridad de la sesión. |
Los tres campos que sostienen toda la sesión
Cualquiera de las dos familias devuelve al iniciar la respuesta un trío de valores que el cliente debe conservar y echar de vuelta en cada PATCH. Sin ellos el servidor no reconoce la sesión.
- flowInterviewState — string opaco que representa el estado del flow interview server-side (Feedback Management ejecuta cada respuesta como un Flow interview). El cliente no lo interpreta; solo lo re-envía.
- invitationId — el Id de la SurveyInvitation asociada a esta respuesta (autenticada) o generada por la unAuth API (pública).
- invitationUuid — un identificador aleatorio adicional que la unAuth API exige en cada PATCH como refuerzo de seguridad. Solo aplica al camino público.
navigationAction — el enum verdadero
Parte 3 · Flujo end-to-end
Cómo se ve una respuesta desde el primer clic hasta la Thank You
Este es el flujo canónico para una encuesta multipágina con branching, usando el endpoint autenticado con invitación existente. El unAuth es idéntico en forma; solo cambia el host, el token y dos campos adicionales en el body (invitationUuid y surveyDeveloperName).
┌────────────────────────────────────────────────────────────────────┐
│ Frontend (React / Next.js / móvil) │
└───────────────────────┬────────────────────────────────────────────┘
│ 1. POST /survey-response (start)
▼
┌────────────────────────────────────────────────────────────────────┐
│ Salesforce ← devuelve Survey Description Output: │
│ · responseId, invitationId, flowInterviewState │
│ · surveyPage = Página 1 (label, name, surveyQuestions[]) │
│ · navigationActions = ["Next"] │
└───────────────────────┬────────────────────────────────────────────┘
│ 2. Renderiza Página 1 · usuario responde
│
│ 3. PATCH /survey-response (Next)
│ body: { navigationAction: "Next", │
│ surveyPageResponses: {...} } │
▼
┌────────────────────────────────────────────────────────────────────┐
│ Salesforce evalúa branching server-side según la respuesta │
│ ← devuelve Survey Response Output: │
│ · flowInterviewState (nuevo) │
│ · surveyPage = Página 2 O Página 5 (según branching) │
│ · navigationActions = ["Next", "Back"] │
└───────────────────────┬────────────────────────────────────────────┘
│ 4. Repite PATCH por cada página
│
│ 5. PATCH última página · Next
▼
┌────────────────────────────────────────────────────────────────────┐
│ Salesforce ← devuelve Survey Response Output con: │
│ · surveyPage = Survey Thank You Page Output │
│ (thankYouMessage, messageDescription, redirectUrl, urlButtons)│
│ · navigationActions = [] │
│ Persistencia: │
│ · SurveyResponse.Status → Completed │
│ · SurveyQuestionResponse por cada pregunta contestada │
└────────────────────────────────────────────────────────────────────┘
1 · POST inicial (start)
POST /services/data/v57.0/connect/surveys/0Kdxx0000000H46CAE/invitation/0Kixx0000004EsOCAU/survey-response
Host: yourInstance.my.salesforce.com
Authorization: Bearer <access_token>
Content-Type: application/json
{}
{
"status": "Success",
"responseId": "0Myxx0000004CYqCAM",
"invitationId": "0Kixx0000004EsOCAU",
"flowInterviewState": "state1",
"languageCode": "en",
"navigationActions": ["Next"],
"surveyDetail": {
"label": "Post-purchase feedback",
"name": "Post_purchase_feedback",
"versionNumber": 3,
"surveyPage": {
"label": "Página 1",
"name": "p_d84a...",
"surveyQuestions": [
{
"name": "q_856a...",
"label": "¿Actualmente eres cliente?",
"questionType": "RadioButton",
"isResponseRequired": true,
"questionChoices": [
{ "name": "c_yes", "label": "Sí" },
{ "name": "c_no", "label": "No" }
]
}
]
}
},
"errors": [],
"warnings": []
}
2 · PATCH para navegar a la siguiente página
PATCH /services/data/v57.0/connect/surveys/0Kdxx.../invitation/0Kixx.../survey-response
Authorization: Bearer <access_token>
Content-Type: application/json
{
"flowInterviewState": "state1",
"invitationId": "0Kixx0000004EsOCAU",
"languageCode": "en",
"navigationAction": "Next",
"surveyPageResponses": {
"questionResponses": [
{
"name": "q_856a...",
"questionType": "RadioButton",
"responses": [ { "name": "c_yes" } ]
}
]
}
}
{
"status": "Success",
"responseId": "0Myxx0000004CYqCAM",
"invitationId": "0Kixx0000004EsOCAU",
"flowInterviewState": "state2",
"languageCode": "en",
"navigationActions": ["Next", "Back"],
"surveyPage": {
"label": "Página 2 — datos de tu experiencia",
"name": "p_c73e...",
"surveyQuestions": [
{
"name": "q_ab12...",
"label": "¿Qué productos usaste esta semana?",
"questionType": "MultiChoice",
"isResponseRequired": true,
"questionChoices": [
{ "name": "c_p1", "label": "Producto A" },
{ "name": "c_p2", "label": "Producto B" },
{ "name": "c_p3", "label": "Producto C" }
]
},
{
"name": "q_cd34...",
"label": "Del 0 al 10, ¿qué tan probable es que nos recomiendes?",
"questionType": "NPS",
"isResponseRequired": true,
"minScale": 0,
"maxScale": 10
}
]
},
"errors": []
}
3 · PATCH final — Salesforce responde con la Thank You page
{
"status": "Success",
"responseId": "0Myxx0000004CYqCAM",
"flowInterviewState": "final",
"navigationActions": [],
"surveyPage": {
"label": "Gracias",
"name": "p_thanks",
"thankYouMessage": "Gracias por tu tiempo.",
"messageDescription": "Tu opinión fue registrada correctamente.",
"redirectUrl": "https://miempresa.com/gracias",
"urlButtons": [
{ "label": "Volver al inicio", "url": "https://miempresa.com" }
]
}
}
Parte 4 · Lógica condicional
Page branching vs question display logic — qué evalúa Salesforce y qué le toca al frontend
Esta es la parte donde se juega la propuesta técnica completa. La pregunta operativa es: ¿puedo evitar reimplementar reglas condicionales en React? La respuesta corta es sí para page branching (confirmado por la forma del contrato) y muy probablemente sí para display logic, aunque este último no está confirmado verbatim en la documentación pública.
Regla: 'si Q1 = Sí, ir a Página 2; si no, ir a Página 5'
Se configura en Survey Builder a nivel página. Cambia qué página aparece a continuación en función de las respuestas ya enviadas. Es una decisión de flujo — determina el orden de navegación.
Regla: 'muestra Q3 solo si Q2 = A o B'
Se configura a nivel pregunta. No cambia la página siguiente — cambia qué preguntas dentro de la página actual son visibles. Es una decisión de visibilidad — determina qué se ve en una misma pantalla.
Page branching — resuelto server-side (confirmado por contrato)
La forma del contrato lo hace obvio: la respuesta de PATCH contiene un surveyPage con label, name y surveyQuestions[] — pero no contiene reglas, ni un nextPage lookup, ni metadata condicional. El frontend no tiene la información necesaria para decidir la siguiente página. Y aún así, al enviar el PATCH con navigationAction: Next, el servidor devuelve la página correcta. La única forma en que eso funciona es que Salesforce evalúa la ramificación del lado del servidor. La feature table oficial confirma explícitamente que 'Apply branching logic' y 'Page branching logic based on merge fields' están en las tres licencias (Response Pack, Starter y Growth).
Question display logic — muy probablemente server-side (requiere validación)
El schema oficial de Survey Question Output expone exactamente seis campos: description, isResponseRequired, label, name, questionType y responseDataType. No hay un campo dependsOn, displayIf ni visibility. Eso deja dos hipótesis: (a) Salesforce filtra server-side las preguntas ocultas y solo devuelve las visibles en surveyQuestions[], o (b) la lógica de display no está soportada vía API y solo funciona en el renderer nativo de Salesforce. La evidencia circunstancial — que la feature aparece publicitada como 'Dynamic Surveys' en las licencias Starter y Growth y que el schema no expone reglas al cliente — sugiere fuertemente la opción (a), pero la documentación pública no lo confirma con una frase textual.
Lo que sí queda del lado del frontend, siempre
- Validación de required en la UI antes de mandar PATCH — para no incurrir en un 400 evitable. El campo isResponseRequired ya viene en cada Survey Question Output.
- Formato local: máscaras de texto, orden de opciones en radio, límite de caracteres visual, contadores. Nada de esto llega desde la API — es UX pura.
- Estado transicional entre PATCHs: spinner, progreso, animación de página. La API no maneja transitions.
- Persistencia optimista del último borrador local antes de mandar el PATCH — por si hay pérdida de red. Independiente del partial-save server-side de v63.0+.
- Analítica de interacción: dwell time, número de cambios de respuesta, abandono por pregunta. Nada de esto va a Salesforce a menos que usted lo mande explícito.
Parte 5 · Frontend
Cómo se ve la arquitectura de componentes del cliente
El árbol de componentes es sencillo porque el motor no está en el frontend. Solo hay un contenedor que orquesta el estado de sesión, un renderer de página que itera por surveyQuestions[] y un dispatcher por tipo de pregunta. Ese es todo el shape.
SurveyContainer
│ · guarda { invitationId, invitationUuid?, flowInterviewState, responseId }
│ · maneja fetch a start / next / back
│ · decide si render <SurveyPage /> o <ThankYouPage />
│
├── SurveyPage
│ │ · recibe surveyQuestions[]
│ │ · valida required antes de habilitar 'Next'
│ │ · muestra 'Back' cuando navigationActions incluye "Back"
│ │
│ └── QuestionRenderer (por cada pregunta)
│ │ switch(questionType)
│ │
│ ├── <SingleChoice /> questionType = "RadioButton"
│ ├── <MultiChoice /> questionType = "MultiChoice"
│ ├── <BooleanYesNo /> questionType = "Boolean"
│ ├── <Rating /> questionType = "Rating"
│ ├── <NPS /> questionType = "NPS"
│ ├── <TextInput /> questionType = "ShortText"
│ ├── <TextArea /> questionType = "FreeText"
│ └── <Unsupported /> fallback + telemetría
│
└── ThankYouPage
· label · thankYouMessage · messageDescription · redirectUrl · urlButtons[]
Dispatcher por tipo de pregunta (TypeScript)
type Question = {
name: string;
label: string;
questionType:
| "RadioButton"
| "MultiChoice"
| "Boolean"
| "Rating"
| "NPS"
| "ShortText"
| "FreeText";
isResponseRequired: boolean;
questionChoices?: { name: string; label: string }[];
minScale?: number;
maxScale?: number;
};
export function QuestionRenderer({ question, value, onChange }: Props) {
switch (question.questionType) {
case "RadioButton":
return <SingleChoice question={question} value={value} onChange={onChange} />;
case "MultiChoice":
return <MultiChoice question={question} value={value} onChange={onChange} />;
case "Boolean":
return <BooleanYesNo question={question} value={value} onChange={onChange} />;
case "Rating":
return <Rating question={question} value={value} onChange={onChange} />;
case "NPS":
return <NPS question={question} value={value} onChange={onChange} />;
case "ShortText":
return <TextInput question={question} value={value} onChange={onChange} />;
case "FreeText":
return <TextArea question={question} value={value} onChange={onChange} />;
default:
// Nunca falle silencioso: logueé el tipo y muestre un placeholder claro.
return <Unsupported type={question.questionType} />;
}
}
Contrato uniforme de respuesta hacia arriba
Cada componente hoja emite el mismo shape hacia arriba: un objeto con name (nombre API de la pregunta), questionType (para que el mapper reconstruya el questionResponse correcto) y ya sea responses[] (array de choice names) para preguntas de selección o responseValue (string o entero) para texto y NPS. Ese mapper es la única pieza no trivial del cliente — y no tiene reglas, solo forma.
export function buildSurveyPageResponses(state: Record<string, any>, questions: Question[]) {
return {
questionResponses: questions.map((q) => {
const raw = state[q.name];
switch (q.questionType) {
case "RadioButton":
case "Boolean":
case "Rating":
return { name: q.name, questionType: q.questionType, responses: [{ name: raw }] };
case "MultiChoice":
return {
name: q.name,
questionType: q.questionType,
responses: (raw as string[]).map((n) => ({ name: n })),
};
case "NPS":
return { name: q.name, questionType: q.questionType, responseValue: Number(raw) };
case "ShortText":
case "FreeText":
return { name: q.name, questionType: q.questionType, responseValue: String(raw) };
}
}),
};
}
Parte 6 · Participantes
Autenticado vs no autenticado — dos APIs, dos hosts, dos contratos
La decisión entre autenticado y no autenticado no es cosmética. Cambia la API que usa, el host donde vive, cómo obtiene el token, qué edición de Feedback Management necesita y qué configuración de sharing debe existir en la org. Esta tabla resume las diferencias reales.
| Dimensión | Autenticado | No autenticado (unAuth) |
|---|---|---|
| Host | yourInstance.my.salesforce.com | yourInstance.my.salesforce-scrt.com (Omni-Channel Engagement URL) |
| Prefijo | /services/data/vXX.0/connect/surveys/… | /surveys/v1/… |
| Autenticación | OAuth 2.0 estándar — Authorization: Bearer <access_token> | Access token per-org emitido por /surveys/v1/accessToken usando Organization Id · TTL 3600s · header Authorization: Bearer <accessToken> |
| Identidad del participante | Debe ser un User (interno o Experience Cloud) o vincularse a Contact / Lead vía invitación | No requiere identidad Salesforce. La invitación se genera contra un Contact o Lead (los User no se soportan en unAuth) |
| Edición mínima requerida | Feedback Management Starter o Growth | Feedback Management Growth (obligatorio) |
| Body extra requerido | flowInterviewState, invitationId, navigationAction, surveyPageResponses, languageCode? | Los mismos + invitationUuid + surveyDeveloperName |
| Sharing / configuración | Perfiles y permisos estándar del User o Experience Cloud site | Encuesta compartida al perfil Guest User · sharing de merge-field records · Setup > Survey Settings > Unauthenticated Survey Participation habilitado |
| Rate limits documentados | 5,400 POST/min · 2,700 PATCH/min a nivel org | 3,600 POST/min · 1,800 PATCH/min a nivel org |
| Content-Type aceptado | application/json | application/json (verbatim: 'only accepts Content-Type: application/json') |
Cuál elegir en cada caso
Cuando el participante ya es alguien conocido
Portal de clientes con login, App móvil con sesión, empleado en una intranet, agente en el Service Console, contacto que abre un link de invitación con el token asociado a su SurveyInvitation. La invitación puede haber sido generada por Flow, invocable, journey de Marketing Cloud o llamada directa al endpoint POST survey-invitation-emails.
Cuando la encuesta se sirve en abierto
Landing pública, encuesta post-checkout embebida en el sitio corporativo, QR en tienda física, campaña de captura de leads con incentivo, encuesta de investigación de mercado. El participante llega sin login, y aún así la respuesta debe quedar asociada a un Contact o Lead (nunca a un User, restricción explícita).
Parte 7 · Integración
¿Llamar Salesforce directo desde el browser, o pasar por un BFF?
La pregunta se responde sola en cuanto se listan los requisitos reales. Cualquier llamada al camino autenticado exige un access token OAuth — ese token no puede vivir en el bundle de JavaScript sin que un atacante lo cosecha en la primera visita. En el camino unAuth, el token es 'per-org' — filtrarlo compromete todas las encuestas públicas de la org. Y encima hay dos hosts distintos (salesforce.com y salesforce-scrt.com) sin evidencia oficial de whitelist CORS para un third-party origin. La conclusión es directa: pase por un BFF.
Antipatrón
OAuth token expuesto en el cliente. Access token unAuth expuesto en el cliente. CORS resuelto ad-hoc con extensiones o proxies frágiles. Log de errores del cliente en consola del usuario final. Rotación de credenciales imposible sin redeploy del bundle. Auditoría distribuida entre origins que no controlas.
Camino recomendado
El BFF guarda el client_id / client_secret / refresh_token en variables de entorno del servidor. Emite tokens de sesión cortos hacia el navegador con solo lo mínimo necesario. Concentra retries, rate limiting, logging estructurado y feature flags. Aísla al cliente del host de Salesforce y evita CORS de raíz.
Superficie mínima del BFF
POST /api/surveys/{surveyDeveloperName}/start
├─ crea la invitación (autenticada o unAuth según el flujo)
├─ traduce a Salesforce y guarda { invitationId, invitationUuid?, flowInterviewState }
└─ devuelve al cliente { pageId, questions[], canGoBack, canGoNext }
PATCH /api/surveys/{surveyDeveloperName}/next
├─ recibe answers[] del cliente
├─ arma el surveyPageResponses.questionResponses[] correcto
├─ PATCH a Salesforce con navigationAction: "Next"
└─ devuelve { pageId, questions[], canGoBack, canGoNext } · O { finished: true, thankYou }
PATCH /api/surveys/{surveyDeveloperName}/back
└─ igual que /next pero con navigationAction: "Back"
GET /api/surveys/{surveyDeveloperName}/state
└─ (opcional) devuelve la página actual sin navegar — útil tras refresh del browser
Qué NO debe hacer el BFF
- No debe reinterpretar branching ni display logic. Su rol es fielmente pasar respuestas y devolver páginas.
- No debe cachear el schema de la encuesta indefinidamente. La SurveyVersion activa puede cambiar; una versión cacheada rompe la respuesta en vuelo si el usuario tarda entre pageviews.
- No debe traducir preguntas. Salesforce ya lo hace por languageCode — el BFF solo forwardea.
- No debe agregar validación de negocio propia (ej: aceptar o rechazar una respuesta según reglas propias). Si necesita eso, es porque la encuesta está mal diseñada — corríjala en Survey Builder.
- No debe persistir respuestas fuera de Salesforce. La verdad vive en SurveyResponse. Si necesita observabilidad, envíe eventos de journey (start / page-completed / abandon) a su plataforma de analítica, no las respuestas mismas.
Parte 8 · Decisión clave
Bajar los SObjects a mano vs. usar la API oficial — comparación honesta
Casi cualquier equipo que ataca este problema por primera vez propone la Opción A (bajar SurveyQuestion / SurveyQuestionChoice / SurveyPage con SOQL o Objects REST y armar el motor en el frontend). Es una tentación entendible porque el modelo de datos es transparente y las queries salen rápidas en el Developer Console. La Opción B (usar la Business API — Survey Response Connect API y su gemela unAuth) parece 'menos flexible' porque el frontend recibe una página a la vez. Esta comparación es lo que necesita ver alguien antes de decidir cuál camino toma.
| Dimensión | Opción A · Reconstruir con SObjects | Opción B · Business API oficial |
|---|---|---|
| Branching | Debe reimplementar toda la lógica de ramificación en TypeScript, replicando lo que hace Survey Builder. Cualquier cambio en el builder requiere sincronizar código. | Salesforce evalúa server-side. El frontend nunca ve la regla — recibe la página correcta. |
| Display logic | Debe interpretar reglas condicionales de visibilidad. La API de Objects no expone estas reglas de forma limpia. | Muy probablemente resuelto server-side (verificar en POC). Su código no lo toca. |
| Versionamiento | Cada publish de una nueva SurveyVersion puede romper el mapping. Debe manejar la migración de respuestas parciales. | Cada invitación queda anclada a una SurveyVersion. Salesforce garantiza la coherencia de la sesión. |
| Merge fields | No los ejecuta — necesita re-implementar el motor de merge y traer los records referenciados desde Salesforce. | Salesforce entrega el label de la pregunta ya renderizado con merge fields resueltos. |
| Persistencia | Debe crear SurveyResponse y SurveyQuestionResponse a mano via DML o Composite API. Fácil equivocarse en el shape. | Salesforce persiste automáticamente. Fila por respuesta, fila por pregunta contestada. |
| Invitaciones | Debe generar SurveyInvitation, calcular UUID, validar expiración y unicidad. | Salesforce crea la invitación en el POST inicial (o consume una existente creada por Flow / invocable). |
| Idiomas | Debe traer traducciones desde el objeto TranslationValue asociado y aplicarlas en cliente. | El endpoint respeta languageCode y devuelve labels traducidos si la traducción existe en la encuesta. |
| Partial-save / resume | Debe diseñarlo. No hay 'PartiallyCompleted' semántico si no lo emula. | shouldLoadPartiallyCmplSurvey (v63.0+) más SurveyResponse.Status = PartiallyCompleted lo hacen nativo. |
| Seguridad de participantes públicos | El equipo diseña autenticación y anti-abuso desde cero. | unAuth Response API + Sharing Guest User + token TTL 3600s ya está pensado. |
| Esfuerzo inicial | Alto. Fácilmente 6–10 semanas de ingeniería + QA para llegar a paridad funcional con el motor nativo. | Bajo. Un sprint alcanza para un POC bien hecho. |
| Mantenimiento | Alto. Cada feature nueva en Feedback Management no llega gratis. | Bajo. Nuevas capacidades del motor (por ejemplo, merge fields nuevos) llegan sin código adicional. |
| Flexibilidad de UX | Máxima. Puede reordenar preguntas de una página, romper con la estructura de páginas del builder, dividir preguntas entre pantallas. | Alta pero acotada al contrato: una página por request, orden respetado por el server. Puede estilizar libremente cada pregunta. |
| Dependencia de Salesforce | Alta pero implícita — cada vez que el builder cambia, el código debe adaptarse. | Alta y explícita — Salesforce es el motor y el sistema de registro. Contrato bien definido. |
Parte 9 · Persistencia
Dónde terminan las respuestas — y por qué eso importa
Usar la Business API oficial no rompe el modelo de datos estándar. Todo lo que se envíe por PATCH termina en los mismos objetos que popularía una respuesta enviada por el renderer nativo de Salesforce. Esa es una de las razones por las que la Opción B es tan valiosa: no se está construyendo un silo — se está alimentando el sistema de registro.
Survey · plantilla del cuestionario
│
└── SurveyVersion · versión publicada (cada publish crea una nueva)
│
├── SurveyPage · páginas de esa versión
│ │
│ └── SurveyQuestion · preguntas de esa página
│ │
│ └── SurveyQuestionChoice
│
└── SurveyInvitation · invitación a un Contact / Lead / User específico
│
└── SurveyResponse · una fila por respuesta
│
└── SurveyQuestionResponse · una fila por pregunta contestada
Consecuencias de que la persistencia sea la estándar
- Los dashboards de Customer Lifecycle Analytics siguen funcionando sin cambios — se alimentan de los mismos objetos.
- Data Mapper puede seguir disparando acciones al recibir SurveyQuestionResponse (por ejemplo: 'si NPS < 6, abre un Case y asigna al gerente de cuenta').
- Las Intelligent Survey Reminders siguen operando sobre SurveyInvitation.
- Reportes y list views existentes no requieren migración.
- La integración a Data Cloud vía Data Mapper y las políticas de retention estándar funcionan igual.
- Cualquier automatización (Flow, Trigger, Process, invocable) que ya escuche cambios en SurveyResponse sigue viva.
Parte 10 · Arquitectura final
Vista lógica de una solución headless de extremo a extremo
Esta es la arquitectura de referencia. Muestra roles y fronteras de responsabilidad — no productos puntuales. Cada capa es explícita sobre qué le toca hacer y qué no debe intentar hacer.
┌────────────────────────────────────────────────────────────────────────────────┐
│ SALESFORCE │
│ (Motor de encuesta · Sistema de registro) │
│ │
│ Survey Builder / Feedback Management │
│ │ │
│ Survey → SurveyVersion → SurveyPage → SurveyQuestion → Choice │
│ │ │
│ Page branching · Question display logic │
│ Required flags · Merge fields · Traducciones │
│ │ │
│ SurveyInvitation │
│ │ │
│ Business API (Connect REST) │
│ /connect/surveys/…/survey-response │
│ salesforce-scrt.com/surveys/v1/… │
│ │ │
│ SurveyResponse · SurveyQuestionResponse │
│ │ │
│ Data Mapper → Case, Task, Flow, Data Cloud │
└────────────────────────────────────┬───────────────────────────────────────────┘
│ POST /survey-response (start)
│ PATCH /survey-response (Next / Back)
▼
┌────────────────────────────────────────────────────────────────────────────────┐
│ BACKEND / BFF │
│ (Broker seguro · Sesión · Observabilidad) │
│ │
│ OAuth 2.0 client-credentials / JWT bearer · Token unAuth per-org │
│ Sesión server-side (httpOnly cookie + Redis) con: │
│ invitationId · invitationUuid · flowInterviewState │
│ │
│ Endpoints públicos hacia el cliente: │
│ POST /api/surveys/{name}/start │
│ PATCH /api/surveys/{name}/next │
│ PATCH /api/surveys/{name}/back │
│ GET /api/surveys/{name}/state │
│ │
│ Responsabilidades: normalización · retries · rate limit · logging │
│ Prohibido: reinterpretar branching · cachear schema · validar negocio │
└────────────────────────────────────┬───────────────────────────────────────────┘
│ Payload normalizado (page + questions)
▼
┌────────────────────────────────────────────────────────────────────────────────┐
│ CUSTOM FRONTEND │
│ (Look & feel · UX · Canal) │
│ │
│ SurveyContainer │
│ ├── SurveyPage │
│ │ └── QuestionRenderer │
│ │ ├── SingleChoice MultiChoice BooleanYesNo │
│ │ ├── Rating NPS │
│ │ └── TextInput TextArea (Unsupported fallback) │
│ └── ThankYouPage │
│ │
│ Responsabilidades: rendering · validación required en UI · a11y · i18n │
│ Prohibido: mantener el mapa de branching · mutar SurveyQuestion │
└────────────────────────────────────────────────────────────────────────────────┘
Parte 11 · Riesgos y limitaciones
Lo que la documentación oficial soporta — y lo que no
Ningún approach técnico es completo sin sus zonas grises. Esta tabla clasifica cada aspecto contra la documentación oficial vigente y los hallazgos empíricos del POC de campo. Los estados usan cuatro niveles: soportado (afirmación oficial verificada o probada), parcialmente soportado (docs contienen matices que hay que respetar), requiere validación (evidencia circunstancial pero sin confirmación explícita), no soportado (contradice la documentación o excede el contrato).
| Aspecto | Estado | Comentario |
|---|---|---|
| Tipos: RadioButton, MultiChoice, Boolean, Rating, NPS, ShortText, FreeText | Soportado | Documentados con ejemplos de request y response en la Feedback Management Developer Guide. |
| Tipos: Date, DateTime, Ranking, Slider, Scoring, Picklist | Requiere validación | Existen en el objeto SurveyQuestionScore, pero no hay ejemplos de Business API para estos tipos. Validar en POC si aparecen en surveyQuestions[] o si el server los omite. |
| Matrix questions | Requiere validación | Disponibles en Starter y Growth según feature table, pero sin schema Connect REST publicado. |
| Attachment questions | Requiere validación | Disponibles en Starter y Growth, pero el upload de archivos vía Business API no está documentado en los mismos ejemplos. |
| Page branching logic | Parcialmente soportado | Server evalúa server-side EN Standard Survey. Pero: Standard es rechazado por la unAuth API — el path unAuth solo acepta Basic, y Basic explícitamente NO soporta page branching (verbatim en Object Reference). Vía: usar path autenticado con Standard, o esperar activación de Conversational Survey. |
| Question display logic | Parcialmente soportado | Igual que branching: Standard lo soporta, Basic no. La Object Reference lista display logic entre lo que Basic excluye explícitamente. En path autenticado con Standard debería funcionar server-side (schema del cliente no expone reglas — inferencia fuerte). |
| SurveyType compatible con unAuth API | Parcialmente soportado | unAuth Response API responde 'Specify a basic or conversational survey' — solo Basic y Conversational aceptados. Standard rechazado. Conversational no está en docs públicas ni en el picklist SurveyType estándar (parece pilot/EA). El path autenticado sí acepta Standard. |
| surveyPageResponses.name en PATCH | Soportado — pero doc engañoso | El campo aparece en docs como 'Reserved for future use' pero en la práctica es REQUERIDO en cada PATCH. Debe llevar el nombre de la página actual (el 'name' que devolvió el surveyPage previo). Confirmado empíricamente en el POC. |
| questionResponses[] count enforcement | Soportado | El array debe tener una entrada por cada pregunta de la página, incluso las no contestadas. Para no contestadas: 'responses: []' en selection types o omitir 'responseValue' en NPS/Text. Salesforce rechaza con 'Specify the same number of questions in the survey and the input representation'. |
| Sesión completa en cookie httpOnly | No soportado | El flowInterviewState pesa ~3.8 KB y la sesión JSON serializada (+ base64 + firma HMAC + gzip) queda ~4.8 KB — por encima del límite ~4096 bytes del browser, que rechaza la cookie silenciosamente. Diseño obligado: session store server-side, cookie solo lleva un ID corto. |
| Labels/choices HTML-encoded | Soportado | Salesforce Survey Builder devuelve labels con entities HTML ('Tecnología' vs 'Tecnología'). El BFF debe decodificarlos server-side antes de mandar al cliente. No es dificultad técnica pero sí sorpresa. |
| Required questions | Soportado | Campo isResponseRequired disponible en cada Survey Question Output. |
| Back navigation | Soportado | navigationAction: 'Back' documentado; el server devuelve la página previa con la misma sesión. |
| Save & resume | Soportado (v63.0+) | shouldLoadPartiallyCmplSurvey: true en POST + SurveyResponse.Status = PartiallyCompleted. Requiere Starter o Growth. |
| Merge fields en labels y branching | Soportado | Se resuelven server-side. En unAuth, los records referenciados deben estar compartidos al Guest User. |
| SurveyVersion switching en vuelo | No soportado | La respuesta queda anclada a la versión activa al momento del POST inicial. Cambios de publish en curso no afectan respuestas activas. |
| Autenticado OAuth | Soportado | Bearer estándar contra el My Domain de Salesforce. Client credentials, JWT bearer o auth code según su patrón de integración. |
| Unauth participants | Soportado (Growth) | Endpoint separado en salesforce-scrt.com/surveys/v1 con access token per-org. |
| CORS directo desde browser | Requiere validación | No documentación de CORS whitelist para orígenes externos hacia salesforce-scrt.com. Un BFF elimina el problema. |
| Content-Type distinto a JSON | No soportado | Doc oficial verbatim: 'only accepts Content-Type: application/json'. |
| Licencia 'Survey Response Pack' base | No soportado | La feature table confirma que 'basic survey via Survey Response Connect API' es No para Response Pack — necesita Starter o Growth. |
| Rate limits nivel org | Soportado | 5,400 POST/min y 2,700 PATCH/min autenticado; 3,600 / 1,800 unAuth. Diseñe backoff en el BFF. |
| Persistencia en SurveyResponse / SurveyQuestionResponse | Soportado | Doc verbatim: 'The survey responses are stored in the SurveyResponse object and the responses to individual questions in the SurveyQuestionResponse object.' |
| SurveyPageResponse como objeto persistido | No soportado | No existe como objeto estándar. Es solo forma de payload en la API. |
| Envío masivo por email desde la API | Soportado | POST /connect/surveys/{surveyId}/survey-invitation-emails a hasta 300 recipients. |
| Recipient tipo User en el flujo unAuth | No soportado | unAuth Response API acepta únicamente Contact o Lead como recipientId. |
Parte 12 · Recomendación · POC comprobado
Qué implementaría yo — y el POC que efectivamente se comprobó
Sí — Salesforce Feedback Management puede operar como un motor de encuesta headless. La respuesta es Opción B: use la Business API oficial (Connect REST autenticada o unAuth Response API según el tipo de participante), interpónga un BFF, deje al frontend hacer solo rendering y validación de UI. Ese patrón produce una experiencia visual completamente propia mientras Salesforce sigue siendo dueño de la definición y la persistencia. Es sostenible, versionable y no requiere reimplementar el motor. Con un matiz que solo salió en el POC: por el path unAuth solo funciona Basic Survey — que no soporta page branching ni question display logic. Si necesita esos dos features, va por el path autenticado con Standard.
Dos POCs — uno comprobado, uno de referencia
Basic Survey lineal · web sin login
5 páginas · 10 preguntas · 7 tipos · flujo lineal · sin branching · sin display logic. Persiste en SurveyResponse + SurveyQuestionResponse. Recorre POST /accessToken → POST /survey-response → PATCH loop → Thank You Page. Es el que sirve para web pública anónima. Implementado y publicado como receta en el portfolio.
Standard Survey con branching + display logic
Diseño ideal de 4-6 páginas con dos rutas por Q1 y display logic Q → Q. Salesforce evalúa branching server-side. Requiere path autenticado (usuario logueado, invitación asociada a Contact/Lead/User) — el unAuth lo rechaza. Diseño de referencia para casos con identidad conocida (portal de clientes, empleados, agente Agentforce in-org).
POC A · La encuesta que sí probamos
┌─────────────────────────────────────────────────────────────────────┐
│ Página 1 · Perfil │
│ Q1 Industria · Single selection · Requerida │
│ Q2 Rol · Single selection · Requerida │
└─────────────────────────────────────────────────────────────────────┘
│ Next
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Página 2 · Estado actual │
│ Q3 ¿Ya usan Agentforce? · Sí/No · Requerida │
│ Q4 Canales desplegados · Multi-select · Opcional │
│ Q5 Satisfacción actual · Rating 1-5 · Opcional │
└─────────────────────────────────────────────────────────────────────┘
│ Next
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Página 3 · Priorización │
│ Q6 Probabilidad IA este año · NPS 0-10 · Requerida │
│ Q7 ¿Qué te frena? · Single sel. · Requerida │
│ Q8 Cuéntanos más · Long text · Opcional │
└─────────────────────────────────────────────────────────────────────┘
│ Next
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Página 4 · Contacto │
│ Q9 ¿Sesión con arquitecto? · Sí/No · Requerida │
│ Q10 Email · Short text · Opcional │
└─────────────────────────────────────────────────────────────────────┘
│ Next
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Página 5 · Thank You │
│ thankYouMessage: "Gracias por tu tiempo." │
│ redirectUrl: /es/insights/headless-feedback-management-… │
│ SurveyResponse.Status → Completed │
└─────────────────────────────────────────────────────────────────────┘
Objetivos técnicos — qué quedó demostrado
- ✅ Iniciar una respuesta anónima vía POST /surveys/v1/survey-response con invitationSettings.collectAnonymousResponse: true — Salesforce devuelve Survey Description Output con la Página 1 renderable.
- ✅ Renderizar los 7 tipos de pregunta (RadioButton, MultiChoice, Boolean, Rating, NPS, ShortText, FreeText) con un solo switch(questionType) — cero condicionales por página.
- ✅ Validar isResponseRequired en la UI antes de habilitar 'Next' — el flag viene por pregunta desde la API.
- ✅ Navegar página a página vía PATCH /survey-response con navigationAction: 'Next' y 'Back' — se preservan las respuestas al regresar.
- ✅ Detectar finalización por polimorfía del surveyPage devuelto (thankYouMessage vs surveyQuestions[]) — no hay un booleano isFinished.
- ✅ Persistir en objetos estándar: SurveyResponse.Status = Completed y una SurveyQuestionResponse por pregunta contestada — dashboards y Data Mapper heredados siguen funcionando.
- ❌ Comprobar branching server-side — NO se pudo con el path unAuth: SurveyType 'Survey' (Standard) rechazado con INVALID_INPUT_COMBINATION. Basic no lo soporta. Queda como POC B pendiente por autenticado o Conversational.
- ❌ Comprobar display logic server-side — misma razón. Basic no expone reglas condicionales. Pendiente para POC B.
Stack efectivamente implementado
Next.js 16 (App Router) + TypeScript
Componentes de encuesta en components/survey/ (SurveyRunner + QuestionInputs). Un dispatcher por questionType. Client component con estado de máquina idle → running → error → thankyou. Sin dependencias adicionales.
Route Handlers de Next.js + Session Store
POST /api/surveys/[name]/start y PATCH /api/surveys/[name]/navigate. Cliente unAuth con cache in-memory del accessToken. Session store en Map anclado a globalThis (sobrevive HMR) — la cookie solo lleva un ID de 64 bytes. Decoder de HTML entities server-side.
Org Enterprise + Feedback Management Growth
Encuesta como Basic Survey (5 páginas lineales). Setup → Survey Settings → Unauthenticated Survey Participation habilitado (con propagación de ~30s). Sharing manual al Guest User Profile del Experience Cloud site elegido.
Trampas empíricas — hallazgos no documentados por Salesforce
- SurveyType 'Survey' (Standard) es rechazado por la unAuth API — solo acepta Basic o 'Conversational' (feature no publicada). Docs no lo dicen; el error INVALID_INPUT_COMBINATION lo revela.
- SurveyType no se puede modificar por DML después de crear la encuesta — hay que borrar y recrear si se eligió el tipo equivocado.
- surveyPageResponses.name es REQUERIDO en cada PATCH aunque el brief lo describe como 'reserved for future use'. Sin él: 400 'Invalid request content'.
- questionResponses[] debe incluir una entrada por cada pregunta de la página, incluso las no contestadas. Sin eso: 'Specify the same number of questions in the survey and the input representation'.
- El flowInterviewState comprimido pesa ~4.8 KB — se pasa del límite ~4096 bytes de cookie del browser. Diseño obligado: session store server-side con solo un ID corto en la cookie.
- Labels llegan HTML-encoded ('Tecnología'). Debe decodificarse en el BFF antes de mandar al cliente.
- El toggle 'Unauthenticated Survey Participation' tiene ~30 segundos de propagación después de guardarse. Reintentos inmediatos siguen fallando con el mismo error de 'API isn't enabled'.
- El DeveloperName de la encuesta lo Salesforce normaliza a minúsculas — hay que guardar el valor case-sensitive real (no el que se escribió en el UI).
Criterios de éxito — qué se cumplió
- ✅ Cero líneas de código de flujo condicional en el frontend — auditable buscando 'if(answer' o 'goToPage'. El único condicional es 'page.kind === thankyou'.
- ✅ Un solo switch(questionType) en QuestionRenderer resuelve los 7 tipos de pregunta.
- ✅ Persistencia verificada en SurveyResponse (Status=Completed) y SurveyQuestionResponse — filas anclada a la SurveyVersion activa.
- ✅ El mismo SurveyRunner sirve para cualquier encuesta Basic — cambia el env SF_LAILA_SURVEY_DEV_NAME, cambia todo. Zero redeploy.
- ❌ Cobertura del flujo autenticado — queda pendiente (POC B).
Referencias
Fuentes oficiales
Toda la superficie técnica descrita en este documento está anclada a documentación oficial vigente de Salesforce al momento de publicación. Las capacidades de Feedback Management evolucionan de release en release — antes de decisiones comerciales, confirme con su cuenta de Salesforce las ediciones, los límites y los SKUs vigentes.
- Salesforce Feedback Management Developer Guide
- Business APIs Overview · Feedback Management
- Connect REST Resources · Surveys
- Create and Submit Surveys · Auth Flow
- Create and Submit Surveys Using Invitation Id
- Surveys for Unauthenticated Participants
- Set Up Your Environment · unAuth APIs
- Get Access Token · unAuth APIs
- Create and Submit Surveys · unAuth APIs
- Survey Response Input · Request body (auth)
- Survey Response Input · Request body (unAuth)
- Survey Response Output · Response body
- Survey Description Output · Response body
- Survey Question Page Output
- Salesforce Surveys · Standard Objects Overview
- SurveyInvitation · SObject Reference
- SurveyResponse · SObject Reference
- Feedback Management Add-on Licenses · Feature Comparison
- Salesforce Feedback Management · Product Page
¿Te resultó útil?
Compártelo con tu equipo o conversémoslo en una llamada de arquitectura.
