Quickstart

Get started in 5 minutes

Get an API key, create shipments and routes, manage fleet (vehicles, drivers, assignments) and subscribe to webhooks. The API Reference documents the 41 HTTP routes (68 operations) of the current contract.

1

Get your API key

Ask your administrator to open the dashboard, go to IntegrationsAPI Keys, and generate a key with at least:

  • 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

The key is shown ONCE when created. Store it in your secrets manager. Format: mss_live_* for production or mss_test_* for sandbox.

Tip. Put values in environment variables to avoid mixing environments: export MASSIMPLE_BASE=https://api.massimple.la/v1, export MASSIMPLE_KEY=mss_test_xxx.
2

Validate that the key works

The /v1/whoami endpoint returns the application and company your key points to. Sanity check before anything else.

curl $MASSIMPLE_BASE/whoami \
  -H "Authorization: Bearer $MASSIMPLE_KEY"

Expect 200 with:

{
  "application": {
    "id": "...",
    "name": "Mi integración",
    "env": "test",
    "scopes": ["shipments:write", "events:read", "webhooks:manage"]
  },
  "company_id": "..."
}
3

Create your first shipment

POST to /v1/shipments. Do not send companyId or status: the shipment is always created as pending. If your company has a single distribution center, you can omit origin_distribution_center_id.

Main fields (full detail in API Reference → Create shipment):

FieldRequiredNotes
destinationYesDestination object
destination.addressYesAddress; geocoded if no lat/lng
destination.latitude / longitudeNo**Recommended for routing
contentYesShipment description
origin_distribution_center_idNoRequired if you have more than one DC
external_id, amount_to_collect, scheduled_date, metadataNoOptional
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"
  }'

201 response with public format: id (shp_...), tracking_number, external_id, etc.

4

Subscribe to events with a webhook

Register a public HTTPS URL in the dashboard (Integrations → Webhooks) or via API. Every time something changes (shipment status, route status, etc.) we send you a signed POST.

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": ["*"]
  }'

We return a secret that is shown ONCE. Store it to verify the HMAC signature of each delivery.

Try without code. Use webhook.site: copy the unique URL, register it here or via API, choose events * or the driver tracking preset (route.stop.status_changed, shipment.status_changed, route.optimized, route.status_changed). The event list cannot be edited after creating the webhook.
Important. Each delivery includes the header X-MasSimple-Signature: t=<unix-time>,v1=<hmac-hex>. The signature is computed over <timestamp>.<body-raw> using your secret (whsec_…). Always verify the signature on the RAW body (not parsed).

Flow on your server:

  1. You receive POST application/json at the registered URL.
  2. Read the body as the exact string (bytes as received).
  3. Parse t= and v1= from the X-MasSimple-Signature header.
  4. Compute HMAC-SHA256(secret, "<t>.<body-raw>") in hex.
  5. Compare with v1 (crypto.timingSafeEqual). Reject if the header is missing, the signature is invalid, or the timestamp is > 5 min old.
  6. If valid → parse JSON → process → respond 200.
5

Verify the signature of each webhook

Verification function + reference Express handler. Environment variable 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));
}

Complete Express handler (use express.raw only on this route, not global express.json()):

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 });
  },
);

Manual debug: echo -n "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex — the hex must match v1= in the header.

If the verifier returns true, you can parse the body and process the event. Return 200 at the end — any 5xx triggers our exponential retry (1m, 5m, 30m, 2h, 12h, 24h).

5b

Audit: query the stream with GET /events

Complement to webhooks: pull instead of push. Scope events:read. Lists what +simple emitted (30 days) even if the webhook POST fails or you have no public URL. Same CloudEvents format as a delivery (id, type, time, data).

QueryUse
limitMax per page (default 50, max 200)
typesComma-separated filter, e.g. shipment.status_changed,route.stop.status_changed
afternext_cursor from the previous page
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"
}

If next_cursor is not null, request the next page with ?after=next_cursor. In the Bazarshop demo (Events tab) you can try polling with filters without curl. Details in API Reference → Event polling and Dashboard → Integrations → Endpoints.

6

Operate by external_id

If you store your own order ID (ORD-1001), you don't need Massimple's shp_...:

# 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}'

Listing, updating and deleting by external_id use the same routes with prefix /shipments/external/.... PATCH only accepts content and amount_to_collect (not destination, dates or metadata). To cancel: POST /v1/shipments/{id}/cancel (or .../external/{external_id}/cancel) with optional body {"error_comment":"..."} — no need to send status. Emits shipment.status_changed and shipment.cancelled. The failed status is not in the public API: only the driver marks it from the DriverApp. To recreate a cancelled or failed shipment: POST /v1/shipments/{id}/recreate (or .../external/{external_id}/recreate) with {"scheduled_date":"YYYY-MM-DD"} — creates a new shipment in pending with the same data; the original does not change. Emits shipment.created. For bulk create: POST /v1/shipments/bulk and POST /v1/shipments/bulk-delete (both accept 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"}'

Shipments without an assigned route: GET /v1/shipments/available (pending, no assigned_route_id).

6b

Check what's available to assign

There are no separate /unassigned endpoints. Use filters on list endpoints:

ResourceRequestWhat it returns
Shipments to routeGET /shipments/availablePending without route
After deleting a routeGET /shipments/availableShipments from the deleted route return to pending
Routes without driver/vehicleGET /routes?has_assignment=falseNo active assignment
Routes ready to assignGET /routes?has_assignment=false&status=optimizedOptimized, no fleet
Available driversGET /drivers?available_only=trueActive, no vehicle assigned
Available vehiclesGET /vehicles?available_only=trueAvailable, no driver
# Rutas sin asignación (ejemplo)
curl "$MASSIMPLE_BASE/routes?has_assignment=false&status=optimized&limit=50" \
  -H "Authorization: Bearer $MASSIMPLE_KEY"

Details for each query param in API Reference → List routes.

7

Create and optimize a route

With scopes routes:write and routes:optimize. You can build a route from pending shipments:

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
  }'

201 response with id (rte_...), status (e.g. optimized) and stops_count. To add or remove shipments on an existing route: PATCH /v1/routes/:id with add_shipment_ids / remove_shipment_ids (only draft/optimized without assignment). To reorder stops: PATCH /v1/routes/:id/stops/sequence. To optimize later: POST /v1/routes/:id/optimize.

Delete route. Scope routes:delete. Only draft or optimized without active assignment. If the route is assigned → first POST /v1/routes/:id/unassign, then DELETE. Response 204 with no body.
Shipments after DELETE. All shipments on that route always go to pending with assigned_route_id null (even if they were assigned on the route). See them again with GET /v1/shipments/available and build another route with 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"

With an assignment on the route, DELETE returns 409. Details in API Reference → Routes: delete, unassign and release fleet.

8

Fleet: vehicle and driver

Scopes vehicles:* and drivers:*. Same patterns as shipments: CRUD, external_id and 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"
  }'

The create driver response may include temporary_password once. Store it for the first DriverApp login. If you send password yourself, it is not returned.

Delete fleet. DELETE /v1/vehicles/:id and DELETE /v1/drivers/:id return 204 with no body if the resource is free (no active assignment). In your client do not parse JSON: use res.ok or status 204. If there is an assignment → 409.
8b

Edit resources (PATCH)

Each PATCH only accepts specific fields; other fields return 400. Details in API Reference → Partial update.

ResourceFieldsNotes
Shipment (PATCH)content, amount_to_collectNot if delivered/cancelled/failed
Shipment (cancel)POST .../cancel — optional error_commentOnly cancelled; failed is DriverApp only
Shipment (recreate)POST .../recreatescheduled_date requiredOnly if source is cancelled/failed; new pending; emits shipment.created
VehiclemileageRequired
Driverphone, license_number, preferred_areas, experienceNot name or document
Routeadd_shipment_*, remove_shipment_*No assignment; shipments from /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

Assign driver and vehicle to a route

Scope assignments:write. Driver and vehicle must be in the same distribution center and available.

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"
  }'

Before assigning, you can list available resources (step 6b). To release fleet: if the assignment has a route, first POST /v1/routes/:id/unassign (route assigned, not yet started). That unlinks the route and leaves driver+vehicle on the assignment (active). Then POST /v1/assignments/:id/unassign only if route_id is null — with a linked route the assignment endpoint returns 409. The optimized route appears again with GET /routes?has_assignment=false.

10

Route execution (driver app)

After POST /assignments, the driver operates the route from the DriverApp (sliders). That does not use public /v1 endpoints: it is internal corp / SDUI API (PUT /api/corp/routes/:id/change-status-to-stop, action deliver_current_stop).

Automatic re-optimization. Every time the driver completes a stop (delivery, failure or skip), the backend reorders only pending stops based on traffic (Google Routes). If the order changes, you receive route.optimized on your webhook.

Tracking webhooks. Each driver slider emits route.stop.status_changed and shipment.status_changed. When the last stop is completed, route.status_changed toward completed. The stop payload includes tracking_number, external_id, stop_type, sequence, previous_status and current_status.

Driver actionTypical webhook events
Start / en routeroute.stop.status_changed, shipment.status_changed (in_transit)
Delivery completedSame + possible route.optimized
Last stop+ route.status_changed → completed

Audit: GET /v1/events?types=route.stop.status_changed,shipment.status_changed (events:read) lists what was emitted even if the webhook POST fails.

Check the updated sequence with GET /v1/routes/:id/stops. Details in API Reference → Route execution and Dashboard → Integrations → Endpoints.

Done. With the steps above you have the integration flow: shipments → route → fleet → assignment → driver app execution (webhooks). The public API does not expose in-route stop status changes. The API Reference documents restrictive PATCH per resource and automatic re-optimization of pending stops. After backend changes: cd backend && npm run sync-openapi-v2 before deploying the web.