API de Franco Envíos

La API de Franco Envíos es OPCIONAL. La forma principal y completa de enviar paquetes es el panel de envíos — no necesitas saber programar ni conectar nada. La API es un complemento para negocios que quieren conectar su propio sitio web y automatizar sus envíos.

Inicio rápido

1. Pide tu clave: un administrador de Franco Food emite la clave de tu negocio (en el panel de administración) y te la entrega una sola vez. Guárdala como un secreto: nunca aparece de nuevo.

2. Tu clave empieza con ff_live_ (producción) o ff_test_ (pruebas). Todas las llamadas llevan el encabezado Authorization: Bearer <tu clave>.

3. Cotiza primero, crea después: la cotización confirma precio y disponibilidad de conductores antes de cobrar tu wallet.

Tu primera cotización

curl -X POST https://francoapp.com/api/v1/quotes \
  -H "Authorization: Bearer ff_live_TU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "ciudad": "Antigua",
    "pickup":   { "lat": 17.1274, "lng": -61.8468, "address": "Market St 12" },
    "delivery": { "lat": 17.1180, "lng": -61.8300, "address": "All Saints Rd 4",
                  "contactName": "María Pérez", "contactPhone": "+12685550123" },
    "lengthCm": 30, "widthCm": 20, "heightCm": 15, "weightKg": 2.5
  }'

Respuesta: total, fee (pago del conductor), subTotal (comisión de la plataforma), moneda, sizeClass (S/M/L), distanceKm, chargeableKg (el mayor entre peso real y volumétrico), capacity.available y el ajuste de IA aplicado, si lo hubo.

Crear el envío (se cobra de tu wallet de negocio)

curl -X POST https://francoapp.com/api/v1/shipments \
  -H "Authorization: Bearer ff_live_TU_CLAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-100234" \
  -d '{
    "ciudad": "Antigua",
    "pickup":   { "lat": 17.1274, "lng": -61.8468, "address": "Market St 12",
                  "contactName": "Mi Tienda", "contactPhone": "+12685550100" },
    "delivery": { "lat": 17.1180, "lng": -61.8300, "address": "All Saints Rd 4",
                  "contactName": "María Pérez", "contactPhone": "+12685550123" },
    "pkgType": "paquete", "pkgDescription": "Caja de zapatos",
    "lengthCm": 30, "widthCm": 20, "heightCm": 15, "weightKg": 2.5
  }'
# 201 → { "id": "…", "order_number": 12345, "status": "solicitado",
#         "total": 21.50, "moneda": "XCD", "label": "P1 · X7K2 · María Pérez",
#         "trackingUrl": "https://francoapp.com/app/#/package/…" }

Notas: las medidas (largo × ancho × alto en cm) y el peso son obligatorios en cada paquete y en cada parada de una ruta. Una ruta lleva stops[] en vez de delivery, con hasta 60 paradas; el servidor optimiza el orden y devuelve el precio por parada (stopPricing). Escribe la etiqueta (label) en el paquete físico: «Parada N — Nombre del cliente».

Autenticación

Cada clave pertenece a un solo negocio y tiene alcances (scopes): shipments:read y shipments:write. El servidor guarda solo un resumen SHA-256 de la clave — la clave en claro nunca se guarda ni se registra. Si una clave se filtra, un administrador la revoca (active = false) y la revocación es inmediata; para rotar, emite una clave nueva y revoca la anterior.

Authorization: Bearer 
X-Test-Mode: (no hace falta: el entorno lo define el prefijo de la clave)

Los envíos por API se pagan con la wallet precargada de tu negocio (saldoActual). Consulta el saldo con GET /v1/wallet y recarga con POST /v1/wallet/topup + POST /v1/wallet/topup/confirm. Si el saldo no alcanza, la creación falla con 402 INSUFFICIENT_WALLET y no se crea ningún envío.

Endpoints

Base: https://francoapp.com/api/v1  ·  Directa: https://us-central1-franco-food-8ba71.cloudfunctions.net/apiV1/v1

EndpointDescripción
GET /v1/healthSalud del servicio (sin clave): status ok/degraded y checks de firestore, stripe e IA. Alimenta la página de estado.
POST /v1/quotesCotiza un envío individual o una ruta (stops[]): precio, clase, km, peso facturable y disponibilidad de conductores. Si mandas clientDistanceKm o legKms (de tu propio Directions), se validan; si no, el km se estima y la respuesta lleva kmEstimated: true.
POST /v1/shipmentsCrea el envío (individual o ruta de hasta 60 paradas). Exige Idempotency-Key. Verifica disponibilidad ANTES de cobrar: sin conductores capaces responde 422 NO_CAPACITY y no cobra nada.
GET /v1/shipments/{id}Detalle y estado de un envío de tu negocio.
GET /v1/shipments?status=&from=&to=&limit=&cursor=Lista paginada de tus envíos.
POST /v1/shipments/{id}/cancelCancela solo antes de la recogida. Reembolso = total − tarifa de cancelación del cliente de la ciudad; si ya había conductor asignado, se le compensa desde la comisión.
GET /v1/walletSaldo de tu wallet: balance, moneda y totales (cargado, reembolsado, recargado).
POST /v1/wallet/topupInicia una recarga {amount, testMode?} y devuelve el PaymentIntent de Stripe.
POST /v1/wallet/topup/confirmConfirma {paymentIntentId}: verifica en Stripe y acredita el saldo una sola vez.

El panel de envíos usa la misma API con tu sesión de Franco Food (rutas /v1/panel/…): todo lo que la API hace, el panel ya lo hace sin programar.

Estados de un envío: solicitado → postulado (un conductor aceptó) → recogido → entregado; cancelado (antes de recogido). Si la entrega falla tras la recogida: pendiente_devolucion → devuelto | no_devuelto (plazo: día siguiente a las 11:00 a. m., hora local de la ciudad).

Idempotencia

POST /v1/shipments exige el encabezado Idempotency-Key. La pareja (tu negocio, tu clave de idempotencia) es única: si reintentas con la misma clave y el mismo cuerpo, recibes el MISMO envío sin un segundo cargo. La misma clave con un cuerpo distinto responde 409 IDEMPOTENCY_CONFLICT. Usa una clave por pedido de tu sistema (por ejemplo, tu número de orden).

Webhooks

Configura la URL de webhook de tu negocio y te avisamos con un POST en cada cambio. Eventos: shipment.accepted, shipment.picked_up, shipment.delivered, shipment.cancelled; por parada: shipment.stop_delivered, shipment.stop_failed, shipment.stop_returned, shipment.stop_not_returned; devoluciones: shipment.return_pending, shipment.returned, shipment.not_returned.

Cada entrega va firmada: el encabezado FF-Signature trae sha256=<HMAC-SHA256 de tu secreto de webhook sobre el cuerpo exacto>. Verifica la firma antes de confiar en el evento. Si tu servidor no responde a tiempo (8 s), el evento entra en una cola y se reintenta con espera creciente hasta 5 intentos.

FF-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Content-Type: application/json

{ "event": "shipment.delivered", "shipmentId": "…", "order_number": 12345,
  "status": "entregado", "ts": "2026-10-22T21:05:00.000Z" }
// Node.js — verificar la firma
const crypto = require('crypto');
function validSignature(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Códigos de error

Toda respuesta de error tiene la forma { "error": { "code": "…", "message": "…" } }.

HTTPcodeSignificado
400VALIDATIONFaltan datos o son inválidos (recuerda: medidas y peso obligatorios en cada paquete/parada).
401INVALID_KEYClave ausente o que no existe.
403KEY_REVOKEDLa clave fue revocada o desactivada.
402INSUFFICIENT_WALLETSaldo de wallet insuficiente: recarga antes de crear el envío.
409IDEMPOTENCY_CONFLICTLa Idempotency-Key ya se usó con un cuerpo distinto.
422NO_CAPACITYNo hay conductores capaces en línea para esa clase de paquete ahora mismo. No se cobró nada.
422OUT_OF_AREARecogida o entrega fuera del área de servicio de la ciudad.
422NO_ROUTENo existe ruta manejable entre recogida y entrega.
422PACKAGE_TOO_LARGEEl paquete supera el peso o la dimensión máxima de la ciudad.
409RETURN_WINDOW_CLOSEDLa ventana de devolución de ese envío ya cerró.
429RATE_LIMITEDSuperaste el límite por minuto de tu clave; espera y reintenta.

Límites y precios

Límite por clave: 120 solicitudes por minuto (configurable por negocio). Rutas: máximo 60 paradas. Precio: base de la ciudad + distancia + peso facturable (el mayor entre real y volumétrico, divisor 5000), por clase S/M/L; la IA de Franco Food puede ajustar el total dentro de bandas controladas por el administrador, siempre protegiendo el piso del conductor. Las tarifas vigentes por ciudad las administra Franco Food; la cotización siempre devuelve el precio final antes de cualquier cargo.

Modo de prueba

Con una clave ff_test_ trabajas en modo de prueba: las recargas de wallet con testMode confirman con la tarjeta de prueba de Stripe (pm_card_visa) y los envíos quedan marcados con testMode. Úsalo para integrar y probar todo el flujo — cotización, creación idempotente, cancelación, webhooks — antes de pasar a ff_live_.

¿Necesitas una clave o ayuda con la integración? Escribe a team@francoapp.com · Estado de la API