Empezá en 5 minutos
Pedí una API key, creá envíos y rutas, gestioná flota (vehículos, conductores, asignaciones) y suscribite a webhooks. La API Reference documenta las 41 rutas HTTP (68 operaciones) del contrato actual.
Conseguí tu API key
Pedile a tu administrador que entre al dashboard, sección Integraciones → API Keys, y genere una key con al menos:
shipments:readshipments:writeshipments:deleteroutes:readroutes:writeroutes:deleteroutes:optimizevehicles:readvehicles:writevehicles:deletedrivers:readdrivers:writedrivers:deleteassignments:readassignments:writeassignments:deleteevents:readwebhooks:manage
La key se ve UNA sola vez al crearla. Guardala en tu gestor de secretos. Formato: mss_live_* para producción o mss_test_* para sandbox.
export MASSIMPLE_BASE=https://api.massimple.la/v1, export MASSIMPLE_KEY=mss_test_xxx.
Validá que la key funciona
El endpoint /v1/whoami te devuelve la aplicación y la compañía a la que apunta tu key. Sanity check antes de cualquier otra cosa.
curl $MASSIMPLE_BASE/whoami \
-H "Authorization: Bearer $MASSIMPLE_KEY"
Esperás 200 con:
{
"application": {
"id": "...",
"name": "Mi integración",
"env": "test",
"scopes": ["shipments:write", "events:read", "webhooks:manage"]
},
"company_id": "..."
}
Creá tu primer envío
POST a /v1/shipments. No envíes companyId ni status: el envío siempre se crea como pending. Si tu empresa tiene un solo centro de distribución, podés omitir origin_distribution_center_id.
Campos principales (detalle completo en API Reference → Crear envío):
| Campo | Requerido | Notas |
|---|---|---|
destination | Sí | Objeto destino |
destination.address | Sí | Dirección; si no hay lat/lng se geocodifica |
destination.latitude / longitude | No* | *Recomendados para ruteo |
content | Sí | Descripción del envío |
origin_distribution_center_id | No | Obligatorio si tenés más de un CD |
external_id, amount_to_collect, scheduled_date, metadata | No | Opcionales |
curl -X POST $MASSIMPLE_BASE/shipments \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_id": "ORD-1001",
"destination": {
"name": "Juan Pérez",
"address": "Av. Corrientes 1234, CABA",
"latitude": -34.6037,
"longitude": -58.3816,
"contact_phone": "+5491155557777"
},
"content": "Caja con dos productos",
"amount_to_collect": 0,
"scheduled_date": "2026-05-20"
}'
Respuesta 201 con formato público: id (shp_...), tracking_number, external_id, etc.
Suscribite a eventos con un webhook
Registrá una URL HTTPS pública en el dashboard (Integraciones → Webhooks) o por API. Cada vez que algo cambia (estado de envío, de ruta, etc.) te mandamos un POST firmado.
curl -X POST $MASSIMPLE_BASE/webhooks \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.tuempresa.com/webhooks/massimple",
"events": ["*"]
}'
Te devolvemos un secret que se muestra UNA vez. Guardalo para verificar la firma HMAC de cada delivery.
* o el preset seguimiento chofer (route.stop.status_changed, shipment.status_changed, route.optimized, route.status_changed). La lista de eventos no se edita después de crear el webhook.
X-MasSimple-Signature: t=<unix-time>,v1=<hmac-hex>. La firma se computa sobre <timestamp>.<body-raw> usando tu secret (whsec_…). Verificá la firma siempre, sobre el body RAW (no parseado).
Flujo en tu servidor:
- Recibís
POST application/jsonen la URL registrada. - Leés el body como string exacto (bytes tal cual llegaron).
- Parseás
t=yv1=del headerX-MasSimple-Signature. - Calculás
HMAC-SHA256(secret, "<t>.<body-raw>")en hex. - Comparás con
v1(crypto.timingSafeEqual). Rechazá si falta header, firma inválida o timestamp > 5 min. - Si es válido → parseás JSON → procesás → respondés
200.
Verificá la firma de cada webhook
Función de verificación + handler Express de referencia. Variable de entorno WEBHOOK_SECRET=whsec_….
import crypto from 'crypto';
export function verifySignatureHeader({ header, body, secret, maxAgeSeconds = 300 }) {
if (typeof header !== 'string') return false;
const parts = Object.fromEntries(
header.split(',').map((p) => {
const idx = p.indexOf('=');
return idx === -1 ? [p, ''] : [p.slice(0, idx).trim(), p.slice(idx + 1)];
}),
);
const ts = Number(parts.t);
if (!Number.isFinite(ts)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > maxAgeSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
const provided = parts.v1 || '';
if (provided.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}
Handler Express completo (usá express.raw solo en esta ruta, no express.json() global):
import crypto from 'crypto';
import express from 'express';
const SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(header, rawBody) {
return verifySignatureHeader({ header, body: rawBody, secret: SECRET });
}
const app = express();
app.post(
'/webhooks/massimple',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
const sig = req.headers['x-massimple-signature'];
if (!verifySignature(sig, rawBody)) {
return res.status(401).json({ error: 'invalid signature' });
}
const event = JSON.parse(rawBody);
console.log('Evento verificado:', event.type, event.data);
return res.status(200).json({ received: true });
},
);
Debug manual: echo -n "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex — el hex debe coincidir con v1= del header.
Si el verificador devuelve true, ya podés parsear el body y procesar el evento. Devolvé 200 al final — cualquier 5xx dispara nuestro retry exponencial (1m, 5m, 30m, 2h, 12h, 24h).
Auditoría: consultá el stream con GET /events
Complemento a webhooks: pull en lugar de push. Scope events:read. Lista lo que emitió +simple (30 días) aunque falle el POST al webhook o no tengas URL pública. Mismo formato CloudEvents que un delivery (id, type, time, data).
| Query | Uso |
|---|---|
limit | Máx por página (default 50, máx 200) |
types | Filtro coma-separado, ej. shipment.status_changed,route.stop.status_changed |
after | Cursor next_cursor de la página anterior |
curl "$MASSIMPLE_BASE/events?limit=50&types=route.stop.status_changed,shipment.status_changed" \
-H "Authorization: Bearer $MASSIMPLE_KEY"
{
"events": [
{
"id": "evt_6a16103d86b6e449bc2ffc76",
"type": "shipment.status_changed",
"source": "masSimple",
"specversion": "1.0",
"time": "2026-05-26T21:27:25.432Z",
"data": { "external_id": "ORD-1001", "current_status": "delivered" }
}
],
"next_cursor": "6a16103d86b6e449bc2ffc77"
}
Si next_cursor no es null, pedí la siguiente página con ?after=next_cursor. En el demo Bazarshop (tab Eventos) podés probar el polling con filtros sin curl. Detalle en API Reference → Polling de eventos y Dashboard → Integraciones → Endpoints.
Operá por external_id
Si guardás tu propio ID de pedido (ORD-1001), no necesitás el shp_... de Massimple:
# Consultar
curl $MASSIMPLE_BASE/shipments/external/ORD-1001 \
-H "Authorization: Bearer $MASSIMPLE_KEY"
# Actualizar (solo content y monto a cobrar)
curl -X PATCH $MASSIMPLE_BASE/shipments/external/ORD-1001 \\
-H "Authorization: Bearer $MASSIMPLE_KEY" \\
-H "Content-Type: application/json" \\
-d '{"content": "Caja actualizada", "amount_to_collect": 1500}'
Listado, actualización y borrado por external_id usan las mismas rutas con prefijo /shipments/external/.... El PATCH solo acepta content y amount_to_collect (no destino, fechas ni metadata). Para cancelar: POST /v1/shipments/{id}/cancel (o .../external/{external_id}/cancel) con body opcional {"error_comment":"..."} — no hace falta enviar status. Emite shipment.status_changed y shipment.cancelled. El estado failed no está en la API pública: solo lo marca el conductor desde la DriverApp. Para recrear un envío cancelled o failed: POST /v1/shipments/{id}/recreate (o .../external/{external_id}/recreate) con {"scheduled_date":"YYYY-MM-DD"} — crea un envío nuevo en pending con la misma data; el original no cambia. Emite shipment.created. Para alta masiva: POST /v1/shipments/bulk y POST /v1/shipments/bulk-delete (ambos aceptan Idempotency-Key).
# Cancelar envío (por id o external_id)
curl -X POST $MASSIMPLE_BASE/shipments/external/ORD-1001/cancel \\
-H "Authorization: Bearer $MASSIMPLE_KEY" \\
-H "Content-Type: application/json" \\
-d '{"error_comment": "Cliente solicitó anulación"}'
# Recrear envío cancelado/fallido (nuevo pending, misma data)
curl -X POST $MASSIMPLE_BASE/shipments/external/ORD-1001/recreate \\
-H "Authorization: Bearer $MASSIMPLE_KEY" \\
-H "Content-Type: application/json" \\
-d '{"scheduled_date": "2026-06-10"}'
Envíos sin ruta asignada: GET /v1/shipments/available (pending, sin assigned_route_id).
Consultá qué está libre para asignar
No hay endpoints separados /unassigned. Usá filtros en los listados:
| Recurso | Request | Qué devuelve |
|---|---|---|
| Envíos para rutear | GET /shipments/available | Pendientes sin ruta |
| Tras borrar una ruta | GET /shipments/available | Los envíos de la ruta eliminada vuelven a pending |
| Rutas sin conductor/vehículo | GET /routes?has_assignment=false | Sin asignación activa |
| Rutas listas para asignar | GET /routes?has_assignment=false&status=optimized | Optimizadas, sin flota |
| Conductores libres | GET /drivers?available_only=true | Activos, sin vehículo asignado |
| Vehículos libres | GET /vehicles?available_only=true | Disponibles, sin conductor |
# Rutas sin asignación (ejemplo)
curl "$MASSIMPLE_BASE/routes?has_assignment=false&status=optimized&limit=50" \
-H "Authorization: Bearer $MASSIMPLE_KEY"
Detalle de cada query param en API Reference → Listar rutas.
Creá y optimizá una ruta
Con scopes routes:write y routes:optimize. Podés armar una ruta desde envíos pendientes:
curl -X POST $MASSIMPLE_BASE/routes \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_id": "ROUTE-2026-001",
"name": "Ruta mañana CABA",
"shipment_external_ids": ["ORD-1001", "ORD-1002"],
"optimize": true
}'
Respuesta 201 con id (rte_...), status (p. ej. optimized) y stops_count. Para agregar o quitar envíos en una ruta existente: PATCH /v1/routes/:id con add_shipment_ids / remove_shipment_ids (solo draft/optimized sin assignment). Para reordenar paradas: PATCH /v1/routes/:id/stops/sequence. Para optimizar después: POST /v1/routes/:id/optimize.
routes:delete. Solo draft u optimized sin assignment activo. Si la ruta está asignada → primero POST /v1/routes/:id/unassign, después DELETE. Respuesta 204 sin body.
pending con assigned_route_id en null (aunque en la ruta hubieran quedado en assigned). Volvé a verlos con GET /v1/shipments/available y armá otra ruta con POST /v1/routes.
# Borrar ruta (sin assignment)
curl -i -X DELETE "$MASSIMPLE_BASE/routes/rte_507f1f77bcf86cd799439011" \
-H "Authorization: Bearer $MASSIMPLE_KEY"
# Por external_id del ERP
curl -i -X DELETE "$MASSIMPLE_BASE/routes/external/ROUTE-2026-001" \
-H "Authorization: Bearer $MASSIMPLE_KEY"
Con assignment en la ruta, DELETE responde 409. Detalle en API Reference → Rutas: eliminar, desasignar y liberar flota.
Flota: vehículo y conductor
Scopes vehicles:* y drivers:*. Mismos patrones que envíos: CRUD, external_id y bulk.
# Vehículo
curl -X POST $MASSIMPLE_BASE/vehicles \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "VEH-01",
"license_plate": "AB123CD",
"distribution_center_id": "dc_507f1f77bcf86cd799439012"
}'
# Conductor (sin password → temporary_password en la respuesta)
curl -X POST $MASSIMPLE_BASE/drivers \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "DRV-01",
"document": "30111222",
"phone": "+5491112345678",
"license_number": "LIC-123",
"license_type": "B"
}'
La respuesta de crear conductor puede incluir temporary_password una sola vez. Guardala para el primer login en DriverApp. Si enviás password vos, no se devuelve.
DELETE /v1/vehicles/:id y DELETE /v1/drivers/:id responden 204 sin body si el recurso está libre (sin asignación activa). En tu cliente no parsees JSON: usá res.ok o status 204. Si hay assignment → 409.
Editar recursos (PATCH)
Cada PATCH solo acepta campos específicos; otros campos responden 400. Detalle en API Reference → Edición parcial.
| Recurso | Campos | Notas |
|---|---|---|
| Envío (PATCH) | content, amount_to_collect | No si delivered/cancelled/failed |
| Envío (cancelar) | POST .../cancel — error_comment opcional | Solo cancelled; failed es solo DriverApp |
| Envío (recrear) | POST .../recreate — scheduled_date requerido | Solo si fuente cancelled/failed; nuevo pending; emite shipment.created |
| Vehículo | mileage | Obligatorio |
| Conductor | phone, license_number, preferred_areas, experience | No nombre ni documento |
| Ruta | add_shipment_*, remove_shipment_* | Sin assignment; envíos de /shipments/available |
# Vehículo — solo kilometraje
curl -X PATCH $MASSIMPLE_BASE/vehicles/external/VEH-01 \\
-H "Authorization: Bearer $MASSIMPLE_KEY" \\
-H "Content-Type: application/json" \\
-d '{"mileage": 45200}'
# Ruta — agregar/quitar envíos
curl -X PATCH $MASSIMPLE_BASE/routes/external/ROUTE-2026-001 \\
-H "Authorization: Bearer $MASSIMPLE_KEY" \\
-H "Content-Type: application/json" \\
-d '{"add_shipment_external_ids":["ORD-1003"],"remove_shipment_external_ids":["ORD-1001"]}'
Asigná conductor y vehículo a una ruta
Scope assignments:write. Conductor y vehículo deben estar en el mismo centro de distribución y disponibles.
curl -X POST $MASSIMPLE_BASE/assignments \
-H "Authorization: Bearer $MASSIMPLE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_id": "ASG-001",
"driver_external_id": "DRV-01",
"vehicle_external_id": "VEH-01",
"route_external_id": "ROUTE-2026-001"
}'
Antes de asignar, podés listar recursos libres (paso 6b). Para liberar flota: si la asignación tiene ruta, primero POST /v1/routes/:id/unassign (ruta assigned, sin haber iniciado). Eso desvincula la ruta y deja conductor+vehículo en la asignación (active). Después POST /v1/assignments/:id/unassign solo si route_id es null — con ruta vinculada el endpoint de asignación responde 409. La ruta optimizada vuelve a listarse con GET /routes?has_assignment=false.
Ejecución en ruta (app chofer)
Después de POST /assignments, el chofer opera la ruta desde la DriverApp (sliders). Eso no usa endpoints públicos /v1: es API corp / SDUI interna (PUT /api/corp/routes/:id/change-status-to-stop, acción deliver_current_stop).
pending según tráfico (Google Routes). Si cambia el orden, recibís route.optimized en tu webhook.
Webhooks de seguimiento. Cada slider del chofer emite route.stop.status_changed y shipment.status_changed. Al completar la última parada, route.status_changed hacia completed. El payload de parada incluye tracking_number, external_id, stop_type, sequence, previous_status y current_status.
| Acción chofer | Eventos webhook típicos |
|---|---|
| Comenzar / en camino | route.stop.status_changed, shipment.status_changed (in_transit) |
| Entrega completada | Igual + posible route.optimized |
| Última parada | + route.status_changed → completed |
Auditoría: GET /v1/events?types=route.stop.status_changed,shipment.status_changed (events:read) lista lo emitido aunque falle el POST al webhook.
Consultá la secuencia actualizada con GET /v1/routes/:id/stops. Detalle en API Reference → Ejecución en ruta y en Dashboard → Integraciones → Endpoints.
cd backend && npm run sync-openapi-v2 antes de desplegar la web.