Skip to main content

Cómo se ve un error

Toda respuesta de error tiene la misma forma:
  • code es estable: decidí por code y por el status HTTP.
  • message es un texto en castellano para una persona. Puede cambiar sin aviso: no lo uses en tu lógica.
  • details es siempre un objeto, vacío ({}) si no hay nada que agregar. Su forma depende de code: ver Qué trae details. La especificación la publica código por código, en los esquemas Error_<código>.

Respuestas que no son de la API

Toda respuesta de la API es JSON, y todo error trae el sobre { "error": { ... } }. Entre tu servidor y la API hay infraestructura de red que puede responder por su cuenta, sin ese sobre. La API nunca responde 502 ni 504. Los códigos de la tabla de abajo sólo cuentan cuando está el sobre.

Tabla de errores

Si recibís un code que no está en la tabla (con el sobre), decidí por el status HTTP: un 4xx no se reintenta sin cambiar algo, un 5xx sí. Sin el sobre, ver Respuestas que no son de la API.

Qué trae details

Un ejemplo real de cada código que trae contenido. Todos los códigos que no figuran acá traen details vacío ({}). El message de los ejemplos es ilustrativo: nunca decidas por él.

validation_error

details.fields es un objeto: la clave es la ruta del campo y el valor es su mensaje (el primero de ese campo).
  • Las rutas son las del cuerpo o de la query, con punto y el índice de la lista: shipping_address.number, items.0.quantity.
  • Un error que no es de un campo en particular va bajo la clave _.

province_required

Un solo campo en details.fields: province al cotizar, shipping_address.province al crear el pedido. Se reconoce por el code, no hace falta mirar details.

out_of_stock

Las líneas sin stock suficiente, por su posición en items (desde 0).

variant_not_orderable

La misma forma que out_of_stock: las líneas cuya variante no se puede pedir de ese producto.

billing_data_required

Lo que falta completar en el panel, con los mismos nombres que missing_billing_fields de GET /me: cuit, razon_social, condicion_fiscal, phone.

product_archived

Los product_id archivados.

external_id_conflict

El pedido que ya tiene ese external_id. Consultalo con GET /orders/:id.

order_in_progress

La misma forma: el pedido que se está terminando de crear. Viene con la cabecera Retry-After.

rate_limited

Los segundos a esperar. Es el mismo número que la cabecera Retry-After.

too_many_unpaid_orders

El tope de pedidos por API sin pagar a la vez.

payload_too_large

El tope del cuerpo, en bytes.

invalid_transition

Sólo en el entorno de pruebas: el estado actual del pedido y el paso que pediste simular.

Reintentos

Qué reintentar: un error de red o un timeout (también un 5xx que no es JSON, ver Respuestas que no son de la API), 500 internal_error, 503 service_unavailable, 429 rate_limited, 409 conflict y 409 order_in_progress. Nada más: el resto de los 4xx no se arregla repitiendo la misma llamada. Una sola regla, con un tope que depende de quién está esperando:
  • Adentro de un checkout (hay un comprador esperando): esperá dentro del pedido sin pasar de 30 segundos por espera. Si Retry-After pide más de 30 segundos, no esperes: guardá el pedido como pendiente y seguí con la receta de abajo, en segundo plano.
  • En segundo plano (sincronizar el catálogo, la receta de pendientes, ponerte al día): esperá lo que pida Retry-After, sea lo que sea, antes del próximo intento. Un 429 de un límite por minuto pide como mucho 60 segundos; uno del límite diario puede pedir horas: reprogramá el trabajo para ese momento en vez de dejar un proceso esperando.
  • Al crear un pedido, reintentá siempre con el mismo external_id y el mismo cuerpo. Es lo único que garantiza que no se duplique. Si el timeout se cumple antes de la respuesta, no sabés si el pedido se creó: reintentá igual.
Qué respuestas traen la cabecera Retry-After (en segundos) y cuánto puede pedir: Retry-After es la única cabecera de respuesta que declara el contrato: las respuestas no traen cabeceras de cupo restante (RateLimit-*). Para frenar a tiempo, medí tu ritmo contra la tabla de Límites.

Receta cuando te quedaste sin intentos

Si se acabaron los intentos y no sabés si el pedido se creó:
  1. Guardá el pedido en tu sistema como pendiente, con el cuerpo exacto que mandaste.
  2. Antes de reintentar, consultá GET /orders?external_id=<tu id>.
  3. Si data trae un pedido, ya está creado: usá ése.
  4. Si data viene vacío, mandá de nuevo el mismo cuerpo con el mismo external_id.

Límites

  • Los límites son por cuenta (todas tus claves suman juntas) y por ventana fija: un minuto, o un día para el tope diario de altas.
  • Cuenta toda solicitud con una clave válida, también las que terminan en error (400, 404, 409) y los reintentos que devuelven 200. Un bucle que reintenta un 409 de negocio se come el cupo.
  • Al pasarte recibís 429 rate_limited con la cabecera Retry-After (segundos) y el mismo número en details.retry_after_seconds. No reintentes antes de eso (ver Reintentos).
  • Las respuestas no traen cabeceras de cupo restante (RateLimit-*): medí tu ritmo con esta tabla.
  • Bajar las imágenes de los productos no cuenta para estos límites.
Si tu operación necesita límites más altos, escribinos a hola@gudink.com contando tu caso.

Versiones

  • La URL base (/api/v1) es parte del contrato y no cambia.
  • Dentro de v1 sólo hay cambios que suman: campos nuevos en las respuestas, códigos de error nuevos, eventos nuevos, valores nuevos de cualquier campo de lista cerrada (status, hold_reason, cancel_reason, product_type, payment.status, tracking.carrier, el type de un aviso).
  • Tu código tiene que ignorar los campos que no conoce y no romperse ante un valor nuevo. Tratá un status desconocido como “en curso”, un cancel_reason desconocido como null, y respondé 2xx a un evento desconocido.
  • Los enum de la especificación son los valores de hoy. Si validás respuestas contra la especificación, que un valor nuevo no te haga descartar la respuesta: tratalo como dice cada página (“en curso” para un estado, 2xx y descartar para un aviso). El type de un aviso ya figura abierto en la especificación (texto, con los de hoy en examples).
  • Un cambio que rompe (sacar o renombrar un campo, cambiar un tipo o un significado) sale como v2, con 90 días de aviso. Durante ese plazo v1 sigue funcionando.
  • La especificación vigente, en formato OpenAPI, la sirve la propia API en GET /api/v1/openapi.json, sin clave. La guía canónica en texto plano, en GET /api/v1/llms.txt, también sin clave. Ver cuál vale si difieren.

Checklist antes de pasar a producción

  1. La clave de pruebas (gk_stg_) está en GUDINK_API_KEY en tu servidor y GUDINK_API_BASE apunta al entorno de pruebas. Tu cliente HTTP manda un User-Agent propio. GET /me responde y billing_complete es true.
  2. El catálogo se sincroniza paginando hasta next_cursor: null. Guardás cada variante por el par (product_id, variant_id), con su precio, y el updated_at del producto. Las imágenes están alojadas en tu sitio.
  3. El checkout valida los formatos de Qué va en cada campo antes de cobrar.
  4. El checkout cotiza el envío con el carrito real y muestra las opciones con su price. Si responde province_required, pide la provincia. Si ofrecés retiro en sucursal, tu comprador elige una sucursal de pickup_points antes de pagar.
  5. El pedido se crea con un external_id único (prefijo de tu instalación más tu número de pedido) y con la province de la cotización. Ante un error de red o un 5xx se reintenta con el mismo external_id y el mismo cuerpo, según Reintentos. Después de cada 200 o 201 tu sistema mira status: si es cancelled, ese pedido no va.
  6. Tu sistema maneja out_of_stock después de haber cobrado, sin reintentar en bucle.
  7. Hay alguien, o algo, que paga los pedidos en tu panel antes de expires_at (72 horas), con una alarma propia antes del vencimiento (ver Pagar es una tarea de todos los días). En pruebas, con la simulación. Un order.cancelled con cancel_reason: "unpaid_expired" quiere decir que se te pasó.
  8. El destino de avisos está dado de alta, verifica la firma y deduplica por webhook-id. “Mandar prueba” llega bien.
  9. Con order.shipped u order.tracking_updated tu comprador recibe el seguimiento. Gudink no le escribe.
  10. Todo lo anterior funciona en el entorno de pruebas, con el recorrido de punta a punta. Recién ahí, clave de producción (gk_live_) y GUDINK_API_BASE="https://app.gudink.com/api/v1".