Cómo se ve un error
Toda respuesta de error tiene la misma forma:codees estable: decidí porcodey por el status HTTP.messagees un texto en castellano para una persona. Puede cambiar sin aviso: no lo uses en tu lógica.detailses siempre un objeto, vacío ({}) si no hay nada que agregar. Su forma depende decode: ver Qué trae details. La especificación la publica código por código, en los esquemasError_<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á traendetails 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 endetails.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 enitems (desde 0).
variant_not_orderable
La misma forma queout_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 quemissing_billing_fields de GET /me: cuit, razon_social, condicion_fiscal, phone.
product_archived
Losproduct_id archivados.
external_id_conflict
El pedido que ya tiene eseexternal_id. Consultalo con GET /orders/:id.
order_in_progress
La misma forma: el pedido que se está terminando de crear. Viene con la cabeceraRetry-After.
rate_limited
Los segundos a esperar. Es el mismo número que la cabeceraRetry-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), 500internal_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-Afterpide 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_idy 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.
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ó:- Guardá el pedido en tu sistema como pendiente, con el cuerpo exacto que mandaste.
- Antes de reintentar, consultá
GET /orders?external_id=<tu id>. - Si
datatrae un pedido, ya está creado: usá ése. - Si
dataviene vacío, mandá de nuevo el mismo cuerpo con el mismoexternal_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_limitedcon la cabeceraRetry-After(segundos) y el mismo número endetails.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.
Versiones
- La URL base (
/api/v1) es parte del contrato y no cambia. - Dentro de
v1só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, eltypede un aviso). - Tu código tiene que ignorar los campos que no conoce y no romperse ante un valor nuevo. Tratá un
statusdesconocido como “en curso”, uncancel_reasondesconocido comonull, y respondé 2xx a un evento desconocido. - Los
enumde 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). Eltypede un aviso ya figura abierto en la especificación (texto, con los de hoy enexamples). - 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 plazov1sigue 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, enGET /api/v1/llms.txt, también sin clave. Ver cuál vale si difieren.
Checklist antes de pasar a producción
- La clave de pruebas (
gk_stg_) está enGUDINK_API_KEYen tu servidor yGUDINK_API_BASEapunta al entorno de pruebas. Tu cliente HTTP manda unUser-Agentpropio.GET /meresponde ybilling_completeestrue. - 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 elupdated_atdel producto. Las imágenes están alojadas en tu sitio. - El checkout valida los formatos de Qué va en cada campo antes de cobrar.
- El checkout cotiza el envío con el carrito real y muestra las opciones con su
price. Si respondeprovince_required, pide la provincia. Si ofrecés retiro en sucursal, tu comprador elige una sucursal depickup_pointsantes de pagar. - El pedido se crea con un
external_idúnico (prefijo de tu instalación más tu número de pedido) y con laprovincede la cotización. Ante un error de red o un 5xx se reintenta con el mismoexternal_idy el mismo cuerpo, según Reintentos. Después de cada200o201tu sistema mirastatus: si escancelled, ese pedido no va. - Tu sistema maneja
out_of_stockdespués de haber cobrado, sin reintentar en bucle. - 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. Unorder.cancelledconcancel_reason: "unpaid_expired"quiere decir que se te pasó. - El destino de avisos está dado de alta, verifica la firma y deduplica por
webhook-id. “Mandar prueba” llega bien. - Con
order.shippeduorder.tracking_updatedtu comprador recibe el seguimiento. Gudink no le escribe. - 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_) yGUDINK_API_BASE="https://app.gudink.com/api/v1".

