> ## 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: listar productos y cotizar el envío

> Cómo traer tus productos de Gudink con sus variantes, precio y disponibilidad, qué hacer con las imágenes, cada cuánto sincronizar, cómo cotizar el envío de un carrito (a domicilio o con retiro en sucursal) y la lista exacta de provincias.

Antes de crear un pedido necesitás dos cosas: saber qué productos y variantes podés pedir, y cotizar el envío del carrito.

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
```

## Listar tus productos

Un producto es un diseño tuyo en Gudink que no está archivado. No hace falta que esté publicado en ningún canal: alcanza con que exista en tu cuenta.

```bash theme={null}
curl -sS "$GUDINK_API_BASE/products?limit=25" \
  -H "Authorization: Bearer $GUDINK_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": 812,
      "name": "Remera Logo Sur",
      "product_type": "tshirt",
      "thumbnail_url": "https://staging.gudink.com/...",
      "mockups": [
        { "url": "https://staging.gudink.com/...", "color": "Negro", "view": "front" }
      ],
      "orderable": true,
      "variants": [
        { "id": 4410, "size": "M", "color": "Negro", "available": true, "price": 15990 },
        { "id": 4411, "size": "L", "color": "Negro", "available": false, "price": 15990 }
      ],
      "updated_at": "2026-10-01T18:30:00.000Z"
    }
  ],
  "next_cursor": "eyJ1Ijo0MTIsImkiOjgxMn0"
}
```

### Qué es cada campo

| Campo | Qué es | ¿Puede ser `null`? |
| - | - | - |
| `id` | El id del producto. Lo mandás como `product_id` | No |
| `name` | El nombre del diseño | No |
| `product_type` | Uno de `tshirt`, `hoodie`, `totebag`, `mug`, `frame`, `lienzo`, `custom` | No |
| `thumbnail_url` | La imagen principal | Sí |
| `mockups[].url` | Una imagen del producto | No |
| `mockups[].color` | El color de esa imagen. Es el mismo texto que `variants[].color` | Sí (productos sin color por imagen, como cuadros y lienzos) |
| `mockups[].view` | La vista de esa imagen. Ver [Imágenes](#imagenes) | Sí |
| `orderable` | `false` si el producto no tiene ninguna variante que se pueda pedir | No |
| `variants[].id` | El id de la variante. Lo mandás como `variant_id`, siempre junto al `id` del producto (ver [Una línea es un par](#una-linea-es-un-par)) | No |
| `variants[].size` | El talle o tamaño | Sí |
| `variants[].color` | El color | Sí |
| `variants[].available` | Una foto del momento de la consulta: `true` quiere decir que ahora hay al menos una unidad para pedir. No garantiza la cantidad que quiera tu comprador ni que siga habiendo cuando crees el pedido (ver [Stock y reservas](/api/pedidos#stock-y-reservas)). La API nunca informa cantidades | No |
| `variants[].price` | Lo que te cobra Gudink por unidad de esa variante **en ese producto**, en pesos enteros con IVA. La misma variante en otro producto puede costar otra cosa. Tu precio de venta lo decidís vos | No |
| `updated_at` | El último cambio del diseño (nombre, imágenes). El precio y `available` pueden cambiar sin que cambie `updated_at` | No |

### Una línea es un par

**Una línea es siempre el par `product_id` + `variant_id`.** El `variant_id` solo no identifica nada: es la variante base (por ejemplo `L` / `Blanco`) y el mismo número aparece en varios de tus productos, cada uno con su precio.

* Guardá tus variantes con la clave `(product_id, variant_id)`, nunca sólo por `variant_id`.
* Mandá siempre el par, al cotizar y al crear el pedido.
* Una variante con el `product_id` de otro producto responde 409 `variant_not_orderable`.

### Paginado

* `limit` va de 1 a 25. Por defecto 20.
* Si `next_cursor` no es `null`, pedí la página siguiente con `cursor=<next_cursor>`. Cuando es `null`, terminaste.
* El cursor es opaco y sólo sirve para tu cuenta. No lo armes ni lo interpretes.

### Un producto solo

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

Responde el mismo objeto que un elemento de `data`. Un id que no existe o que no es de tu cuenta responde 404 `not_found`. Es siempre el mismo 404: la API no confirma si un id ajeno existe.

## Imágenes

**Descargá `thumbnail_url` y `mockups[].url` y alojalas en tu sitio.** No las enlaces directo desde Gudink: la dirección cambia cuando la imagen se regenera y la anterior deja de funcionar.

| Regla | Detalle |
| - | - |
| Son públicas | Se bajan con un `GET` común, **sin** la cabecera `Authorization`. Nunca le mandes tu clave a una URL de imagen |
| Vencimiento | No vencen mientras la imagen no se regenere |
| Límites | Bajarlas no cuenta para los límites de la API |
| Formato | PNG, JPEG o WebP. Guiate por la cabecera `Content-Type` de la respuesta, no por la extensión de la URL |
| Sin extensión | Las URLs no terminan en `.png` ni `.jpg`. Si tu plataforma sólo importa imágenes desde URLs con extensión (por ejemplo `media_sideload_image` de WordPress), bajá el archivo vos, ponele la extensión que corresponde al `Content-Type` y subilo desde ahí |
| `view` en prendas y bolsas | Uno de `front`, `back`, `folded`, `model_front`, `model_back`, `model_side` |
| `view` en tazas | Pueden traer otras vistas, por ejemplo `three-quarter` |
| `view` en `null` | El mockup no tiene vista (cuadros, lienzos) |
| `view` desconocido | Tratalo como una foto más |
| `color` | Es el mismo texto que `variants[].color` de las variantes de ese color. Para mostrar la foto de la variante elegida, compará los dos textos tal cual |
| Cuándo volver a bajarlas | Guardá el `updated_at` de cada producto. Si en la próxima sincronización es el mismo, las imágenes no cambiaron |

## Cada cuánto sincronizar

| Qué | Cuándo |
| - | - |
| Catálogo completo (`GET /products` paginando hasta `next_cursor: null`) | Cada 60 minutos alcanza. Si tu catálogo casi no cambia, una vez por día |
| `GET /products/:id` de cada producto del carrito | Justo antes de cobrarle a tu comprador. No en cada visita a tu web |

* **No vendas una variante con `available: false`.** `available` puede cambiar en cualquier momento.
* Igual el alta del pedido puede responder 409 `out_of_stock`: `available: true` dice que hay al menos una unidad, no cuántas, y los pedidos sin pagar reservan unidades. Prepará tu checkout para eso. Ver [Stock y reservas](/api/pedidos#stock-y-reservas).
* **Un producto que desaparece** de `GET /products` (o que responde 404 en `GET /products/:id`) se archivó o dejó de ser tuyo: dejá de venderlo. Si igual llega a un pedido, el alta responde 409 `product_archived`.
* No hay aviso (webhook) de cambios de producto: un evento `product.updated` todavía no existe. Los avisos son sólo de pedidos.
* **Programá la sincronización con un reloj de verdad** (el cron del servidor). Si tu plataforma corre sus tareas programadas sólo cuando alguien visita el sitio (WP-Cron de WordPress, por ejemplo), en una tienda con poco tráfico pueden pasar horas sin sincronizar.

### Cuando cambia un precio

* **El pedido toma el precio del momento en que lo creás.** `items[].unit_price` del pedido es lo que se te cobra, aunque el producto cambie de precio después.
* Gudink te avisa por mail los cambios de precio antes de que entren en vigor, con la fecha.
* Desde ese día, tu próxima sincronización ya trae el precio nuevo en `variants[].price`.

## Cotizar el envío

Antes de crear el pedido, cotizá el envío para esas líneas y ese código postal. La cotización trae todos los tipos de entrega que ofrece Gudink: **envío a domicilio** (`type: "home"`) y **retiro en sucursal** (`type: "pickup"`, con las sucursales donde se puede retirar). Queda guardada 24 horas, y el pedido se cobra exactamente al precio de la opción que elijas.

```bash theme={null}
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 }]
  }'
```

Respuesta `201`:

```json theme={null}
{
  "quote_id": "sq_9f3kQ2v7c1ZxLwP0aB8mT4yR6eHn5uJd",
  "postal_code": "1043",
  "province": "Capital Federal",
  "expires_at": "2026-10-03T15:04:05.000Z",
  "options": [
    {
      "service_code": "opt_1",
      "type": "home",
      "label": "Envío a domicilio",
      "price": 6890,
      "min_business_days": 3,
      "max_business_days": 5,
      "pickup_points": []
    },
    {
      "service_code": "opt_2",
      "type": "home",
      "label": "Envío express a domicilio",
      "price": 9450,
      "min_business_days": 1,
      "max_business_days": 2,
      "pickup_points": []
    },
    {
      "service_code": "opt_3",
      "type": "pickup",
      "label": "Retiro en sucursal",
      "price": 5480,
      "min_business_days": 4,
      "max_business_days": 6,
      "pickup_points": [
        {
          "id": "pp_1",
          "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"
        },
        {
          "id": "pp_2",
          "name": "Sucursal Tribunales",
          "carrier": "Operador logístico",
          "address": {
            "street": "Talcahuano",
            "number": "560",
            "city": "Capital Federal",
            "province": "Capital Federal",
            "postal_code": "1013"
          },
          "hours": "Lun 10:00-17:00, Mar 10:00-17:00, Mié 10:00-17:00, Jue 10:00-17:00, Vie 10:00-17:00"
        }
      ]
    }
  ]
}
```

### Qué mandar

| Campo | Regla |
| - | - |
| `postal_code` | Obligatorio. El código de 4 dígitos (`1043`) o el CPA (`C1043AAZ`), hasta 16 caracteres. Se cotiza por los 4 dígitos |
| `province` | Opcional. Sólo hace falta si la API no reconoce el código postal. Una de las [24 provincias](#provincias) |
| `items` | De 1 a 20 líneas. Cada una con `product_id`, `variant_id` y `quantity` (de 1 a 20) |

### Qué devuelve

| Campo | Qué es |
| - | - |
| `quote_id` | Lo que va en `shipping.quote_id` al crear el pedido |
| `postal_code` | Los 4 dígitos con los que se cotizó (`C1043AAZ` cotiza como `1043`) |
| `province` | Siempre uno de los 24 nombres exactos. **Mandá esta misma `province` en `shipping_address.province` del pedido** |
| `expires_at` | 24 horas después de cotizar. Después, el alta del pedido responde 409 `quote_expired` |
| `options[].service_code` | Lo que va en `shipping.service_code` al crear el pedido (`opt_1`, `opt_2`, ...) |
| `options[].type` | Qué entrega es: `home` (a domicilio) o `pickup` (retiro en sucursal) |
| `options[].label` | El texto de la opción, para mostrar |
| `options[].price` | Lo que te cobra Gudink a vos por el envío, en pesos enteros con IVA |
| `options[].min_business_days`, `max_business_days` | Los días hábiles estimados en total, sumando producción y envío |
| `options[].pickup_points` | Sólo en la opción `pickup`: las sucursales donde se puede retirar, hasta 10, las más cercanas al código postal primero. En una opción `home` es una lista vacía |
| `options[].pickup_points[].id` | El id de la sucursal (`pp_1`, `pp_2`, ...). Lo que va en `shipping.pickup_point_id` al crear el pedido |
| `options[].pickup_points[].name`, `address`, `hours` | El nombre, la dirección y el horario de la sucursal. `hours` es texto para personas |
| `options[].pickup_points[].carrier` | El nombre del operador logístico que atiende la sucursal |

### Reglas de la cotización

* **`service_code` identifica una opción dentro de esa cotización** y sólo vale junto con su `quote_id`. Copialo tal cual en el pedido.
* **No se garantiza que sea el mismo en dos cotizaciones del mismo carrito:** el `opt_1` de una cotización nueva puede ser otro envío. Si recotizás, elegí de nuevo entre las opciones nuevas, por `label` y `price`.
* Las líneas tienen que ser exactamente las del pedido: mismo producto, misma variante, misma cantidad. El orden no importa. Si cambia el carrito, cotizá de nuevo.
* Una cotización vigente sirve para más de un pedido con las mismas líneas y el mismo código postal.
* **Cotizar no mira el stock.** El stock se reserva recién al crear el pedido.
* **Mandá cada par `product_id` + `variant_id` una sola vez, con la cantidad sumada.** Si repetís el par en dos líneas, la cotización y el pedido lo aceptan y quedan como dos líneas separadas (cada una con su tope de unidades), pero no lo necesitás para nada: evitalo.
* **`type` dice qué entrega es.** Primero vienen las opciones `home` y después la de retiro. Puede venir sólo una de las dos: sin sucursales cerca, sólo domicilio; sin envío a domicilio a esa zona, sólo retiro. Dentro de v1 pueden sumarse tipos nuevos: una opción con un `type` que no conocés, no la muestres.
* **Mostrale a tu comprador las sucursales de `pickup_points`** con su nombre, su dirección, su horario y el operador que la atiende: tiene que saber adónde ir. El precio de un retiro es el de la opción, el mismo para todas sus sucursales.
* **El `id` de una sucursal (`pp_1`, `pp_2`, ...) vale, como el `service_code`, sólo junto con su `quote_id`.** No lo guardes para otra cotización. Cómo se crea un pedido con retiro: ver [Retiro en sucursal](/api/pedidos#retiro-en-sucursal).
* **Carritos mixtos.** Si el carrito tiene productos que no son de Gudink, cotizá sólo las líneas de Gudink: lo de Gudink sale en su propio paquete, desde Gudink, y el envío del resto lo calculás vos como siempre. Mostrale a tu comprador la suma, o los dos envíos por separado.
* Límite: 30 cotizaciones por minuto por cuenta.

El `price` de la opción es lo que te cobra Gudink a vos. Cuánto le cobrás de envío a tu comprador lo decidís vos. Ver [Cuánto cobrar de envío](/logistica/cuanto-cobrar-de-envio).

### Errores de la cotización

Se deciden en este orden. Responde el primero que corresponde:

| Orden | Status | `error.code` | Qué pasó | Qué hacer |
| - | - | - | - | - |
| 1 | 400 | `validation_error` | El cuerpo no cumple el formato, o `province` no es una de las 24 | Corregí los campos de `details.fields` |
| 2 | 400 | `province_required` | La API no reconoce el código postal y no mandaste `province` | Pedile la provincia a tu comprador y repetí con `province` |
| 3 | 404 | `not_found` | Algún `product_id` no existe, no es tuyo o está archivado | Revisá los `product_id` y volvé a sincronizar el catálogo |
| 4 | 409 | `variant_not_orderable` | Alguna variante no se puede pedir de ese producto | Ver `details.items` y volvé a leer el producto |
| 5 | 409 | `no_shipping_options` | No hay envío a ese código postal: ni a domicilio ni retiro en sucursal | No ofrezcas el envío a ese destino |

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

`province_required` es un código propio, para que no tengas que distinguirlo de un dato mal tipeado:

```json theme={null}
{
  "error": {
    "code": "province_required",
    "message": "(texto para una persona)",
    "details": {
      "fields": { "province": "El código postal no está en nuestra tabla: mandá también la provincia." }
    }
  }
}
```

Repetí la cotización con la provincia:

```bash theme={null}
curl -sS "$GUDINK_API_BASE/shipping/quotes" \
  -H "Authorization: Bearer $GUDINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "postal_code": "9999",
    "province": "Santa Cruz",
    "items": [{ "product_id": 812, "variant_id": 4410, "quantity": 2 }]
  }'
```

## Provincias

`province` (al cotizar) y `shipping_address.province` (en el pedido) son una de estas 24. La API siempre devuelve el nombre tal cual figura acá:

| | | | |
| - | - | - | - |
| `Buenos Aires` | `Capital Federal` | `Catamarca` | `Chaco` |
| `Chubut` | `Córdoba` | `Corrientes` | `Entre Ríos` |
| `Formosa` | `Jujuy` | `La Pampa` | `La Rioja` |
| `Mendoza` | `Misiones` | `Neuquén` | `Río Negro` |
| `Salta` | `San Juan` | `San Luis` | `Santa Cruz` |
| `Santa Fe` | `Santiago del Estero` | `Tierra del Fuego` | `Tucumán` |

Cómo se normaliza lo que mandás:

| Lo que mandás | Cómo se toma |
| - | - |
| Sin tildes o con otras mayúsculas (`cordoba`, `CÓRDOBA`) | `Córdoba` |
| El código ISO 3166-2 (`X` o `AR-X`) | La provincia de ese código. Si tu plataforma guarda la provincia así (WooCommerce usa `C`, `B`, `X`, ...), mandalo tal cual o con `AR-` delante |
| `CABA` o `Ciudad Autónoma de Buenos Aires` | `Capital Federal` |
| Cualquier otro texto | 400 `validation_error` |

Tres reglas para no equivocarte:

1. **Si la API reconoce el código postal, la provincia sale del código postal** y no hace falta mandarla al cotizar. En el pedido, la que mandes se ignora, no se rechaza: el pedido sale con la del código postal.
2. **Si no lo reconoce**, responde 400 `province_required`: pedísela a tu comprador con un selector de estos 24 valores.
3. **En un pedido a domicilio, mandá en `shipping_address.province` la `province` que devolvió la cotización.** Así nunca difiere. Qué compara la API entre la cotización y el pedido: ver [Qué compara la cotización con el pedido](/api/pedidos#que-compara-la-cotizacion-con-el-pedido).


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