# API de pedidos (conexión genérica)

> El endpoint HTTP que registra un pedido —o un carrito que alguien empieza a pagar— desde cualquier herramienta. Contrato exacto, referencia de campos, respuestas, catálogo de errores 422 y qué nivel de contacto crea cada payload.

Fuente / source: https://soporte.konverda.com/es/docs/desarrolladores/api-de-pedidos
Idioma / language: es
Actualizado / updated: 2026-09-11

---
La **conexión genérica** es un solo endpoint HTTP que convierte un pedido de cualquier herramienta —Zapier, Make, un módulo de webhooks o tu propio código— en una conversión registrada y, cuando el teléfono es reconocible, en un contacto con su nivel de consentimiento.

Encontrarás tu URL exacta en la app, en **Ajustes → Integraciones → Conexión genérica**.

## El endpoint {#endpoint}

```http
POST https://app.konverda.com/webhooks/orders/<tu-token>
Content-Type: application/json
```

:::aviso
La URL **contiene el token de acceso**: es la credencial. Trátala como una contraseña — no la publiques, no la subas a un repositorio y no la pegues en un ticket. Si se filtra, rótala desde la tarjeta de la conexión genérica y actualízala en el sitio donde la usabas.
:::

## Payload {#payload}

Solo `order_ref` es obligatorio. Todo lo demás es opcional y mejora lo que la plataforma puede hacer con el pedido.

```json
{
  "order_ref": "pedido-1234",
  "status": "paid",
  "occurred_at": "2026-07-12T10:00:00Z",
  "total": 49.9,
  "currency": "EUR",
  "customer": {
    "phone": "+34 612 345 678",
    "name": "Ana Pérez",
    "email": "ana@example.com"
  },
  "consent": true,
  "items": [
    { "id": "SKU-9", "name": "Curso X", "quantity": 1, "total": 49.9 }
  ],
  "tags": ["cliente"]
}
```

## Referencia de campos {#campos}

| Campo | Tipo | Descripción |
|---|---|---|
| `order_ref` | texto o número · obligatorio | Referencia única del pedido en tu plataforma (máx. 120 caracteres). Es la clave de deduplicación: reenviar el mismo `order_ref` nunca duplica la venta. |
| `status` | `'paid'` \| `'refunded'` | Por defecto `'paid'`. `'refunded'` registra un reembolso del mismo pedido. |
| `occurred_at` | fecha ISO 8601 · opcional | Cuándo ocurrió. Sin él se usa el momento de recepción. |
| `total` | número · opcional | Importe total del pedido. Acepta también texto numérico (`"49.90"`). |
| `currency` | 3 letras · opcional | Por defecto `EUR`. |
| `customer` | objeto · opcional | Contenedor de los datos del cliente: `phone`, `name`, `email`. |
| `customer.phone` | texto · opcional | Teléfono WhatsApp del cliente. Sin él (o si no es reconocible) la venta se registra igualmente, pero SIN contacto — aparece en «Conversiones sin contacto». |
| `customer.name` | texto · opcional | Nombre del cliente. |
| `customer.email` | texto · opcional | Se acepta pero NUNCA se guarda — el email vive en tu herramienta de email. Se borra también del registro técnico de la petición. |
| `consent` | booleano estricto (`true` / `false`) · opcional | La casilla de consentimiento de tu checkout. DECIDE EL NIVEL DEL CONTACTO — ver [Consentimiento](#consentimiento). Ojo: aquí NO se acepta `"true"` entre comillas ni `1`; un valor que no sea booleano devuelve `422 bad_consent`. |
| `items[]` | lista · opcional | Máx. 100 artículos: `{ id, name, quantity, total }`. Cada uno necesita al menos `id` o `name`. Alimentan el Catálogo y las automatizaciones por producto. |
| `tags[]` | lista de texto · opcional | Máx. 20 etiquetas a aplicar al contacto. |
| `token` | texto · opcional | El parámetro `wtk` que los enlaces enviados añaden a la URL de destino (máx. 64 caracteres). Si el pedido no trae teléfono utilizable pero sí este token, la venta se vincula al contacto que recibió el enlace (aparece como «vía enlace»). |
| `cart_ref` | texto o número · opcional | En un pedido: el carrito que este pago cierra (máx. 120 caracteres), el mismo `cart_ref` que enviaste al empezar el pago. Sin él, el pedido cierra igualmente los carritos abiertos de ese contacto. En un carrito es obligatorio — ver [Carritos](#carrito). |
| `event` | `'checkout.started'` · opcional | Sin él, el payload es un pedido. Con `"checkout.started"` es un carrito que alguien ha empezado a pagar — ver [Carritos](#carrito). Cualquier otro valor devuelve `422 bad_event`. |
| `recovery_url` | URL `https` o `http` · solo en carritos | El enlace que reabre ese carrito en tu tienda. En un pedido se ignora. |

:::nota
`token` es una clave **de primer nivel** del JSON y se llama `token`, aunque el parámetro que viaja en la URL de destino se llame `wtk`. Es decir: `{"order_ref":"…","token":"<el valor de ?wtk=>"}`, no `{"wtk":"…"}`.
:::

:::nota
Los dos endpoints públicos **no leen el consentimiento igual**, y es a propósito. Aquí `consent` es un **booleano estricto**: `"true"` entre comillas o `1` se rechazan con `bad_consent`, porque un pedido lo manda una tienda y una tienda sabe lo que tiene. El [endpoint de puntos de captura](/es/docs/desarrolladores/puntos-de-captura#campos) sí acepta las formas de texto (`"1"`, `"yes"`, `"sí"`…) porque le llegan formularios y automatizadores que solo saben mandar cadenas.
:::

## Probarlo con curl {#curl}

```bash
curl -X POST 'https://app.konverda.com/webhooks/orders/<tu-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "order_ref": "prueba-1",
    "total": 49.9,
    "customer": { "phone": "+34612345678", "name": "Ana Pérez" },
    "consent": true,
    "items": [{ "id": "SKU-9", "name": "Curso X", "quantity": 1, "total": 49.9 }]
  }'
```

Una respuesta correcta es `{"ok":true,"matched":true}`. La venta aparece en **Ajustes → Integraciones → Catálogo** y, si el teléfono es reconocible, en la ficha del contacto.

## Respuestas {#respuestas}

| Código | Cuerpo | Significado |
|---|---|---|
| `200` | `{"ok":true,"matched":true}` | Registrado y vinculado a un contacto. |
| `200` | `{"ok":true,"matched":false}` | Registrado SIN contacto (teléfono ausente o irreconocible) — visible en «Conversiones sin contacto». |
| `200` | `{"ok":true,"duplicate":true}` | Ese `order_ref` ya estaba registrado. Reenviar es seguro. |
| `200` | `{"ok":true,"recorded":false,"reason":"no_consent"}` | `consent:false` y el teléfono no corresponde a un contacto ya dado de alta — no se crea ningún perfil. Es un `200` a propósito: reintentar nunca cambiaría el resultado. |
| `404` | `{"error":"not_found"}` | URL desconocida o conexión desactivada. Ambas responden igual: un `404` no revela si el token existe. |
| `422` | `{"error":"invalid_payload","details":[…]}` | Payload inválido; `details` lleva los códigos de error por campo. |
| `429` | `{"error":"rate_limited"}` | Más de 60 peticiones por minuto y por IP — espera y reintenta. |
| `500` | `{"error":"ingest_error"}` | Error interno; reintenta más tarde (la deduplicación por `order_ref` hace el reintento seguro). |

### Catálogo de errores del 422 {#errores-422}

`details` es una lista de códigos, uno por problema encontrado. La petición se rechaza entera: no se registra nada parcialmente.

| Código | Qué lo provoca |
|---|---|
| `missing_order_ref` | Falta `order_ref` (o llega `null`). |
| `bad_order_ref` | `order_ref` no es texto ni número, está vacío o pasa de 120 caracteres. |
| `bad_status` | `status` no es `'paid'` ni `'refunded'`. |
| `bad_occurred_at` | `occurred_at` no es texto o no es una fecha interpretable. |
| `bad_total` | `total` no se puede leer como número. |
| `bad_currency` | `currency` no son exactamente 3 letras. |
| `bad_customer` | `customer` no es un objeto (llegó como texto o como lista). |
| `bad_consent` | `consent` no es booleano. `"true"`, `"1"` y `1` se rechazan aquí a propósito: el consentimiento no se adivina. |
| `bad_items` | `items` no es una lista, pasa de 100, o algún artículo no trae ni `id` ni `name` (o trae `quantity`/`total` no numéricos). |
| `bad_tags` | `tags` no es una lista de textos o pasa de 20. |
| `bad_token` | `token` no es texto o pasa de 64 caracteres. |

## Consentimiento: qué nivel de contacto crea cada payload {#consentimiento}

Desde el 25 de agosto de 2026 un contacto tiene DOS niveles posibles, y el campo `consent` de tu payload decide cuál.

- **«Marketing»** = aceptó recibir envíos: campañas, oleadas, automatizaciones proactivas y plantillas 1:1.
- **«No»** = solo podemos responderle cuando escribe él.

No es un matiz de informe: un contacto de nivel «No» no recibe NINGÚN mensaje que iniciemos nosotros — ni siquiera una plantilla de utilidad, ni la automatización de bienvenida. El intento queda registrado como `not_marketing` y no se envía.

| Lo que envías | Base registrada | Opt-in resultante | Qué puede recibir |
|---|---|---|---|
| `consent: true` | `checkbox` | Marketing | Todo: campañas, oleadas, automatizaciones proactivas (`order.paid` incluida) y plantillas 1:1. Además, al ser un acto explícito del propio cliente, levanta una baja anterior. |
| `consent` ausente | `purchase` | No | Solo respuestas cuando la persona escribe. La automatización de `order.paid` NO se envía. Una baja anterior se respeta siempre. |
| `consent: false` | — (no se registra alta) | Sin cambios | La venta se registra SOLO si el teléfono ya era un contacto dado de alta; si no, no se crea ningún perfil y la respuesta lo dice (`recorded:false`). |

:::aviso
`consent` no es un campo de informe: es la diferencia entre un contacto al que puedes escribir y uno al que no. Envía `consent:true` SOLO cuando tu checkout tenga una casilla explícita y esté marcada — nunca un `true` fijo. Un pedido sin señal da de alta al contacto en nivel «No» (podrás responderle, no iniciar), y nunca levanta una baja previa.
:::

## Carritos: `checkout.started` {#carrito}

La misma URL registra también un **carrito**: el momento en que alguien empieza a pagar en tu tienda. Si no termina la compra en el tiempo que marques en **Ajustes → Reglas** (45 minutos si no lo cambias), Konverda lo da por abandonado y se inicia el flujo que tengas con el disparador **Carrito abandonado**. Lo que ve el cliente, el consentimiento que exige y las protecciones están en [Carrito abandonado](/es/docs/flujos/carrito-abandonado).

```json
{
  "event": "checkout.started",
  "cart_ref": "carrito-5678",
  "recovery_url": "https://tu-tienda.com/carrito/recuperar/5678",
  "occurred_at": "2026-09-11T10:00:00Z",
  "total": 79.9,
  "currency": "EUR",
  "customer": {
    "phone": "+34 612 345 678",
    "email": "ana@example.com"
  },
  "items": [
    { "id": "SKU-9", "name": "Curso X", "quantity": 1, "total": 49.9 },
    { "id": "SKU-12", "name": "Libro del curso", "quantity": 1, "total": 30 }
  ]
}
```

:::importante
Un carrito **no da de alta a nadie ni registra consentimiento**. Konverda busca un contacto que ya exista —primero por teléfono, después por email— y, si no lo encuentra, no crea ninguno. El consentimiento llega por otra puerta: la casilla de tu checkout, enviada a un [punto de captura](/es/docs/desarrolladores/puntos-de-captura) en cuanto se marca. Por eso aquí `consent` se ignora.
:::

### Campos del carrito {#carrito-campos}

| Campo | Tipo | Descripción |
|---|---|---|
| `event` | `'checkout.started'` · obligatorio | Lo que convierte el payload en un carrito. |
| `cart_ref` | texto o número · obligatorio | El identificador del carrito en tu tienda (máx. 120 caracteres). Reenviar el mismo `cart_ref` actualiza ese carrito y vuelve a empezar su cuenta atrás: nunca crea un segundo carrito ni un segundo aviso. |
| `recovery_url` | URL `https` o `http` · obligatorio | El enlace que reabre el carrito (máx. 2048 caracteres). Konverda nunca lo envía tal cual: lo convierte en un enlace con seguimiento, con sus UTM. |
| `customer.phone` | texto · opcional | Para encontrar al contacto. Mismo formato que en los pedidos. |
| `customer.email` | texto · opcional | Para encontrar al contacto si el teléfono no basta. En un carrito sin contacto se guarda **solo mientras el carrito está abierto** —la casilla puede llegar un instante después— y se borra en cuanto se cierra. |
| `items[]` | lista · opcional | Como en los pedidos. El primer artículo con nombre y el número de unidades se pueden poner en el mensaje. |
| `total` | número · opcional | El importe del carrito. Se puede poner en el mensaje con el formato de tu moneda («79,90 €»). |
| `currency` | 3 letras · opcional | Por defecto `EUR`. |
| `occurred_at` | fecha ISO 8601 · opcional | Cuándo empezó el pago. La cuenta atrás empieza al recibirlo, no en esta fecha. |

`status`, `consent`, `token` y `tags` se ignoran en un carrito.

### Respuestas del carrito {#carrito-respuestas}

| Código | Cuerpo | Significado |
|---|---|---|
| `200` | `{"ok":true,"matched":true,"armed":true}` | Carrito registrado, contacto encontrado, cuenta atrás en marcha. |
| `200` | `{"ok":true,"matched":false,"armed":true}` | Registrado sin contacto todavía. Se vuelve a buscar cuando termina la cuenta atrás; si sigue sin haber nadie, el carrito se cierra sin mensaje. |
| `200` | `{"ok":true,"matched":…,"armed":false}` | Ese carrito ya estaba cerrado (comprado, recordado o descartado). El reenvío no cambia nada. |

`404`, `429` y `500` significan lo mismo que en los pedidos.

### Errores del carrito {#carrito-errores}

| Código | Qué lo provoca |
|---|---|
| `bad_event` | `event` trae un valor que no es `"checkout.started"`. |
| `missing_cart_ref` | Falta `cart_ref`. |
| `bad_cart_ref` | `cart_ref` no es texto ni número, está vacío o pasa de 120 caracteres. |
| `missing_recovery_url` | Falta `recovery_url`. |
| `bad_recovery_url` | `recovery_url` no es una dirección `https`/`http` válida o pasa de 2048 caracteres. |
| `bad_occurred_at` | `occurred_at` no es una fecha interpretable. |
| `bad_total` | `total` no se puede leer como número. |
| `bad_currency` | `currency` no son exactamente 3 letras. |
| `bad_customer` | `customer` no es un objeto. |
| `bad_items` | `items` no es una lista válida (mismas reglas que en los pedidos). |

### Cerrar el carrito al pagar {#carrito-cierre}

Cuando la compra se completa, envía el pedido normal a esta misma URL. Si lleva el `cart_ref` del carrito, cierra ese carrito exactamente; si no, cierra los carritos abiertos de ese contacto. Además, justo antes de enviar el recordatorio, Konverda comprueba si esa persona ha comprado desde que empezó el carrito —por cualquier conexión, también Stripe o WooCommerce—, y si ha comprado no envía nada.

## Seguir leyendo {#seguir-leyendo}

- [El opt-in de marketing y «Añadir a marketing»](/es/docs/contactos-y-consentimiento/el-opt-in-de-marketing) — qué significa la columna Opt-in, quién entra en una campaña y cómo declarar un consentimiento que tienes fuera de la plataforma.
- [La baja y qué recuerda la plataforma](/es/docs/contactos-y-consentimiento/la-baja-y-que-recuerda-la-plataforma) — la baja se guarda contra el número, sobrevive a borrados y solo el propio contacto puede levantarla.
- [Conectar vía Zapier o Make](/es/docs/desarrolladores/zapier-y-make) — el mismo contrato, sin escribir código.
- [Tiendas y pasarelas](/es/docs/desarrolladores/tiendas-y-pasarelas) — recetas para PrestaShop, Wix, Squarespace, Shopify y Stripe.
- [Carrito abandonado](/es/docs/flujos/carrito-abandonado) — la casilla del checkout, la cuenta atrás, las protecciones y el flujo que envía el recordatorio.
