# Orders API (generic connection)

> The HTTP endpoint that records an order — or a cart someone has started to pay for — from any tool. Exact contract, field reference, responses, the 422 error catalogue and which contact level each payload creates.

Fuente / source: https://soporte.konverda.com/en/docs/developers/orders-api
Idioma / language: en
Actualizado / updated: 2026-09-11

---
The **«Conexión genérica»** (generic connection) is a single HTTP endpoint that turns an order from any tool — Zapier, Make, a webhooks module or your own code — into a recorded conversion and, when the phone number is recognisable, into a contact with its consent level.

You will find your exact URL in the app, under **«Ajustes → Integraciones → Conexión genérica»** (settings → integrations → generic connection).

## The endpoint {#endpoint}

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

:::aviso
The URL **contains the access token**: it is the credential. Treat it like a password — do not publish it, do not push it to a repository and do not paste it into a ticket. If it leaks, rotate it from the generic connection card and update it wherever you were using it.
:::

## Payload {#payload}

Only `order_ref` is required. Everything else is optional and improves what the platform can do with the order.

```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"]
}
```

## Field reference {#fields}

| Field | Type | Description |
|---|---|---|
| `order_ref` | text or number · required | Unique reference of the order in your platform (max. 120 characters). It is the deduplication key: resending the same `order_ref` never duplicates the sale. |
| `status` | `'paid'` \| `'refunded'` | Defaults to `'paid'`. `'refunded'` records a refund of the same order. |
| `occurred_at` | ISO 8601 date · optional | When it happened. Without it, the moment of receipt is used. |
| `total` | number · optional | Total amount of the order. Numeric text is also accepted (`"49.90"`). |
| `currency` | 3 letters · optional | Defaults to `EUR`. |
| `customer` | object · optional | Container for the customer's data: `phone`, `name`, `email`. |
| `customer.phone` | text · optional | The customer's WhatsApp phone number. Without it (or if it is not recognisable) the sale is recorded all the same, but with NO contact — it shows up under «Conversiones sin contacto» (conversions with no contact). |
| `customer.name` | text · optional | The customer's name. |
| `customer.email` | text · optional | Accepted but NEVER stored — email lives in your email tool. It is also scrubbed from the technical log of the request. |
| `consent` | strict boolean (`true` / `false`) · optional | The consent checkbox of your checkout. IT DECIDES THE CONTACT'S LEVEL — see [Consent](#consent). Careful: a quoted `"true"` or a `1` is NOT accepted here; a non-boolean value returns `422 bad_consent`. |
| `items[]` | list · optional | Max. 100 items: `{ id, name, quantity, total }`. Each one needs at least `id` or `name`. They feed the catalogue and the per-product automations. |
| `tags[]` | list of text · optional | Max. 20 tags to apply to the contact. |
| `token` | text · optional | The `wtk` parameter that sent links append to the destination URL (max. 64 characters). If the order carries no usable phone number but does carry this token, the sale is linked to the contact who received the link (it shows as «vía enlace» — via link). |
| `cart_ref` | text or number · optional | On an order: the cart this payment closes (max. 120 characters) — the same `cart_ref` you sent when the checkout started. Without it, the order still closes that contact's open carts. On a cart it is required — see [Carts](#cart). |
| `event` | `'checkout.started'` · optional | Without it, the payload is an order. With `"checkout.started"` it is a cart someone has started to pay for — see [Carts](#cart). Any other value returns `422 bad_event`. |
| `recovery_url` | `https` or `http` URL · carts only | The link that reopens that cart in your store. Ignored on an order. |

:::nota
`token` is a **top-level** JSON key and it is called `token`, even though the parameter that travels on the destination URL is called `wtk`. That is: `{"order_ref":"…","token":"<the value of ?wtk=>"}`, not `{"wtk":"…"}`.
:::

:::nota
The two public endpoints **do not read consent the same way**, and that is deliberate. Here `consent` is a **strict boolean**: `"true"` in quotes or `1` are rejected with `bad_consent`, because an order is sent by a store and a store knows what it has. The [capture-points endpoint](/en/docs/developers/capture-points#fields) does accept the string forms (`"1"`, `"yes"`, `"sí"`…) because what reaches it is forms and automation tools that can only send strings.
:::

## Trying it with 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 }]
  }'
```

A correct response is `{"ok":true,"matched":true}`. The sale appears under **«Ajustes → Integraciones → Catálogo»** (settings → integrations → catalogue) and, if the phone number is recognisable, on the contact's page.

## Responses {#responses}

| Code | Body | Meaning |
|---|---|---|
| `200` | `{"ok":true,"matched":true}` | Recorded and linked to a contact. |
| `200` | `{"ok":true,"matched":false}` | Recorded with NO contact (phone number absent or unrecognisable) — visible under «Conversiones sin contacto» (conversions with no contact). |
| `200` | `{"ok":true,"duplicate":true}` | That `order_ref` was already recorded. Resending is safe. |
| `200` | `{"ok":true,"recorded":false,"reason":"no_consent"}` | `consent:false` and the phone number does not belong to an already registered contact — no profile is created. It is a `200` on purpose: retrying would never change the outcome. |
| `404` | `{"error":"not_found"}` | Unknown URL or deactivated connection. Both answer the same: a `404` does not reveal whether the token exists. |
| `422` | `{"error":"invalid_payload","details":[…]}` | Invalid payload; `details` carries the per-field error codes. |
| `429` | `{"error":"rate_limited"}` | More than 60 requests per minute per IP — wait and retry. |
| `500` | `{"error":"ingest_error"}` | Internal error; retry later (deduplication by `order_ref` makes the retry safe). |

### The 422 error catalogue {#errors-422}

`details` is a list of codes, one per problem found. The whole request is rejected: nothing is recorded partially.

| Code | What causes it |
|---|---|
| `missing_order_ref` | `order_ref` is missing (or arrives as `null`). |
| `bad_order_ref` | `order_ref` is neither text nor number, is empty, or is longer than 120 characters. |
| `bad_status` | `status` is neither `'paid'` nor `'refunded'`. |
| `bad_occurred_at` | `occurred_at` is not text, or is not an interpretable date. |
| `bad_total` | `total` cannot be read as a number. |
| `bad_currency` | `currency` is not exactly 3 letters. |
| `bad_customer` | `customer` is not an object (it arrived as text or as a list). |
| `bad_consent` | `consent` is not a boolean. `"true"`, `"1"` and `1` are rejected here on purpose: consent is not guessed. |
| `bad_items` | `items` is not a list, is longer than 100, or some item carries neither `id` nor `name` (or carries a non-numeric `quantity`/`total`). |
| `bad_tags` | `tags` is not a list of text, or is longer than 20. |
| `bad_token` | `token` is not text, or is longer than 64 characters. |

## Consent: which contact level each payload creates {#consent}

Since 25 August 2026 a contact has TWO possible levels, and the `consent` field of your payload decides which one.

- **«Marketing»** = they agreed to receive messages: campaigns, waves, proactive automations and 1:1 templates.
- **«No»** = we can only reply to them when they write.

This is not a reporting nuance: a contact at level «No» receives NO message started by us — not even a utility template, not even the welcome automation. The attempt is logged as `not_marketing` and nothing is sent.

| What you send | Basis recorded | Resulting opt-in | What they can receive |
|---|---|---|---|
| `consent: true` | `checkbox` | «Marketing» | Everything: campaigns, waves, proactive automations (`order.paid` included) and 1:1 templates. And, being an explicit act by the customer themselves, it lifts a previous opt-out. |
| `consent` absent | `purchase` | «No» | Only replies when the person writes. The `order.paid` automation is NOT sent. A previous opt-out is always respected. |
| `consent: false` | — (no registration recorded) | Unchanged | The sale is recorded ONLY if the phone number was already a registered contact; if not, no profile is created and the response says so (`recorded:false`). |

:::aviso
`consent` is not a reporting field: it is the difference between a contact you may write to and one you may not. Send `consent:true` ONLY when your checkout has an explicit checkbox and it is ticked — never a hardcoded `true`. An order with no signal registers the contact at level «No» (you will be able to reply, not to initiate), and never lifts a previous opt-out.
:::

## Carts: `checkout.started` {#cart}

The same URL also records a **cart**: the moment someone starts to pay in your store. If they do not finish the purchase within the time you set in **«Ajustes → Reglas»** (settings → rules; 45 minutes unless you change it), Konverda treats the cart as abandoned and the flow you built on the **«Carrito abandonado»** (abandoned cart) trigger starts. What the customer sees, the consent it requires and the safeguards are in [Abandoned cart](/en/docs/flows/abandoned-cart).

```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
A cart **creates no contact and records no consent**. Konverda looks for a contact that already exists — by phone first, then by email — and if it finds none, it creates none. Consent arrives through a different door: your checkout's checkbox, sent to a [capture point](/en/docs/developers/capture-points) the moment it is ticked. That is why `consent` is ignored here.
:::

### Cart fields {#cart-fields}

| Field | Type | Description |
|---|---|---|
| `event` | `'checkout.started'` · required | What turns the payload into a cart. |
| `cart_ref` | text or number · required | The cart's id in your store (max. 120 characters). Resending the same `cart_ref` updates that cart and restarts its countdown: it never creates a second cart or a second reminder. |
| `recovery_url` | `https` or `http` URL · required | The link that reopens the cart (max. 2048 characters). Konverda never sends it as is: it turns it into a tracked link, with its UTMs. |
| `customer.phone` | text · optional | Used to find the contact. Same format as on orders. |
| `customer.email` | text · optional | Used to find the contact when the phone is not enough. On a cart with no contact it is kept **only while the cart is open** — the checkbox can arrive a moment later — and deleted as soon as the cart closes. |
| `items[]` | list · optional | As on orders. The first named item and the number of units can go into the message. |
| `total` | number · optional | The cart's value. It can go into the message in your currency's format («79,90 €»). |
| `currency` | 3 letters · optional | Defaults to `EUR`. |
| `occurred_at` | ISO 8601 date · optional | When the checkout started. The countdown starts when we receive it, not at this date. |

`status`, `consent`, `token` and `tags` are ignored on a cart.

### Cart responses {#cart-responses}

| Code | Body | Meaning |
|---|---|---|
| `200` | `{"ok":true,"matched":true,"armed":true}` | Cart recorded, contact found, countdown running. |
| `200` | `{"ok":true,"matched":false,"armed":true}` | Recorded with no contact yet. We look again when the countdown ends; if there is still nobody, the cart closes with no message. |
| `200` | `{"ok":true,"matched":…,"armed":false}` | That cart was already closed (bought, reminded or dropped). The resend changes nothing. |

`404`, `429` and `500` mean the same as for orders.

### Cart errors {#cart-errors}

| Code | What causes it |
|---|---|
| `bad_event` | `event` carries a value other than `"checkout.started"`. |
| `missing_cart_ref` | `cart_ref` is missing. |
| `bad_cart_ref` | `cart_ref` is not text or a number, is empty, or is longer than 120 characters. |
| `missing_recovery_url` | `recovery_url` is missing. |
| `bad_recovery_url` | `recovery_url` is not a valid `https`/`http` address or is longer than 2048 characters. |
| `bad_occurred_at` | `occurred_at` is not a readable date. |
| `bad_total` | `total` cannot be read as a number. |
| `bad_currency` | `currency` is not exactly 3 letters. |
| `bad_customer` | `customer` is not an object. |
| `bad_items` | `items` is not a valid list (same rules as on orders). |

### Closing the cart on payment {#cart-close}

When the purchase completes, send the normal order to this same URL. If it carries the cart's `cart_ref`, it closes exactly that cart; if not, it closes that contact's open carts. On top of that, right before sending the reminder, Konverda checks whether the person has bought since the cart started — through any connection, Stripe or WooCommerce included — and if they have, nothing is sent.

## Read next {#read-next}

- [The marketing opt-in and «Añadir a marketing»](/en/docs/contacts-and-consent/the-marketing-opt-in) — what the Opt-in column means, who is included in a campaign, and how to declare consent you hold outside the platform.
- [Opt-outs and what the platform remembers](/en/docs/contacts-and-consent/opt-outs-and-what-the-platform-remembers) — an opt-out is stored against the number, survives deletion, and only the contact themselves can lift it.
- [Connecting through Zapier or Make](/en/docs/developers/zapier-and-make) — the same contract, without writing code.
- [Stores and payment gateways](/en/docs/developers/stores-and-gateways) — recipes for PrestaShop, Wix, Squarespace, Shopify and Stripe.
- [Abandoned cart](/en/docs/flows/abandoned-cart) — the checkout checkbox, the countdown, the safeguards and the flow that sends the reminder.
