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
| Endpoint | Descripción |
|---|---|
GET /v1/health | Salud del servicio (sin clave): status ok/degraded y checks de firestore, stripe e IA. Alimenta la página de estado. |
POST /v1/quotes | Cotiza 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/shipments | Crea 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}/cancel | Cancela 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/wallet | Saldo de tu wallet: balance, moneda y totales (cargado, reembolsado, recargado). |
POST /v1/wallet/topup | Inicia una recarga {amount, testMode?} y devuelve el PaymentIntent de Stripe. |
POST /v1/wallet/topup/confirm | Confirma {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": "…" } }.
| HTTP | code | Significado |
|---|---|---|
| 400 | VALIDATION | Faltan datos o son inválidos (recuerda: medidas y peso obligatorios en cada paquete/parada). |
| 401 | INVALID_KEY | Clave ausente o que no existe. |
| 403 | KEY_REVOKED | La clave fue revocada o desactivada. |
| 402 | INSUFFICIENT_WALLET | Saldo de wallet insuficiente: recarga antes de crear el envío. |
| 409 | IDEMPOTENCY_CONFLICT | La Idempotency-Key ya se usó con un cuerpo distinto. |
| 422 | NO_CAPACITY | No hay conductores capaces en línea para esa clase de paquete ahora mismo. No se cobró nada. |
| 422 | OUT_OF_AREA | Recogida o entrega fuera del área de servicio de la ciudad. |
| 422 | NO_ROUTE | No existe ruta manejable entre recogida y entrega. |
| 422 | PACKAGE_TOO_LARGE | El paquete supera el peso o la dimensión máxima de la ciudad. |
| 409 | RETURN_WINDOW_CLOSED | La ventana de devolución de ese envío ya cerró. |
| 429 | RATE_LIMITED | Superaste 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
Franco Food