Quickstart

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.

1

Conseguí tu API key

Pedile a tu administrador que entre al dashboard, sección IntegracionesAPI Keys, y genere una key con al menos:

  • shipments:read shipments:write shipments:delete
  • routes:read routes:write routes:delete routes:optimize
  • vehicles:read vehicles:write vehicles:delete
  • drivers:read drivers:write drivers:delete
  • assignments:read assignments:write assignments:delete
  • events:read webhooks: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.

Tip. Poné los valores en variables para no mezclar entornos: export MASSIMPLE_BASE=https://api.massimple.la/v1, export MASSIMPLE_KEY=mss_test_xxx.
2

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": "..."
}
3

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):

CampoRequeridoNotas
destinationObjeto destino
destination.addressDirección; si no hay lat/lng se geocodifica
destination.latitude / longitudeNo**Recomendados para ruteo
contentDescripción del envío
origin_distribution_center_idNoObligatorio si tenés más de un CD
external_id, amount_to_collect, scheduled_date, metadataNoOpcionales
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.

4

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.

Probar sin código. Usá webhook.site: copiá la URL única, registrála acá o por API, elegí eventos * 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.
Importante. Cada delivery viene con el header 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:

  1. Recibís POST application/json en la URL registrada.
  2. Leés el body como string exacto (bytes tal cual llegaron).
  3. Parseás t= y v1= del header X-MasSimple-Signature.
  4. Calculás HMAC-SHA256(secret, "<t>.<body-raw>") en hex.
  5. Comparás con v1 (crypto.timingSafeEqual). Rechazá si falta header, firma inválida o timestamp > 5 min.
  6. Si es válido → parseás JSON → procesás → respondés 200.
5

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).

5b

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).

QueryUso
limitMáx por página (default 50, máx 200)
typesFiltro coma-separado, ej. shipment.status_changed,route.stop.status_changed
afterCursor 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.

6

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).

6b

Consultá qué está libre para asignar

No hay endpoints separados /unassigned. Usá filtros en los listados:

RecursoRequestQué devuelve
Envíos para rutearGET /shipments/availablePendientes sin ruta
Tras borrar una rutaGET /shipments/availableLos envíos de la ruta eliminada vuelven a pending
Rutas sin conductor/vehículoGET /routes?has_assignment=falseSin asignación activa
Rutas listas para asignarGET /routes?has_assignment=false&status=optimizedOptimizadas, sin flota
Conductores libresGET /drivers?available_only=trueActivos, sin vehículo asignado
Vehículos libresGET /vehicles?available_only=trueDisponibles, 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.

7

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.

Eliminar ruta. Scope 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.
Envíos tras el DELETE. Todos los envíos de esa ruta pasan siempre a 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.

8

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.

Borrar flota. 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.
8b

Editar recursos (PATCH)

Cada PATCH solo acepta campos específicos; otros campos responden 400. Detalle en API Reference → Edición parcial.

RecursoCamposNotas
Envío (PATCH)content, amount_to_collectNo si delivered/cancelled/failed
Envío (cancelar)POST .../cancelerror_comment opcionalSolo cancelled; failed es solo DriverApp
Envío (recrear)POST .../recreatescheduled_date requeridoSolo si fuente cancelled/failed; nuevo pending; emite shipment.created
VehículomileageObligatorio
Conductorphone, license_number, preferred_areas, experienceNo nombre ni documento
Rutaadd_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"]}'
9

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.

10

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).

Re-optimización automática. Cada vez que el chofer termina una parada (entrega, fallo o skip), el backend reordena solo las paradas 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 choferEventos webhook típicos
Comenzar / en caminoroute.stop.status_changed, shipment.status_changed (in_transit)
Entrega completadaIgual + 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.

Listo. Con los pasos anteriores tenés el flujo de integración: envíos → ruta → flota → asignación → ejecución en app chofer (webhooks). La API pública no expone cambio de estado de paradas en ruta. La API Reference documenta PATCH restrictivo por recurso y re-optimización automática de pendientes. Tras cambios en el backend: cd backend && npm run sync-openapi-v2 antes de desplegar la web.