# Crear un formulario y una solicitud en una sola llamada

Envía los bloques con la solicitud: Formstep crea el formulario, lo publica y devuelve el enlace, sin pasar por el editor.

## Cuándo usarlo

Normalmente, una solicitud apunta a un formulario que alguien creó en el editor y publicó. Cuando las preguntas dependen del caso, el llamante las envía con la solicitud: `requests.create` acepta `form` en lugar de `formId`. Formstep crea un formulario normal a partir de los bloques, lo publica y crea la solicitud en un solo paso, así que existen los dos o ninguno. Lo llamamos **formulario integrado**.

Sigue usando `formId` para un formulario que envías a muchas personas con la misma forma y quieres diseñar a mano en el editor. Usa `form` cuando una automatización decide qué preguntar.

## La llamada

```bash
curl -X POST https://api.formstep.io/api/v1 \
  -H "Authorization: Bearer $FORMSTEP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method":"requests.create","params":{
        "form":{
          "name":"Documents and fee for your mortgage application",
          "logo":{"type":"image","url":"https://acme.example/logo.png"},
          "cover":{"type":"color","color":"#0f4c81"},
          "emoji":"🏦",
          "blocks":[
            {"type":"paragraph","content":"Upload the two documents and pay the processing fee."},
            {"type":"file","key":"payslips","title":"Your last two payslips","maxFiles":2},
            {"type":"payment","key":"fee","title":"Processing fee","amount":40,"currency":"EUR"},
            {"type":"scheduleAppointment","key":"call","title":"Pick a time for our call","eventType":"15 min call"}]},
        "recipient":{"email":"ada@example.com","name":"Ada"},
        "delivery":"email",
        "idempotencyKey":"call-8841"}}'
```

Todo lo demás de la solicitud conserva su significado: `recipient`, `prefill`, `context`, `readonly`, `delivery`, los recordatorios, la caducidad, `callbackUrl`, `test` e `idempotencyKey` se comprueban contra el nuevo formulario igual que contra uno creado a mano. La respuesta añade el formulario, con las claves con las que vuelven sus respuestas:

```json
{
  "id": "kd7...",
  "status": "pending",
  "url": "https://form.formstep.io/r/rq_...",
  "form": {
    "id": "j58...",
    "name": "Documents and fee for your mortgage application",
    "reused": false,
    "fields": [
      { "key": "payslips", "type": "file" },
      { "key": "fee", "type": "payment" },
      { "key": "call", "type": "schedule-appointment" }
    ]
  }
}
```

Para precompletar una pregunta de elección en la misma llamada, envía claves de opción, como con cualquier formulario. La clave de una opción se deriva de su etiqueta, así que `Self-employed` pasa a ser `self_employed`. Una etiqueta en lugar de una clave falla con `INVALID_PREFILL_VALUE`, que lista las claves válidas.

## El objeto form

| Campo | Qué hace |
| - | - |
| name | Obligatorio, de 1 a 100 caracteres. El nombre en el panel y en la pestaña del navegador, el mismo para cada destinatario. Personaliza con prefill y context, nunca con el nombre. |
| blocks | Obligatorio, de 1 a 200 bloques, en el orden en que los muestra el formulario. Los bloques de las columnas de una fila y los campos de un grupo repetible también cuentan. |
| language | El idioma de la interfaz del formulario. Por defecto, inglés. |
| logo, cover, emoji | Tu marca, enviada con cada solicitud. logo: { "type": "icon", "name": "HeartIcon" } o { "type": "image", "url": "https\://…" }. cover: { "type": "color", "color": "#0f4c81" } o { "type": "image", "url": "https\://…", "offsetY": 50 }. emoji: un emoji junto al nombre. Consulta más abajo. |
| theme | Un aspecto por modo de color: { "light": "solarized-light", "dark": "dracula" }. Cada modo acepta el id de un preajuste, gratis en todos los planes, o un objeto de tema completo como el que acepta la herramienta MCP formTheme\_set, que requiere el plan Pro. |
| settings | El comportamiento del formulario que fija la página de ajustes: la marca, un dominio de correo verificado (emailDomainId), los correos al propietario y al encuestado con su texto, el calendario de recordatorios, el correo de invitación, la redirección y la retención. Campos por clave: respondentNotificationTo es la clave de una pregunta de correo, redirectQueryParams asigna un parámetro a una clave de campo. El texto de los correos es texto plano con marcadores {{key}}. No hay barrera de contraseña ni de inicio de sesión, ni ajustes del enlace público como la protección contra bots o las ediciones tras el envío: una solicitud lleva su propio enlace. Cualquier otra clave se rechaza. |
| translations | El formulario en más idiomas, con un selector de idioma para el destinatario: { "de": { "Your name": "Ihr Name" } }, cada texto del formulario con el texto tal como lo escribieron los bloques como clave. Consulta más abajo. |

## Bloques

Un bloque acepta los parámetros de la herramienta `editor_insert*` correspondiente del [servidor MCP](https://docs.formstep.io/es/developers/mcp-server/), sin `formId` ni `after`, más una `key` opcional. Sin ella, la clave se deriva del título, así que “Full name” pasa a ser `full_name`. Un parámetro que el bloque no conoce se rechaza en lugar de ignorarse.

| Tipo | Bloques |
| - | - |
| Preguntas | text, contact, number, date, time, radio, checkbox, select, switch, rating, linearScale, matrix, ranking, pictureChoice, file, signature, decision |
| Documentos | documents, para mostrar archivos que el destinatario lee o firma |
| Pagos y reservas | payment, scheduleAppointment |
| Contenido | header, paragraph, image (URL en src), embedded, table (cells la rellena), list, row (columns: los bloques de cada columna, de 2 a 6 columnas, y widths en porcentaje). Una key opcional permite que la lógica muestre u oculte el bloque. |
| Estructura | pageDivider, repeatingGroup (fields: las preguntas del grupo como bloques, cada una con su key), hiddenField (key obligatoria: es la clave de context), calculatedField (su name es su clave), logic |

El texto de header y paragraph, los elementos de lista y las celdas de tabla admiten `bold`, `italic`, `struck`, `<u>underlined</u>`, `code`, `$$equation$$`, `text`, `<span color=“red” background=“yellow” font=“font-georgia” size=“1.25”>…</span>` con los colores, fuentes y tamaños de la barra de herramientas del editor, y `{{key}}`, que muestra el valor del campo con esa clave. Las marcas se pueden anidar; un marcador sin pareja es texto. Una columna de una fila admite cualquier bloque excepto pageDivider, logic, hiddenField, calculatedField, repeatingGroup, row y scheduleAppointment. Un bloque `decision` acepta su `title` y sus `labels` para aprobar, rechazar y pedir cambios en el idioma del formulario.

Los bloques se refieren unos a otros por clave, nunca por id de elemento: un bloque `logic` acepta su `rule` como lo hace `editor_insertLogic`, con claves de campo donde pide ids, etiquetas de opción como valores de condición y nombres de página para `setNextPage`.

### Pagos

Un bloque `payment` acepta `amount` y `currency`, o `amountFromField`: la clave de un campo oculto o de una pregunta numérica. Con un campo oculto, cada solicitud pasa su propio importe en `context`, así que un solo formulario cobra a cada cliente su propio importe:

```json
{
  "form": {
    "name": "Pay your invoice",
    "blocks": [
      { "type": "hiddenField", "key": "invoice_total" },
      { "type": "payment", "key": "invoice", "title": "Invoice", "currency": "EUR", "amountFromField": "invoice_total" }
    ]
  },
  "recipient": { "email": "ada@example.com" },
  "context": { "invoice_total": 1200 }
}
```

Una pregunta numérica que fija el importe debe ser obligatoria y tener un `min` mayor que 0, para que nadie llegue al checkout con un importe vacío, cero o negativo.

El formulario cobra a través de la cuenta de Stripe predeterminada del espacio de trabajo, o de la que indique `connection`. Una persona [conecta Stripe una vez para el espacio de trabajo](https://docs.formstep.io/es/building-forms/payment-collection/#connect-stripe); una automatización no puede. Sin una cuenta utilizable, la llamada falla y te dice dónde conectarla.

### Reservas

Un bloque `scheduleAppointment` acepta `eventType`, el título o el id de un tipo de evento de la conexión de [Cal.com](https://docs.formstep.io/es/integrations/cal-com/) del espacio de trabajo, y `connection` cuando el espacio de trabajo tiene varias. Formstep busca el tipo de evento al crear la solicitud. `prefill: { name, email }` indica las preguntas cuyas respuestas rellenan la reserva.

## Tu marca

Una automatización no puede inventarse tu imagen, así que se la das una vez y la envía con cada solicitud: `logo`, `cover` y `emoji` junto a los bloques, y `theme` para los colores y la tipografía.

| Campo | Acepta |
| - | - |
| logo | Un icono del editor, { "type": "icon", "name": "HeartIcon" }, o tu propia imagen, { "type": "image", "url": "https\://…/logo.png" }. |
| cover | Un color sólido, { "type": "color", "color": "#0f4c81" }, o una imagen, { "type": "image", "url": "https\://…/cover.jpg", "offsetY": 50 }, donde offsetY, de 0 a 100, elige la parte visible de una imagen alta. |
| emoji | Un emoji, que se muestra junto al nombre del formulario. |

Cada imagen, incluido el `src` de un bloque image, debe ser un JPEG, PNG o WebP público. Formstep descarga cada una una vez al crear el formulario y guarda su propia copia, así que el formulario y los PDF de sus envíos sobreviven al original. Una URL que no se puede descargar hace fallar la llamada con `INLINE_FORM_IMAGE_UNREACHABLE`, que indica el campo. Sin marca, el formulario se ve como uno nuevo creado desde el panel.

## Traducciones

`translations` publica el formulario en más idiomas a la vez, y el destinatario recibe el selector de idioma que tiene un formulario traducido: `{ "de": { "Your name": "Ihr Name", "Order for **{{customer}}**": "Bestellung für **{{customer}}**" } }`. Debe estar cada texto del formulario, con el texto exacto que escribieron los bloques como clave: títulos de preguntas, opciones, marcadores de posición, encabezados, párrafos, elementos de lista, celdas de tabla, nombres de grupo y etiquetas de botones. El marcado y `{{key}}` se mantienen como en el original. Si falta un texto, la llamada falla con `INLINE_FORM_TRANSLATION_MISSING`, que lista los textos que hay que añadir. El texto de los correos no se traduce.

## Una definición, un formulario

Una llamada posterior con el mismo objeto `form` reutiliza el mismo formulario, y la respuesta indica `“reused”: true`. Cien solicitudes con una misma definición crean un solo formulario, con un solo conjunto de respuestas que exportar. Cambiar un bloque, el nombre, la marca o una traducción crea un formulario nuevo; las solicitudes ya enviadas conservan el formulario con el que se crearon. Un formulario cerrado o despublicado no se reutiliza: la siguiente llamada con la misma definición crea uno nuevo.

> **Personaliza con prefill y context**
>
> Pon el nombre del cliente, el número de factura o el importe en `prefill` y `context`, nunca en el nombre del formulario, en un título o en un importe fijo. Una definición que cambia con cada cliente crea un formulario nuevo por cliente. Pon una `key` en cada pregunta: una clave derivada sigue al título, así que una pregunta con otro título responde con una clave nueva. Un espacio de trabajo crea como máximo 300 formularios inline nuevos al día; a partir de ahí, una llamada que crearía un formulario nuevo falla con `RATE_LIMITED`, mientras que las que reutilizan un formulario siguen funcionando.

Cuando una llamada crea un formulario nuevo aunque un formulario integrado abierto tenga los mismos tipos de bloque en el mismo orden, la respuesta nombra ese formulario en `form.similarForm`. Normalmente se debe a un nombre o un título cambiados. Para leer todas las respuestas de un formulario integrado, lista sus envíos en lugar de leer cada solicitud.

## En el panel

Un formulario integrado pertenece a su solicitud, así que vive en la página de la solicitud y no en la página Formularios. La página lista las respuestas por clave de campo, y **Vista previa** abre el formulario tal como lo vio el destinatario, rellenado con sus respuestas. Un enlace al editor o a los envíos de un formulario integrado abre en su lugar la página Solicitudes filtrada por ese formulario. Los agentes siguen viendo los formularios integrados en `form_list`, y la API se niega a modificarlos.

Un formulario integrado no tiene enlace público y no se le puede añadir uno: sus solicitudes son la única forma de entrar, y el callback lleva todas las respuestas. Tampoco tiene integraciones.

Como no tiene una página propia, un formulario integrado nunca se elimina a mano. Se va con su última solicitud: cuando [eliminas una solicitud](https://docs.formstep.io/es/requests/managing-requests/#delete), o el período de retención del formulario se la lleva, y ninguna otra solicitud sigue usando el formulario, el formulario también se elimina.

## Cuando se rechaza la llamada

Cada rechazo es un `VALIDATION_ERROR` con un `details.reason` según el cual tu automatización puede decidir qué hacer. No se crea nada.

| Motivo | Qué hacer |
| - | - |
| FORM\_SOURCE\_MISSING / FORM\_SOURCE\_AMBIGUOUS | Envía exactamente uno de formId y form. |
| INLINE\_FORM\_INVALID | Un bloque o un campo de form no cumple su esquema. details.issues indica cada uno. |
| INLINE\_FORM\_DUPLICATE\_KEY | Dos bloques dan la misma clave. Asigna a uno de ellos su propia key. |
| INLINE\_FORM\_UNKNOWN\_KEY | Una regla de lógica, amountFromField o el prefill de una reserva indica una clave que ningún bloque tiene. |
| INLINE\_FORM\_AMOUNT\_MISSING | Pasa un número positivo para el campo del importe en context. |
| INLINE\_FORM\_CONNECTION\_MISSING | Stripe o Cal.com no está conectado. details.connectUrl es la página que una persona abre una vez para conectarlo. |
| INLINE\_FORM\_EVENT\_TYPE\_NOT\_FOUND | Ningún tipo de evento de Cal.com coincide. El error enumera los que hay. |
| INLINE\_FORM\_IMAGE\_UNREACHABLE | Una URL de imagen no es un JPEG, PNG o WebP público. details.field la indica. Usa una URL que cualquiera pueda abrir, o quita la imagen. |
| INLINE\_FORM\_TRANSLATION\_MISSING | A una traducción le faltan textos que tiene el formulario. details.missing los lista, tal como están escritos en los bloques. |
| INLINE\_FORM\_PUBLISH\_ISSUES | La publicación rechazó el formulario, por ejemplo por un tipo de evento que no se puede reservar desde un formulario. |

## Desde un agente

En el servidor MCP, `request_create` acepta el mismo `form`. Antes de crear un formulario con un pago o una reserva, un agente consulta `workspaceConnection_list`: muestra las cuentas de Stripe y Cal.com conectadas y, cuando falta una, el enlace que una persona debe abrir una vez. `scheduler_listEventTypes` enumera los tipos de evento que puede indicar una reserva, y `paymentConnection_setDefault` cambia la cuenta de Stripe predeterminada. En la API REST, esas mismas tres son `connections.list`, `scheduler.listEventTypes` y `paymentConnections.setDefault`.

[Crear una solicitud](https://docs.formstep.io/es/requests/creating-requests/) — Precompletar, bloquear y contexto para cualquier formulario.

[Claves de campo](https://docs.formstep.io/es/requests/field-keys/) — Cómo se derivan las claves y cómo se mantienen estables.

[Callbacks y firma](https://docs.formstep.io/es/requests/callbacks/) — Recibe las respuestas cuando termina la solicitud.

[Cobro de pagos](https://docs.formstep.io/es/building-forms/payment-collection/) — Conecta Stripe para el espacio de trabajo.
