API de Kroni
Conecta tu sistema con nuestras paqueterías
Una API REST sobre HTTPS, con JSON y autenticación por API Key, para que tu ERP cotice envíos con varias paqueterías en tiempo real y —en la siguiente etapa— genere guías, descargue etiquetas y consulte el estado del envío.
URL base — producción y pruebas
https://go-api.kroni.io/v1El entorno lo determina el prefijo de tu llave: kg_test_ para pruebas, kg_live_ para producción.
Referencia
Cada endpoint, sus parámetros y el catálogo completo de errores.
OpenAPI 3.1
Descarga la especificación y genera tu cliente automáticamente.
Obtener una API Key
Ajustes → Desarrolladores en tu cuenta de Kroni.
Inicio rápido
Genera una API Key desde el portal de Kroni, en Ajustes → Desarrolladores. Las llaves de pruebas están disponibles desde el primer día; las de producción requieren que tu cuenta esté verificada.
Con la llave en la mano, tu primera cotización es una sola llamada:
curl -X POST https://go-api.kroni.io/v1/rates \
-H "Authorization: Bearer kg_test_TU_LLAVE" \
-H "Content-Type: application/json" \
-d '{
"origin": { "postal_code": "44520" },
"destination": { "postal_code": "03940" },
"package": { "weight_kg": 2.5, "length_cm": 30, "width_cm": 20, "height_cm": 15 }
}'Autenticación
Todas las llamadas llevan el encabezado Authorization con tu API Key como Bearer token.
La llave se muestra una sola vez, al crearla. Si la pierdes, revócala y genera otra: no podemos recuperarla porque no la guardamos, solo guardamos su huella.
Authorization: Bearer kg_live_TU_API_KEYEntornos y credenciales
Hay dos entornos con credenciales independientes y una sola URL base: https://go-api.kroni.io/v1. El entorno lo determina el prefijo de tu llave, no la dirección a la que llamas — igual que en Stripe. Así tu ERP configura una sola URL y cambia de entorno cambiando la credencial.
Los datos están aislados: una llave de pruebas nunca ve cotizaciones ni envíos de producción, y viceversa.
| Entorno | Prefijo de la llave | Qué pasa |
|---|---|---|
| Pruebas | kg_test_ | Tarifas de una paquetería simulada y determinista. No se cobra saldo ni se contacta a ninguna paquetería. |
| Producción | kg_live_ | Tarifas y envíos reales. Cada guía descuenta del saldo prepago. |
Cotizar
POST /rates devuelve una tarifa por cada servicio disponible para la ruta. Cada tarifa trae el costo total real que se cobrará, con IVA incluido y el desglose de cargos.
Los campos de ciudad y estado son opcionales: el código postal es lo que manda.
{
"quote_id": "qt_9f3c1a7e5b2d4a8000000000000000aa",
"expires_at": "2026-09-10T18:30:00Z",
"currency": "MXN",
"rates": [
{
"rate_id": "rt_2c8a4f10ab12",
"carrier": { "code": "estafeta", "name": "Estafeta" },
"service": { "code": "estafeta_express", "name": "Día siguiente" },
"total_amount": 214.60,
"currency": "MXN",
"taxes_included": true,
"amount_breakdown": {
"subtotal": 185.00,
"surcharges": [
{ "code": "extended_zone", "label": "Zona extendida", "amount": 35.00 }
],
"taxes": [{ "code": "IVA", "rate": 0.16, "amount": 29.60 }],
"total": 214.60
},
"delivery": {
"estimated_days": 2,
"estimated_date": "2026-09-12",
"eta_label": "2 días hábiles"
},
"coverage": { "available": true, "extended_zone": true },
"package_count": 1,
"expires_at": "2026-09-10T18:30:00Z"
}
]
}Valor declarado y protección
declared_value_mxn por sí solo no cobra nada. Viaja al envío y sirve de base para la protección, que es opcional.
Para contratarla, manda protect_declared_value en true. Se cobra el mayor entre un mínimo fijo y un porcentaje del valor declarado, más IVA, y aparece en el desglose como el recargo declared_value_protection.
Folio y vigencia
Cada cotización devuelve un folio único (quote_id) y vive 60 minutos. Guárdalo: es lo que amarra la tarifa que le mostraste a tu cliente con el envío que se genere después.
Puedes recuperar un folio en cualquier momento con GET /rates/{quote_id}, incluso vencido: en ese caso responde 200 con expired en true, para que puedas auditar históricos. El rechazo por vigencia ocurre al generar la guía, no al consultar.
Dentro de un folio, cada tarifa tiene su rate_id. Esa pareja (quote_id, rate_id) es lo que identifica «esta tarifa exacta» al momento de generar el envío.
Límites de peticiones
Los límites son por llave y por minuto. Toda respuesta trae los encabezados X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (este último en epoch de segundos), así que puedes frenar antes de chocar.
Al rebasarlos, la API responde 429 rate_limited con el encabezado Retry-After en segundos. Si tu volumen necesita más, escríbenos: los límites se ajustan por cuenta.
| Endpoint | Peticiones por minuto |
|---|---|
| POST /rates | 60 |
| POST /shipments | 30 |
| GET /tracking/{numero} | 120 |
| Resto de endpoints | 300 |
Errores
Todos los errores comparten la misma forma. Ramifica por code, que es estable; el texto de message puede cambiar de redacción sin previo aviso.
El request_id identifica la llamada en nuestros registros: inclúyelo si necesitas soporte.
{
"error": {
"code": "validation_error",
"message": "La petición tiene campos inválidos.",
"details": [
{ "field": "destination.postal_code", "issue": "Debe ser un código postal de 5 dígitos" }
],
"request_id": "req_01JBX..."
}
}Peso volumétrico
Las paqueterías cobran el mayor entre el peso real y el peso volumétrico, que se calcula a partir de las dimensiones del paquete.
Cada paquetería aplica su propio divisor y lo hace de su lado, así que el peso facturable ya viene reflejado en el precio que te devolvemos. En GET /reference/carriers publicamos el divisor de las paqueterías cuyo cálculo conocemos con certeza; en el resto lo marcamos en null, porque preferimos no darte un número que después discrepe de la factura.
Restricciones de peso y dimensiones
Kroni acepta hasta 70 kg por bulto, 300 cm por lado y 20 bultos por cotización. Una petición fuera de esos rangos se rechaza en la validación, antes de consultar a ninguna paquetería.
Cada paquetería tiene además sus propios topes, que pueden ser más estrictos. Los que tenemos registrados vienen en GET /reference/carriers, por paquetería.
Cargos adicionales
Cuando aplican, los cargos vienen desglosados dentro de amount_breakdown.surcharges con su concepto y su monto antes de IVA. El total que te devolvemos ya los incluye: no hay que sumarlos aparte.
Estos son los conceptos que puedes encontrar. El catálogo vigente, con su descripción, está siempre en GET /reference/carriers.
| Concepto | Cuándo aplica |
|---|---|
extended_zone | El código postal de destino está fuera del área de cobertura ordinaria de la paquetería. |
overweight | El peso facturable rebasa el incluido en la tarifa base del servicio. |
fuel | Recargo variable que la paquetería aplica sobre el flete. |
pickup | Se solicita recolección en domicilio en lugar de entrega en sucursal. |
declared_value_protection | Contrataste la protección sobre el valor declarado. |
Generar guías
POST /shipments genera la guía a partir del folio (quote_id) y la tarifa (rate_id) que elegiste, más las direcciones completas de origen y destino. Peso y medidas no se repiten: ya van en el folio. Los códigos postales deben coincidir con los de la cotización.
El encabezado Idempotency-Key es obligatorio: si reintentas con la misma llave y el mismo cuerpo recibes la misma guía (200) sin que se cobre dos veces; si cambias el cuerpo bajo la misma llave, 409 idempotency_conflict.
Manda tu número de pedido en reference: te lo devolvemos en toda consulta y puedes filtrar el listado por él. En producción cada guía descuenta del saldo prepago; sin saldo suficiente, 402 insufficient_balance.
{
"shipment_id": "shp_7d2e9b4100000000000000000000aaaa",
"status": "created",
"status_label": "Guía generada",
"quote_id": "qt_9f3c1a7e5b2d4a8000000000000000aa",
"rate_id": "rt_2c8a4f10ab12",
"reference": "PED-2026-00123",
"carrier": { "code": "estafeta", "name": "Estafeta" },
"tracking_number": "7412589630",
"label": { "available": true, "formats": ["pdf", "pdf_letter"],
"url": "https://go-api.kroni.io/v1/shipments/shp_7d2e9b41.../label" },
"amount_charged": 214.60,
"currency": "MXN"
}Etiquetas
GET /shipments/{id}/label devuelve el PDF de la guía. Con format=pdf (por defecto) sale en térmica de 4 × 6 pulgadas; con format=pdf_letter, en tamaño carta. Con redirect=true respondemos 302 a una URL firmada temporal, útil si prefieres no proxear el binario.
ZPL no está disponible por ahora; pedirlo responde 415 label_format_unavailable. Una guía cancelada no tiene etiqueta descargable.
Rastreo
GET /tracking/{numero} consulta a la paquetería en tiempo real y devuelve un estado normalizado —created, picked_up, in_transit, out_for_delivery, delivered, incident, returned, cancelled, failed— con su etiqueta en español y el historial de eventos. Solo rastrea guías de tu organización.
En pruebas el estado avanza con el reloj desde que se creó la guía: recolectada a los 5 minutos, en tránsito a los 10, en reparto a los 15, entregada a los 20. Así puedes probar tu lógica sin esperar días.
Cancelación
POST /shipments/{id}/cancel registra una solicitud de cancelación con el motivo que envíes en texto libre. Responde 202 con status pending_review: no es una cancelación inmediata, Kroni la revisa y te avisa la resolución por webhook. Refleja ese estado intermedio en tu sistema.
Solo procede mientras la paquetería no haya recolectado el paquete; después, 409 shipment_not_cancellable. En el entorno de pruebas la solicitud se aprueba al instante.
Webhooks
Registra una URL en tu aplicación (Ajustes → Desarrolladores) y te avisamos cuando una guía cambia de estado, en vez de que tengas que sondear. Cada entrega es un POST con JSON y tres encabezados: X-Kroni-Event con el tipo de evento, X-Kroni-Event-Id con un identificador estable para deduplicar reenvíos, y X-Kroni-Signature con la firma.
La firma tiene la forma t=<epoch>,v1=<HMAC-SHA256 de "t.cuerpo" con el secreto de tu aplicación>. Verifícala antes de procesar y rechaza si la diferencia entre tu reloj y t supera 300 segundos. Responde cualquier 2xx en menos de 10 segundos; si no, reintentamos con espera creciente hasta cinco veces.
| Evento | Cuándo |
|---|---|
| shipment.created | Se generó la guía. |
| shipment.status_changed | La paquetería reportó un cambio de estado (solo producción). |
| shipment.delivered | La paquetería confirmó la entrega. |
| shipment.cancellation_requested | Registramos tu solicitud de cancelación. |
| shipment.cancelled | La cancelación se aprobó. |
Versionado
La versión va en la ruta. Dentro de v1 solo hacemos cambios compatibles: agregar un campo opcional en una respuesta, un valor nuevo en un catálogo, un endpoint nuevo.
Un cambio incompatible sale como /v2, y v1 se mantiene funcionando con aviso previo. Construye tu integración tolerando campos que no conozcas.
Cifras de ejemplo. El precio real depende de la ruta, el paquete y las condiciones vigentes de tu cuenta.
¿Dudas con la integración?
Escríbenos y te ayudamos a conectar tu sistema. Si ya tienes unrequest_idde un error, inclúyelo.