Registrar un envío de un formulario
/v1/forms/{id}/submissionsPara desarrolladores
Descripción
Registra un envío de un formulario que su sistema presentó al contacto. El formulario se construye y configura en el panel; este endpoint permite presentarlo con su propio diseño y en su propio sitio, y el envío conserva todo el comportamiento configurado en el formulario:
- Validación de cada valor según la configuración de su campo.
- Doble confirmación por correo, cuando el formulario la exige.
- Respuesta automática al contacto.
- Aviso al equipo.
- Registro de los consentimientos, con el texto del término aceptado.
El registro queda con source=form_api, que lo distingue tanto de un envío hecho en una página de Fidelizador como de un consentimiento declarado con POST /v1/consent-events, que no tiene formulario detrás.
Cuándo usar este endpoint
| Situación | Endpoint |
|---|---|
| Necesita presentar el formulario con su propio diseño y en su propio sitio, conservando la configuración del formulario de Fidelizador | Este endpoint, POST /v1/forms/{id}/submissions |
| Captó el consentimiento en un canal propio (un contrato, una llamada, su checkout) y solo necesita registrarlo | POST /v1/consent-events |
| No necesita presentarlo en su propio sitio | El enlace público o el código de inserción que ofrece el panel |
Al usar este endpoint, deberá agregar su propia protección anti-bot. El enlace público y el código de inserción del panel ya incluyen una verificación anti-bot. Los envíos hechos por este endpoint, no. Si el formulario que construye queda expuesto en internet, protéjalo antes de publicarlo. El detalle está en La protección anti-bot queda de su lado.
Guía relacionada
- Formularios — Arme formularios de captura y revise lo que se envió.
Autenticación
Authorizationbearer tokenheaderobligatorio- API key con scope
form:write. Formato:Bearer <API_KEY>. X-Instance-Slugstringheaderobligatorio- Identificador de su instancia. Ver Autenticación.
Se recomienda leer el formulario con GET /v1/forms/{id} antes de cada envío, por lo que la API key necesita también el scope form:read. Un scope no incluye al otro: una clave que solo tiene form:write recibe un 403 al leer el formulario.
La protección anti-bot queda de su lado
Al publicar el formulario con este endpoint, la protección anti-bot depende de su sitio.
Este endpoint no comprueba si quien envía el formulario es una persona. Una verificación anti-bot confirma que hay una persona frente a un navegador; una API key identifica a quien hace la llamada, que es su sistema. Son comprobaciones distintas, y un servidor no puede resolver un desafío anti-bot porque no tiene un navegador donde mostrarlo. Es lo habitual en las APIs que reciben formularios: autentican a quien integra, no a quien completó el formulario.
Por qué no sirve la verificación de Fidelizador. Un token de esa verificación se emite para el dominio de Fidelizador y solo allí se valida. Uno generado en su dominio sería rechazado.
Qué hacer. Si la página donde incluye el formulario está abierta en internet, agréguele su propia protección antes de publicarla. Sirve cualquiera de las habituales; lo recomendable es usar la que ya tenga en el resto de su sitio:
- Una verificación anti-bot integrada en el navegador, como reCAPTCHA, Turnstile o hCaptcha.
- Un campo trampa: un campo oculto que una persona nunca completa y que un robot sí completa.
- Un límite de envíos por dirección IP.
- Una combinación de varias de las anteriores.
Si migra desde el enlace público o el código de inserción, tenga en cuenta que el formulario ya no traerá esta protección. Lo más práctico es resolverlo en el mismo cambio y no después.
Lo que la plataforma sigue cubriendo:
- La doble confirmación, cuando el formulario la exige. Es la defensa más sólida contra la suscripción de terceros: el consentimiento no rige hasta que esa persona confirma desde su propio correo, así que un robot que use la dirección de otra persona no obtiene un consentimiento válido.
- La validación de cada valor contra la configuración de su campo.
- Las reglas de integridad del envío descritas más abajo.
- El descuento del cupo del plan por cada correo que genera el envío, que fija el máximo que puede producirse por esta vía.
Request
Path params:
idintegerobligatorio- Identificador del formulario (ver `GET /v1/forms`).
Headers opcionales:
Idempotency-Keyheader- Cadena que identifica este envío. Ver Idempotencia.
Body:
{
"items": [
{ "item_id": 42, "value": true, "term_version_id": 7 }
],
"ip_address": "200.55.200.100",
"user_agent": "Mozilla/5.0 (...)"
}
ip_addressstring- Dirección desde la que el contacto envió el formulario, tal como la observó usted. Queda registrada como dato declarado.
user_agentstring- Cliente desde el que el contacto lo envió, tal como lo observó usted. Queda registrado como dato declarado.
itemsarrayobligatorio- Una entrada por cada campo y cada consentimiento que declara el formulario. Ver las reglas más abajo.
item_idintegerobligatorio- Identificador del ítem: el
idque trae `GET /v1/forms/{id}` enitems[]. valueanyobligatorio- El valor capturado. En un ítem de consentimiento es
truesi se marcó yfalsesi no. term_version_idinteger- Obligatorio en los ítems de consentimiento. La versión del término que se mostró al contacto, tal como la entrega
GET /v1/forms/{id}.
El envío tiene que reflejar el formulario completo
La plataforma no tiene acceso a lo que su sistema mostró al contacto, por lo que verifica que el envío corresponda al formulario que declara enviar. Son cuatro reglas, y cada una evita una forma distinta de registrar un envío incorrecto sin que se advierta:
| Regla | Si no se cumple | Por qué |
|---|---|---|
Envíe todos los campos del formulario, incluidos los opcionales. Si el contacto dejó uno vacío, envíelo con value: null | field_item_omitted | Omitir un campo equivale a indicar que no se le mostró al contacto, lo que es distinto de que lo haya dejado en blanco. Un formulario que pregunta menos de lo que declara ya no es el que se publicó. |
Envíe todos los consentimientos del formulario, estén marcados o no. Los no marcados van con value: false | consent_item_omitted | Un consentimiento sin marcar es un rechazo y se registra como tal. Si se omitiera, ese rechazo no quedaría en el historial. |
Envíe cada item_id una sola vez | item_duplicated | Con un ítem repetido, el valor registrado dependería del orden de las entradas. Se rechaza el envío en lugar de suponer cuál quiso enviar. |
Envíe solo item_id que el formulario declare | item_unknown | Un ítem que el formulario no declara no se registraría, y la llamada respondería 201 sin que el valor quedara guardado. Suele indicar un envío armado para otro formulario, o para una versión de este que ya cambió. |
Los separadores y los bloques de texto no se envían, porque no capturan ningún valor.
La versión del término tiene que ser la vigente
term_version_id es la prueba de que el contacto leyó el texto que está en vigor. Si entre su última lectura del formulario y el envío se publicó una versión nueva del término, el envío se rechaza con consent_term_version_stale y la respuesta indica cuál es la vigente. Para resolverlo, consulte de nuevo GET /v1/forms/{id}, muestre al contacto el texto vigente y repita el envío.
Todos los problemas se informan juntos. Una llamada con varios errores los devuelve todos en un 422, para corregirlos de una vez y no de a uno por reintento.
Response
idinteger- Identificador del envío.
statusstring, enum: `confirmed | pending`confirmed: el envío quedó registrado y sus consentimientos rigen.pending: el formulario exige doble confirmación y se envió al contacto el correo con el enlace. Los consentimientos no rigen hasta que lo confirme.
200 OK — ya existía un envío con el mismo Idempotency-Key; no se creó nada. El cuerpo es el del envío original.
Idempotencia
Reintentar una llamada cuya respuesta no se recibió crearía un segundo envío, un segundo registro de consentimiento y un segundo correo al contacto. Para evitarlo, envíe Idempotency-Key con una cadena que identifique el envío, por ejemplo el identificador que este tiene en su propio sistema.
- La primera llamada responde 201 y registra el envío.
- Un reintento del mismo envío responde 200 con el mismo
id, sin crear un nuevo registro ni enviar un segundo correo.ip_addressyuser_agentpueden diferir, porque los observa el proceso que reintenta y no el contacto; sigue siendo el mismo envío. - La misma clave con un envío distinto se rechaza con
422ycode: idempotency_key_reused. Una clave repetida suele deberse a un contador que se reinició. Responder 200 con el envío anterior haría que el nuevo se perdiera sin aviso, por eso se rechaza. - El alcance de la clave es el formulario: dos formularios distintos pueden usar su propia numeración sin interferir.
- Sin la cabecera, cada llamada es un envío nuevo.
La clave se conserva mientras exista el envío, sin plazo de caducidad. La clave se guarda junto con el envío, que se conserva como evidencia. Use una clave única de forma permanente, como un UUID o el identificador que ese envío tiene en su sistema, y no un contador que se reinicie.
Errores
| Código | Cuándo |
|---|---|
403 | La API key no tiene el scope form:write, o la IP no está en la lista de acceso permitido. |
404 | El formulario no existe o está desactivado (form_not_found). Las dos situaciones responden igual. |
409 | Un consentimiento enviado no tiene en el formulario un campo que identifique al contacto en su canal (por ejemplo, un término de correo en un formulario sin campo de correo). |
422 | El envío no cumple el formulario (validation_error). Cada falla llega en errors[] con su propio code: un valor no cumple la configuración de su campo (field_value_invalid), falta un valor obligatorio (field_value_missing), no se aceptó un consentimiento obligatorio (consent_required), falta un campo (field_item_omitted) o un consentimiento (consent_item_omitted) que el formulario declara, se repitió un ítem (item_duplicated), se mandó uno que el formulario no declara (item_unknown), o la versión del término declarada ya no es la vigente (consent_term_version_stale). Se informan todas a la vez. |
Un caso del 422 tiene código propio: idempotency_key_reused. Se devuelve al reutilizar un Idempotency-Key con un envío distinto (ver Idempotencia). Su código es distinto del validation_error de los demás 422 porque la solución es otra: el cuerpo no tiene nada que corregir. Reenvíe el envío con una clave nueva.
Formato del cuerpo de error y detalle por código: Respuestas de error.
- Los correos consumen el cupo de su plan. El correo de doble confirmación y la respuesta automática son correos salientes, y cada uno se descuenta del plan igual que cualquier otro envío.
ip_addressyuser_agentson datos declarados por usted, no observados por la plataforma. En el historial y en la exportación, estos envíos se distinguen de los hechos en una página de Fidelizador.- No hay alta de formularios por API. El formulario, sus campos y sus términos se configuran en el panel.
curl -X POST "https://$API_HOST/v1/forms/12/submissions" \
-H "Authorization: Bearer $API_KEY" \
-H "X-Instance-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: alta-web-48213" \
-d '{
"items": [
{
"item_id": 41,
"value": "[email protected]"
},
{
"item_id": 42,
"value": true,
"term_version_id": 7
}
],
"ip_address": "200.55.200.100"
}'
{
"id": 9814,
"status": "confirmed"
}