> ## Documentation Index
> Fetch the complete documentation index at: https://ayuda.gudink.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API: crear, consultar y cancelar pedidos

> Cómo crear un pedido por la API de Gudink sin duplicarlo, a domicilio o con retiro en sucursal, el formato exacto de cada campo, cómo se paga, cuándo vence, qué significa cada estado y sus transiciones, cómo consultarlo y cómo cancelarlo.

Un pedido por API es un pedido tuyo como cualquier otro: lo ves en tu panel, lo pagás desde ahí y Gudink lo produce y lo envía a tu comprador.

Los ejemplos usan dos variables de entorno. Así como están apuntan al [entorno de pruebas](/api/entorno-de-pruebas); para producción cambiá las dos (`https://app.gudink.com/api/v1` y tu clave `gk_live_`):

```bash theme={null}
export GUDINK_API_BASE="https://staging.gudink.com/api/v1"
export GUDINK_API_KEY="gk_stg_..."   # nunca la escribas en el código
```

## Crear un pedido

Este ejemplo es un envío a domicilio. Para un retiro, ver [Retiro en sucursal](#retiro-en-sucursal).

```bash theme={null}
curl -sS "$GUDINK_API_BASE/orders" \
  -H "Authorization: Bearer $GUDINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "woo-k7p2-10432",
    "items": [{ "product_id": 812, "variant_id": 4410, "quantity": 2 }],
    "recipient": { "name": "Ana Pérez", "phone": "+54 9 11 5555 5555" },
    "shipping_address": {
      "street": "Av. Corrientes",
      "number": "1234",
      "floor": "3",
      "apartment": "B",
      "city": "CABA",
      "province": "Capital Federal",
      "postal_code": "C1043AAZ",
      "notes": "Timbre roto, llamar al llegar"
    },
    "shipping": {
      "quote_id": "sq_9f3kQ2v7c1ZxLwP0aB8mT4yR6eHn5uJd",
      "service_code": "<el de la opción elegida>"
    }
  }'
```

Respuesta `201` (la misma forma que devuelve `GET /orders/:id`):

```json theme={null}
{
  "id": 1043,
  "number": "GU-1000412",
  "external_id": "woo-k7p2-10432",
  "status": "pending_payment",
  "hold_reason": null,
  "cancel_reason": null,
  "created_at": "2026-10-02T15:10:00.000Z",
  "updated_at": "2026-10-02T15:10:00.000Z",
  "expires_at": "2026-10-05T15:10:00.000Z",
  "items": [
    {
      "product_id": 812,
      "variant_id": 4410,
      "name": "Remera Logo Sur",
      "size": "M",
      "color": "Negro",
      "quantity": 2,
      "unit_price": 15990,
      "total_price": 31980
    }
  ],
  "amounts": { "subtotal": 31980, "shipping": 6890, "total": 38870 },
  "payment": { "status": "pending", "pay_url": "https://staging.gudink.com/pedidos/1043" },
  "tracking": { "number": null, "url": null, "carrier": null },
  "recipient": { "name": "Ana Pérez", "phone": "+54 9 11 5555 5555" },
  "delivery_type": "home",
  "pickup_point": null,
  "shipping_address": {
    "street": "Av. Corrientes",
    "number": "1234",
    "floor": "3",
    "apartment": "B",
    "city": "CABA",
    "province": "Capital Federal",
    "postal_code": "C1043AAZ",
    "notes": "Timbre roto, llamar al llegar"
  }
}
```

### Retiro en sucursal

Si tu comprador elige la opción de la cotización con `type: "pickup"`, el pedido no lleva dirección: se entrega en la sucursal que eligió. Cambian dos cosas del cuerpo:

* `shipping` lleva, además de `quote_id` y `service_code` (el de la opción de retiro), `pickup_point_id`: el `id` de la sucursal elegida, de `pickup_points` de **esa** opción y **esa** cotización.
* `shipping_address` **no va**. Mandar las dos cosas (o ninguna) responde 400 `validation_error`.

`recipient` sigue siendo obligatorio: es quien retira, y su nombre y su teléfono van a la etiqueta. Todo lo demás (precio, `external_id`, vencimiento, pago, estados, avisos) funciona igual que a domicilio.

El flujo completo, listo para copiar (usa `jq`):

```bash theme={null}
# 1. Cotizar con el código postal de tu comprador (el retiro se ofrece por zona).
QUOTE=$(curl -sS "$GUDINK_API_BASE/shipping/quotes" \
  -H "Authorization: Bearer $GUDINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "postal_code": "C1043AAZ",
    "items": [{ "product_id": 812, "variant_id": 4410, "quantity": 2 }]
  }')

# 2. La opción de retiro y las sucursales que le vas a mostrar a tu comprador.
QUOTE_ID=$(echo "$QUOTE" | jq -r '.quote_id')
PICKUP=$(echo "$QUOTE" | jq -c '.options[] | select(.type == "pickup")')
SERVICE_CODE=$(echo "$PICKUP" | jq -r '.service_code')
echo "$PICKUP" | jq -r '.pickup_points[] | "\(.id)  \(.name), \(.address.street) \(.address.number // "S/N"), \(.address.city) (\(.carrier))"'

# 3. Tu comprador elige una (acá, la primera) y se crea el pedido, sin shipping_address.
PICKUP_POINT_ID=$(echo "$PICKUP" | jq -r '.pickup_points[0].id')
curl -sS "$GUDINK_API_BASE/orders" \
  -H "Authorization: Bearer $GUDINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg quote_id "$QUOTE_ID" --arg service_code "$SERVICE_CODE" --arg pickup_point_id "$PICKUP_POINT_ID" '{
    external_id: "woo-k7p2-10433",
    items: [{ product_id: 812, variant_id: 4410, quantity: 2 }],
    recipient: { name: "Ana Pérez", phone: "+54 9 11 5555 5555" },
    shipping: { quote_id: $quote_id, service_code: $service_code, pickup_point_id: $pickup_point_id }
  }')"
```

El cuerpo que manda el paso 3, con los valores de la cotización de ejemplo de [Cotizar el envío](/api/productos-y-envio#cotizar-el-envio):

```json theme={null}
{
  "external_id": "woo-k7p2-10433",
  "items": [{ "product_id": 812, "variant_id": 4410, "quantity": 2 }],
  "recipient": { "name": "Ana Pérez", "phone": "+54 9 11 5555 5555" },
  "shipping": {
    "quote_id": "sq_9f3kQ2v7c1ZxLwP0aB8mT4yR6eHn5uJd",
    "service_code": "opt_3",
    "pickup_point_id": "pp_1"
  }
}
```

En la respuesta (y en `GET /orders/:id` y en los avisos), un retiro sale así:

```json theme={null}
{
  "delivery_type": "pickup",
  "pickup_point": {
    "name": "Sucursal Microcentro",
    "carrier": "Operador logístico",
    "address": {
      "street": "Sarmiento",
      "number": "1120",
      "city": "Capital Federal",
      "province": "Capital Federal",
      "postal_code": "1041"
    },
    "hours": "Lun 09:00-18:00, Mar 09:00-18:00, Mié 09:00-18:00, Jue 09:00-18:00, Vie 09:00-18:00"
  },
  "shipping_address": null
}
```

| Campo | A domicilio | Retiro en sucursal |
| - | - | - |
| `delivery_type` | `home` | `pickup` |
| `pickup_point` | `null` | La sucursal elegida: `name`, `carrier`, `address` y `hours` |
| `shipping_address` | La dirección de tu comprador | `null` |

* Cuando el pedido sale (`shipped`), el seguimiento funciona igual que a domicilio. Avisale a tu comprador que lo retire en esa sucursal, con su documento: Gudink no le escribe.
* Errores propios del retiro, además de los de siempre:
  * 400 `pickup_point_required`: la opción elegida es un retiro y mandaste `shipping_address` en lugar de `shipping.pickup_point_id`.
  * 409 `pickup_point_mismatch`: el `pickup_point_id` no es una sucursal de esa opción en esa cotización, o la opción elegida es a domicilio y mandaste una sucursal.
* En un retiro, `quote_mismatch` compara las líneas y que el `service_code` esté en esa cotización: no hay código postal ni provincia del pedido que comparar.
* En qué orden se deciden: ver [Por qué se rechaza un pedido](#por-que-se-rechaza-un-pedido).

## Qué va en cada campo

**Validá estos formatos en tu checkout antes de cobrarle a tu comprador.** El pedido se crea cuando tu comprador ya pagó, y un campo inválido vuelve como 400 `validation_error`.

| Campo | ¿Obligatorio? | Regla |
| - | - | - |
| `external_id` | Sí | Único en toda tu cuenta de Gudink y para siempre: un prefijo propio de tu instalación más tu número de pedido (ver [No duplicar pedidos](#no-duplicar-pedidos)). De 1 a 120 caracteres entre `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` y `-` |
| `items` | Sí | De 1 a 20 líneas, de 1 a 20 unidades por línea, 100 unidades por pedido en total. Cada línea lleva sólo `product_id`, `variant_id` y `quantity`. Una línea es siempre el par `product_id` + `variant_id` (ver [Una línea es un par](/api/productos-y-envio#una-linea-es-un-par)); mandá cada par una sola vez, con la cantidad sumada |
| `recipient.name` | Sí | De 1 a 120 caracteres. El nombre de **tu comprador**: va a la etiqueta del paquete |
| `recipient.phone` | Sí | De 6 a 30 caracteres. Sólo dígitos, espacios y `+ ( ) - .` |
| `shipping_address` | A domicilio, sí. En un retiro en sucursal, no va | En un retiro va `shipping.pickup_point_id` en su lugar (ver [Retiro en sucursal](#retiro-en-sucursal)). Las filas `shipping_address.*` que siguen valen cuando va |
| `shipping_address.street` | Sí | De 1 a 120 caracteres. La calle, **sin** la altura |
| `shipping_address.number` | Sí | De 1 a 20 caracteres. La altura (`1234`, `1234 bis`). Si la dirección no tiene altura, mandá `S/N`. Si tu plataforma la guarda junto con la calle, ver [Dirección en una sola línea](#si-tu-plataforma-guarda-la-direccion-en-una-sola-linea) |
| `shipping_address.floor` | No | Hasta 20 caracteres |
| `shipping_address.apartment` | No | Hasta 20 caracteres |
| `shipping_address.city` | Sí | De 1 a 80 caracteres. No se valida contra el código postal |
| `shipping_address.province` | Sólo si la API no reconoce el código postal | Una de las [24 provincias](/api/productos-y-envio#provincias). **Mandá la `province` que devolvió la cotización** |
| `shipping_address.postal_code` | Sí | Al menos 4 dígitos, hasta 16 caracteres. Sus 4 dígitos tienen que ser los de la cotización |
| `shipping_address.notes` | No | Hasta 300 caracteres |
| `shipping.quote_id` | Sí | Una cotización tuya y vigente, con el mismo código postal y las mismas líneas |
| `shipping.service_code` | Sí | Uno de los `service_code` de esa cotización (`opt_1`, `opt_2`, ...), copiado tal cual. Sólo vale junto a su `quote_id`: el `opt_1` de otra cotización puede ser otro envío |
| `shipping.pickup_point_id` | Sólo en un retiro en sucursal, y ahí sí | El `id` de una sucursal de `pickup_points` de la opción elegida (`pp_1`, `pp_2`, ...), hasta 32 caracteres. Sólo vale junto a su `quote_id` |

Reglas que evitan un rechazo después de cobrar:

* **Teléfono:** `11 5555-5555 int 4` no pasa, porque tiene letras. Mandá `11 5555-5555`.
* **Provincia:** si la API reconoce el código postal, la provincia sale del código postal y la que mandes se ignora, no se rechaza. Si no lo reconoce y no la mandás, responde 400 `province_required`. Mandando siempre la `province` de la cotización no tenés que distinguir los dos casos.
* **Código postal:** `1043` y `C1043AAZ` cotizan igual (cuentan los 4 dígitos), así que podés cotizar con uno y crear el pedido con el otro. Pero para reintentar el mismo pedido tenés que mandar el mismo texto: ver [No duplicar pedidos](#no-duplicar-pedidos).
* **No mandes nombre, talle, color ni precio del producto:** los pone Gudink a partir del producto y la variante.
* **Cualquier campo que no esté en esta tabla** responde 400 `validation_error` ("Campo desconocido").
* No hay cupones ni descuentos por API.

El operador logístico usa el nombre y el teléfono para coordinar la entrega. Gudink no le escribe a tu comprador.

### Si tu plataforma guarda la dirección en una sola línea

`street` y `number` van separados, y separados se entregan mejor: la altura se busca en su propio campo. Si tu checkout guarda "Av. Corrientes 1234" en un solo campo, lo mejor es pedirle la altura a tu comprador en un campo aparte. Si no podés, separala así (la altura es el último número de la línea; sin número al final, `S/N`):

```js theme={null}
function splitStreetLine(line) {
  const text = String(line ?? "").replace(/\s+/g, " ").trim();
  // El último número de la línea, con una letra o "bis" opcional, después de "N°", "Nº", "nro" o "#" opcionales.
  const m = text.match(/^(.*?[^\s,])[\s,]+(?:(?:n[°º.]?|nro\.?|n[uú]mero|#)\s*)?(\d{1,6}(?:\s?[a-z]|\s+bis)?)$/i);
  if (!m || !/[a-záéíóúñ]/i.test(m[1])) return { street: text, number: "S/N" };
  return { street: m[1], number: m[2] };
}
// splitStreetLine("Av. Corrientes 1234")    -> { street: "Av. Corrientes", number: "1234" }
// splitStreetLine("San Martín N° 450 bis")  -> { street: "San Martín", number: "450 bis" }
// splitStreetLine("Ruta 2 km")              -> { street: "Ruta 2 km", number: "S/N" }
```

* Revisá las calles que tienen número en el nombre (`Calle 50`, `Ruta 3`) y las líneas que traen también piso y departamento (`Belgrano 1234 3B`): la función no puede saber qué número es la altura. Mostrale a tu comprador cómo quedó antes de cobrarle.
* La API acepta la línea entera en `street` con `number: "S/N"`, pero el operador logístico tiene que adivinar la altura: usalo sólo como último recurso.
* **Piso y departamento:** si tu plataforma tiene un solo campo libre ("Piso 3, depto B"), mandalo en `apartment` si entra en 20 caracteres; si es más largo, mandalo en `notes` (hasta 300). No lo cortes: un texto cortado confunde más que uno en las notas.

### Qué compara la cotización con el pedido

Si la cotización no coincide con el pedido, el alta responde 409 `quote_mismatch`. Se comparan cuatro cosas:

| Qué | Cómo se compara |
| - | - |
| Código postal | Por sus 4 dígitos: `1043` y `C1043AAZ` son el mismo |
| Líneas | Mismo producto, misma variante y misma cantidad. El orden no importa |
| `service_code` | Tiene que ser uno de los de esa cotización |
| Provincia | Tiene que ser la que devolvió la cotización (ver abajo) |

Con un código postal que la API reconoce, la provincia que mandes **se ignora, no se rechaza**: el pedido sale con la del código postal. Por ejemplo, si mandás `Salta` con el código postal `1425`, el pedido queda en `Capital Federal`, sin error. Sólo cuando tuviste que mandarla vos (un código postal que la API no reconoce) una provincia distinta de la cotizada responde 409 `quote_mismatch`.

En un retiro en sucursal no hay dirección: se comparan sólo las líneas y el `service_code`.

## No duplicar pedidos

Las conexiones se cortan. Si mandaste un pedido y no recibiste respuesta, no sabés si se creó. Para eso está `external_id`.

### Un id único por instalación

* **Es único en toda tu cuenta de Gudink y para siempre**: un pedido cancelado lo sigue ocupando.
* **No uses sólo el número de pedido de tu tienda.** Ese número se repite si reinstalás la tienda, si tenés una copia de pruebas de tu sitio o si conectás más de una tienda a la misma cuenta de Gudink. Armalo con un prefijo propio de cada instalación más tu número de pedido, por ejemplo `woo-k7p2-10432`: `k7p2` es un código que generás una sola vez al instalar tu integración y guardás en su configuración.
* **Qué pasa si choca** (el mismo `external_id` para dos compras distintas): casi siempre 409 `external_id_conflict`, con el pedido viejo en `details.order_id`. No se arregla reintentando: es tu `external_id` que no es único. Si el cuerpo fuera idéntico (mismas líneas, mismo destinatario, misma dirección y la misma cotización), recibirías `200` con el pedido viejo como si fuera el tuyo. Por eso el prefijo por instalación no es opcional.

### Reintentar sin duplicar

* **Reintentá siempre con el mismo `external_id` y el mismo cuerpo.** Si el pedido ya existía, la API responde `200` con ese pedido y no crea otro.
* **El `200` devuelve el pedido como está hoy, que puede estar `cancelled`.** Ver [Qué mirar después de cada 200 o 201](#que-mirar-despues-de-cada-200-o-201).
* **Nunca generes un `external_id` nuevo para reintentar.** Crearías un segundo pedido, que también se te cobra.
* **Si reintentás en el primer minuto**, mientras el alta original todavía se está completando, puede responder 409 `order_in_progress` con la cabecera `Retry-After` (2 segundos). Esperá eso y repetí la misma llamada. Puede terminar en `200` (el pedido quedó) o en un alta nueva, si el original se anuló por falta de stock. Es raro: dos altas iguales casi simultáneas suelen dar `201` y `200`. Que nunca lo veas no es un error.
* **El mismo `external_id` con otro cuerpo** (otras líneas, otro destinatario, otra dirección u otro envío) responde 409 `external_id_conflict`. El pedido que ya existía está en `details.order_id`.
* **Un intento rechazado no crea nada ni reserva el `external_id`.** Cualquier error del alta (por ejemplo `quote_expired`, `quote_mismatch`, `out_of_stock` o `validation_error`) deja el `external_id` libre: podés volver a mandarlo con otro contenido, por ejemplo con una cotización nueva. Lo único que lo ocupa es un pedido creado (`201`).
* **Un pedido cancelado sigue ocupando su `external_id`.** Volver a pedir lo mismo es un pedido nuevo, con un `external_id` nuevo.
* **Antes de mandar un cuerpo distinto con un `external_id` que ya mandaste** (por ejemplo, recotizaste porque la cotización venció mientras reintentabas un timeout), consultá `GET /orders?external_id=<tu id>`. Si `data` trae un pedido, el intento que creías perdido sí se creó: usá ése. Si viene vacío, el `external_id` está libre y podés mandar el cuerpo nuevo.

### Qué mirar después de cada 200 o 201

Un 2xx de `POST /orders` quiere decir "éste es el pedido con ese `external_id`", no "el pedido sigue en pie". Si lo cancelaste (o venció) y reenviás el mismo cuerpo, recibís `200` con ese pedido cancelado: no se crea otro. Después de **cada** `200` o `201`, tu tienda tiene que mirar `status`:

1. **`status` es `cancelled`:** ese pedido no se va a fabricar. No le confirmes nada a tu comprador con él. Para volver a pedirlo, creá un pedido nuevo con un `external_id` nuevo (por ejemplo `woo-k7p2-10432-2`).
2. **Cualquier otro `status`:** el pedido existe y sigue su curso. Ver [Estados](#estados).

### Cómo se compara "el mismo cuerpo"

La comparación es normalizada, no byte a byte:

| Diferencia | ¿Es el mismo cuerpo? |
| - | - |
| Espacios al principio o al final de un texto | Sí, se ignoran |
| Un campo opcional ausente contra el mismo campo vacío (`"notes": ""`) | Sí |
| Las líneas en otro orden | Sí |
| La provincia escrita de otra forma válida (`CABA` contra `Capital Federal`) | Sí, se compara por su nombre exacto de la lista |
| El código postal con otras mayúsculas (`c1043aaz` contra `C1043AAZ`) | Sí |
| El código postal en otro formato (`1043` contra `C1043AAZ`) | **No**, aunque coticen igual |
| Un espacio en el medio de un texto | **No** |
| Una mayúscula distinta en el nombre | **No** |
| Otro `quote_id` | **No** |

**Guardá el cuerpo que mandaste y reenviá exactamente ése. No lo vuelvas a armar desde el pedido de tu plataforma**, que puede haber normalizado el código postal, la provincia o el teléfono entre el checkout y el alta.

Qué hacer cuando te quedaste sin reintentos y no sabés si el pedido se creó: ver [Reintentos](/api/errores-y-limites#reintentos).

## Qué te cobra Gudink

`amounts.subtotal` (la suma de `items[].total_price`) más `amounts.shipping` (el precio de la opción de envío que elegiste) es `amounts.total`. Todo en pesos enteros con IVA incluido. La factura sale a nombre de tu cuenta, como cualquier pedido tuyo.

`items[].unit_price` es el precio del momento en que creaste el pedido: es lo que se te cobra aunque el producto cambie de precio después.

## El pago y el vencimiento

* El pedido nace con `status: "pending_payment"` y `payment.status: "pending"`. Te mandamos un mail con el link de pago.
* `payment.pay_url` es el pedido en **tu** panel de Gudink. Ahí lo pagás con transferencia o Mercado Pago, y podés pagar varios juntos. No es un link para tu comprador.
* Cuando se acredita el pago, el pedido pasa a `paid` y entra a producción.
* **Un pedido sin pagar se cancela solo a las 72 horas de creado.** `expires_at` dice exactamente cuándo. El mail con el link de pago y el recordatorio también dicen la fecha y la hora.
* `expires_at` es `null` una vez pagado o cancelado, y mientras dura una revisión de Gudink. Ver [Transiciones de estado](#transiciones-de-estado).
* Si hay un pago tuyo en curso justo en el momento del vencimiento, Gudink espera a que se resuelva antes de cancelar.
* Al cancelarse por vencimiento recibís el aviso `order.cancelled` con `cancel_reason: "unpaid_expired"`.
* Podés tener hasta 20 pedidos por API sin pagar a la vez. Con 20, el siguiente responde 409 `too_many_unpaid_orders`: pagá o cancelá alguno.
* En el entorno de pruebas no pagues desde `pay_url`: usá la [simulación](/api/entorno-de-pruebas#simular-el-recorrido-de-un-pedido).

Gudink no le cobra a tu comprador ni le escribe. Lo que tu comprador te pagó a vos es independiente de este pago.

### Pagar es una tarea de todos los días

Tu comprador ya te pagó a vos, pero Gudink fabrica recién cuando vos pagás el pedido. Si no lo pagás antes de `expires_at`, se cancela y tu comprador se queda sin su producto aunque te haya pagado. La API no paga pedidos: se pagan en tu panel, y podés pagar varios juntos.

Armá tu propia alarma antes del vencimiento, por ejemplo una vez por hora:

1. Pedí `GET /orders?status=pending_payment` y seguí con `next_cursor` hasta que sea `null`.
2. Para cada pedido con `expires_at` en las próximas 24 horas, avisate a vos (un mail, un mensaje interno) con `payment.pay_url`.
3. Si igual vence, vas a ver `status: "cancelled"` con `cancel_reason: "unpaid_expired"`: decidí vos si lo volvés a pedir (con un `external_id` nuevo) o le devolvés la plata a tu comprador.

No hay un aviso (webhook) de "está por vencer": `expires_at` ya viene en cada pedido y en `GET /orders`, y no cambia (salvo que una revisión de Gudink lo frene). Programá la alarma con el cron del servidor, no con tareas que dependan de las visitas a tu sitio.

`payment.status` es informativo. **Lo que manda es `status`:**

| `payment.status` | Qué significa |
| - | - |
| `pending` | Sin pagar |
| `failed` | Un intento de pago falló. Sigue sin pagar: `status` sigue en `pending_payment` |
| `paid` | Pagado |
| `refunded` | Se devolvió el pago. El pedido está `cancelled` |
| `voided` | Cancelado sin cobro. El pedido está `cancelled`. Es lo que ves en un pedido que se canceló antes de pagarse (por vos, por vencimiento o por Gudink): no quiere decir que haya habido un pago |

## Por qué se rechaza un pedido

Las validaciones corren en este orden. Responde la primera que falla:

| Orden | Status | `error.code` | Qué pasó | Qué hacer |
| - | - | - | - | - |
| 1 | 400 | `validation_error` | El cuerpo no cumple el formato | Corregí los campos de `details.fields` |
| 1 | 400 | `province_required` | La API no reconoce el código postal y falta `shipping_address.province` | Mandá la provincia |
| 2 | 200 o 409 | (pedido existente), `external_id_conflict`, `order_in_progress` | Ya hay un pedido con ese `external_id` | Con `200`, mirá su `status`. Ver [No duplicar pedidos](#no-duplicar-pedidos) |
| 3 | 403 | `billing_data_required` | Faltan datos de facturación en tu cuenta | Completá en tu panel lo que dice `details.missing_fields` |
| 4 | 404 | `not_found` | Algún `product_id` no existe o no es tuyo | Revisá los `product_id` |
| 5.1 | 409 | `product_archived` | Algún producto se archivó | Ver `details.product_ids` |
| 5.2 | 409 | `variant_not_orderable` | Alguna variante no se puede pedir de su producto | Ver `details.items` y volvé a leer el producto |
| 5.3 | 409 | `quote_expired` | La cotización venció o no existe | Cotizá de nuevo y reintentá con el mismo `external_id` |
| 5.4 | 409 | `quote_mismatch` | La cotización no coincide con el pedido | Cotizá con las líneas y el código postal del pedido, y mandá su `province` |
| 5.5 | 400 o 409 | `pickup_point_required`, `pickup_point_mismatch` | El tipo de entrega no coincide con la opción elegida: es un retiro y mandaste dirección (400), o la sucursal no es de esa opción, o la opción es a domicilio (409) | Ver [Retiro en sucursal](#retiro-en-sucursal) |
| 5.6 | 409 | `too_many_unpaid_orders` | Tenés 20 pedidos por API sin pagar | Pagá o cancelá alguno |
| 6 | 409 | `out_of_stock` | No hay stock suficiente de alguna variante | Ver `details.items` y [Stock y reservas](#stock-y-reservas). Podés reintentar más tarde con el mismo `external_id` |

Ningún rechazo de los pasos 1 y 3 a 6 crea un pedido ni ocupa el `external_id`.

Los rechazos del paso 5 se deciden en el orden de la tabla (5.1 a 5.6): si hay más de un problema, responde el primero.

Sobre `out_of_stock`:

* No queda ningún pedido en la API. Si el stock se agotó en el último instante, en tu panel puede verse uno cancelado, que la API no lista y que no se te cobra.
* Si lo recibís después de haberle cobrado a tu comprador, tu sistema tiene que resolverlo: ofrecer otra variante, esperar la reposición o devolver el dinero. Para reducir el riesgo, leé `GET /products/:id` justo antes de cobrar.

## Stock y reservas

* `available` (en `GET /products`) es una foto: dice que en ese momento hay al menos una unidad para pedir. No dice cuántas, y puede cambiar un segundo después.
* **Crear un pedido reserva sus unidades** hasta que se paga, se cancela o vence (72 horas sin pagar). Mientras tanto esas unidades no están disponibles para otro pedido, tampoco para los tuyos.
* Por eso un pedido puede responder 409 `out_of_stock` aunque la variante diga `available: true`: por ejemplo, si pedís más unidades de las que hay, o si tus propios pedidos sin pagar ya reservaron lo que quedaba.
* **Cancelar un pedido sin pagar libera sus unidades en el momento** (`POST /orders/{id}/cancel`). Cancelá los pedidos que no vas a pagar, en especial los de prueba: si los dejás vencer, retienen stock hasta 72 horas.

Qué hacer con un `out_of_stock`:

1. Mirá `details.items`: dice qué líneas no alcanzan.
2. **No reintentes en bucle.** Avisale a tu comprador y elegí: esperar y reintentar más tarde el mismo cuerpo, o cambiar el carrito (por ejemplo, menos unidades).
3. Si cambiás el carrito, cotizá de nuevo con las líneas nuevas. El `external_id` sigue libre, porque un rechazo no lo ocupa.
4. Releé `GET /products/:id` antes de volver a ofrecer la variante.

Además puede responder los errores de toda ruta con clave (401, 403, 429, 500, 503), los del cuerpo (400 `invalid_body`, 413 `payload_too_large`) y 409 `conflict` (conflicto momentáneo: reintentá con el mismo `external_id`). Qué trae `details` en cada uno: ver [Errores y límites](/api/errores-y-limites#que-trae-details).

## Consultar pedidos

Las rutas de lectura (`GET /me`, `/products`, `/products/:id`, `/orders`, `/orders/:id`) sólo pueden responder: 400 `validation_error` (un parámetro de la consulta mal formado o desconocido), 401, 403, 404 `not_found`, 429 y 5xx.

Un pedido por su id:

```bash theme={null}
curl -sS "$GUDINK_API_BASE/orders/1043" \
  -H "Authorization: Bearer $GUDINK_API_KEY"
```

Responde el pedido, igual que el `201` del alta, con el estado, los importes, el pago y el seguimiento al día. Un id que no existe o no es tuyo responde 404 `not_found`.

Por tu `external_id`:

```bash theme={null}
curl -sS "$GUDINK_API_BASE/orders?external_id=woo-k7p2-10432" \
  -H "Authorization: Bearer $GUDINK_API_KEY"
```

```json theme={null}
{
  "data": [
    { "id": 1043, "number": "GU-1000412", "external_id": "woo-k7p2-10432", "status": "pending_payment" }
  ],
  "next_cursor": null
}
```

Cada elemento de `data` es el pedido completo. En el ejemplo se recortó. Si no hay ningún pedido con ese `external_id`, `data` viene vacío.

### Listar y filtrar

`GET /orders` lista sólo los pedidos creados por la API, del más nuevo al más viejo.

| Parámetro | Regla |
| - | - |
| `limit` | De 1 a 25. Por defecto 20 |
| `cursor` | El `next_cursor` de la página anterior |
| `external_id` | Tu id del pedido |
| `status` | Uno de los valores de la tabla de [Estados](#estados) |
| `created_after` | ISO 8601 con zona (`2026-10-02T15:04:05.000Z`). Trae los pedidos creados después de ese instante |
| `updated_after` | ISO 8601 con zona. Trae los pedidos con `updated_at` posterior a ese instante: cambió el estado, el pago o el seguimiento. Puede traer algún pedido sin un cambio que te importe: compará con lo que tenés |

* Con el filtro `status`, una página puede traer menos resultados que `limit` aunque haya más: seguí con `next_cursor` hasta que sea `null`.
* Los pedidos de Tiendanube y los que cargaste a mano en tu panel no aparecen por la API.

### updated\_at

`updated_at` cambia con cada cambio de estado, de pago o de seguimiento. Sirve para dos cosas:

* **Ordenar una consulta contra un aviso que llegó tarde:** comparalo con el `created_at` del aviso para saber cuál es más nuevo.
* **Ponerte al día:** pedí `GET /orders?updated_after=<la última vez que te pusiste al día>` y actualizá los pedidos cuyo `updated_at` sea más nuevo que el que tenés guardado.

### Campos que pueden ser null

| Campo | Cuándo es `null` |
| - | - |
| `hold_reason` | Siempre que `status` no sea `on_hold` |
| `cancel_reason` | Siempre que `status` no sea `cancelled`, o si el motivo no quedó registrado |
| `expires_at` | Pedido pagado, cancelado o en revisión de Gudink |
| `items[].product_id` | El diseño se borró después de crear el pedido |
| `items[].variant_id` | La variante se borró después de crear el pedido |
| `items[].size`, `items[].color` | Pueden venir en `null` |
| `tracking.number`, `tracking.url`, `tracking.carrier` | Antes del despacho. `url` puede seguir en `null` después |
| `recipient.name`, `recipient.phone` | Sólo en pedidos viejos sin destinatario guardado |
| `shipping_address` | Entero en un retiro en sucursal (`delivery_type: "pickup"`) |
| `shipping_address.number`, `floor`, `apartment`, `notes` | Pueden venir en `null` |
| `pickup_point` | En un envío a domicilio (`delivery_type: "home"`) |
| `pickup_point.carrier`, `address.number`, `address.postal_code`, `hours` | Pueden venir en `null` |

### El seguimiento

`tracking` se completa cuando el pedido se despacha:

| Campo | Qué es |
| - | - |
| `tracking.number` | El número de seguimiento del operador logístico |
| `tracking.url` | La página para seguirlo. Puede ser `null` |
| `tracking.carrier` | El nombre del operador logístico, listo para mostrarle a tu comprador |

Con algunos operadores el número llega un rato después del despacho: te enterás con el aviso `order.tracking_updated`.

Pasarle el seguimiento a tu comprador es tarea tuya.

## Estados

`status` tiene un solo valor, de esta tabla. Si se cumple más de una condición, gana la primera fila:

| `status` | Qué significa | Qué hacés |
| - | - | - |
| `cancelled` | Cancelado (por vos, por vencimiento, por Gudink, o por un pago anulado o reembolsado). El motivo está en `cancel_reason` | Nada más va a pasar. Si tu comprador ya te pagó, volvé a pedirlo con un `external_id` nuevo o devolvele la plata |
| `on_hold` | Detenido. El motivo está en `hold_reason` | Esperá. Si dura, escribinos a soporte |
| `pending_payment` | Esperando tu pago | Pagalo desde `payment.pay_url` antes de `expires_at` |
| `delivered` | Entregado | Fin |
| `delivery_failed` | El operador no pudo entregarlo | Escribinos a soporte |
| `shipped` | Despachado. Ver `tracking` | Pasale el seguimiento a tu comprador |
| `ready_to_ship` | Producido, esperando el despacho | Esperá |
| `in_production` | En producción | Esperá |
| `paid` | Pagado, todavía no entró a producción | Esperá |

Motivos de `hold_reason` (sólo cuando `status` es `on_hold`; si no, `null`):

| `hold_reason` | Qué significa |
| - | - |
| `blocked` | Gudink lo bloqueó: escribinos a soporte |
| `under_review` | En revisión manual de Gudink |
| `paused` | Pausado, por vos o por Gudink |
| `awaiting_labels` | Lleva etiquetas de marca y falta el paquete de etiquetas |

Motivos de `cancel_reason` (sólo cuando `status` es `cancelled`; si no, `null`):

| `cancel_reason` | Qué significa |
| - | - |
| `unpaid_expired` | Venció sin pagar: pasó `expires_at` |
| `cancelled_by_seller` | Lo cancelaste vos, por la API o en el panel |
| `cancelled_by_gudink` | Lo canceló Gudink: escribinos a soporte si no sabés por qué |
| `null` | El motivo no quedó registrado (por ejemplo, pedidos cancelados antes de que existiera el campo, o una devolución del pago) |

Si aparece un valor de `status` o de `hold_reason` que tu código no conoce, tratalo como "en curso", y uno de `cancel_reason` que no conocés, como `null`, hasta actualizar tu código. Ver [Versiones](/api/errores-y-limites#versiones).

## Transiciones de estado

El camino normal:

```
pending_payment -> paid -> in_production -> ready_to_ship -> shipped -> delivered
                                                                    \-> delivery_failed
```

| Regla | Detalle |
| - | - |
| Puede saltear pasos | Tu sistema puede ver un pedido pasar de `pending_payment` directo a `in_production`, si las dos cosas pasaron entre dos consultas |
| `cancelled` desde cualquier estado | También uno pagado, si Gudink devuelve o anula el pago |
| `cancelled` es final | No se reabre. Volver a pedir lo mismo es un pedido nuevo, con un `external_id` nuevo: el viejo queda ocupado por el pedido cancelado |
| `on_hold` desde cualquier estado antes de terminar | Cuando se resuelve, vuelve al estado que corresponda |
| Sin aviso propio | `ready_to_ship`, `delivery_failed` y la vuelta a `pending_payment`. Ver [Eventos](/api/avisos#eventos) |

### Un pedido en revisión antes de pagarlo

Gudink puede revisar un pedido antes de que lo pagues: `pending_payment` -> `on_hold` (con `hold_reason: "under_review"`) -> `pending_payment`.

| Momento | `status` | `expires_at` | Qué hacés |
| - | - | - | - |
| Recién creado | `pending_payment` | 72 horas desde que se creó | Pagalo |
| Durante la revisión | `on_hold`, `hold_reason: "under_review"` | `null`: no vence | Nada. No se puede pagar mientras dura |
| Termina la revisión | `pending_payment` | El mismo vencimiento de antes: 72 horas desde que se creó, no 72 horas nuevas | Pagalo |

* Si al terminar la revisión ese vencimiento ya pasó, el pedido se cancela dentro de la hora siguiente.
* Llega el aviso `order.on_hold` cuando entra en revisión. La vuelta a `pending_payment` no tiene aviso propio: consultá el pedido.
* Mostrá "pagá este pedido" sólo cuando `status` es `pending_payment`.

## Cancelar

```bash theme={null}
curl -sS -X POST "$GUDINK_API_BASE/orders/1043/cancel" \
  -H "Authorization: Bearer $GUDINK_API_KEY"
```

| Estado del pedido | Qué responde |
| - | - |
| `pending_payment` | `200` con el pedido en `"status": "cancelled"`, `"expires_at": null` y `"cancel_reason": "cancelled_by_seller"`. Sus unidades reservadas se liberan en el momento |
| `cancelled` | `200` sin cambios |
| Cualquier otro (pagado, en producción, `on_hold`, …) | 409 `cancel_requires_panel` |
| Se pagó mientras lo cancelabas | 409 `cancel_requires_panel` |

* La llamada va sin cuerpo.
* **Por API sólo se cancela un pedido que todavía no pagaste.** Cancelar un pedido pago implica un reembolso, y eso se hace desde tu panel, no con una clave. Ver [Cancelación de pedidos](/pedidos/cancelacion-de-pedidos).
* Límite: 30 cancelaciones por minuto por cuenta.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.