MMoovExpressDocs

Integrare come piattaforma

Aggiungi MoovExpress come corriere dentro la tua piattaforma, e lascia che siano i tuoi utenti a collegare il proprio account

Se gestisci una piattaforma — un gestionale di spedizioni, un e-commerce SaaS, un marketplace — puoi offrire MoovExpress ai tuoi utenti come corriere disponibile. Scrivi il connettore una volta sola; poi ogni tuo utente collega il proprio account MoovExpress incollando la sua API key.

Il modello in una riga

Tu costruisci il connettore. Il tuo utente incolla la sua API key MoovExpress (una per corriere). Tu, con quella chiave, chiami la nostra API per conto suo.

Nessuna chiave condivisa, nessun account "di piattaforma": ogni chiamata avviene con la chiave del singolo utente, e vede solo i dati del suo contratto.

Tre cose da sapere prima di iniziare

▲

Conserva le API key dei tuoi utenti cifrate e mai in codice lato browser. Valgono come una password: permettono di creare spedizioni e addebitare il credito del contratto collegato.

Il flusso di integrazione

1. Chiedi la API key (una per corriere)

Nel punto in cui l'utente aggiunge un corriere — es. "Collega Poste di MoovExpress" — mostra un campo per incollare la chiave (mvx_live_…).

2. Valida e identifica la connessione

Appena ricevi la chiave, chiama GET /api/v1/account: è il test di connessione e ti dice quale corriere è quella chiave, così etichetti la connessione senza chiederlo all'utente.

curl https://moovexpress.com/api/v1/account \
  -H "Authorization: Bearer mvx_live_xxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://moovexpress.com/api/v1/account", {
  headers: { Authorization: `Bearer ${chiaveUtente}` },
});
if (res.status === 401) throw new Error("Chiave non valida");
const acc = await res.json();
// acc.contratto → es. "Poste" : usalo come etichetta della connessione
// acc.contratto_attivo, acc.listino_attivo, acc.credito → mostra lo stato all'utente
{ "ok": true, "account": "Cliente Demo Srl", "contratto": "Standard Nazionale",
  "contratto_attivo": true, "listino_attivo": true, "credito": 150.00, "valuta": "EUR" }

3. Le operazioni, per conto dell'utente

Da qui in poi usi sempre la chiave di quell'utente. Ogni chiamata è isolata sul suo contratto.

PassoEndpointA cosa serve
PreventivoPOST /api/v1/ratesPrezzo del corriere per quel pacco e destinazione
Crea spedizionePOST /api/v1/shipmentsGenera LDV + tracking + label_url + prezzo
EtichettaGET /api/v1/shipments/{id}/labelScarica il PDF dell'etichetta
TrackingGET /api/v1/tracking/{tracking}Stato ed eventi (meglio i webhook)
RitiroPOST /api/v1/pickupsPrenota il ritiro delle spedizioni
DistintaPOST /api/v1/shipping-listsChiude la distinta di viaggio
AnnullaDELETE /api/v1/shipments/{id}Annulla una spedizione (reversibile 48h)

Dettaglio di ogni endpoint, con parametri e risposte, nel riferimento API.

▲

Come si chiede il ritiro. Chiedilo sempre alla creazione, con il campo pickup nel corpo di POST /shipments (data: un giorno lavorativo, da oggi in poi):

{ "packages": [ ... ], "shipFrom": { ... }, "shipTo": { ... },
  "pickup": { "requested": true, "date": "2026-08-12", "time": "mattina" } }

Poi leggi il blocco ritiro della risposta:

  • "richiesto": true → il ritiro è prenotato presso il corriere. Il codice spesso arriva qualche minuto dopo: se è null, rileggilo con GET /shipments/{id}. Su questi contratti il ritiro non si può aggiungere dopo, quindi non chiamare POST /pickups (risponde 400).
  • "richiesto": false → su questo contratto il ritiro non si prenota insieme alla spedizione. Il campo avviso dice come richiederlo: di norma con POST /pickups.

4. Ricevi gli aggiornamenti di stato

Per il tracking, preferisci i webhook: li registra il singolo utente (o li registri tu per suo conto) e ti notifichiamo noi appena una spedizione cambia stato — così non interroghi /tracking in continuazione.

Esempio completo, dalla tariffa alla consegna

Un flusso reale end-to-end con la chiave di un tuo utente: preventivo → creazione → etichetta, poi la ricezione degli aggiornamenti via webhook. È in Node.js, ma la sequenza e i campi sono identici in qualunque linguaggio.

Preventivo, spedizione ed etichetta

const BASE = "https://moovexpress.com/api/v1";

// Piccolo wrapper: usa SEMPRE la chiave del singolo utente (una per corriere).
async function moov(path, { key, method = "GET", body } = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (res.status === 429) {
    const attesa = Number(res.headers.get("Retry-After") || 5);
    throw new Error(`Limite di frequenza: riprova tra ${attesa}s`);
  }
  const data = await res.json();
  if (!res.ok) throw new Error(data.error || `Errore ${res.status}`); // 401, 402 credito, 400…
  return data;
}

async function creaSpedizione(key) {
  // 0) Verifica la chiave e scopri QUALE corriere è (per etichettare la connessione).
  const acc = await moov("/account", { key });
  console.log(`Connesso a ${acc.contratto} — credito € ${acc.credito}`);

  const packages = [{ weight: 2.5, length: 30, width: 20, height: 15 }]; // kg, cm
  const shipFrom = {
    name: "Magazzino del merchant", street1: "Via Torino 5", city: "Milano",
    state: "MI", postalCode: "20100", country: "IT",
    phone: "0212345678", email: "logistica@merchant.it",
  };
  const shipTo = {
    name: "Mario Rossi", street1: "Via Roma 10", city: "Roma",
    state: "RM", postalCode: "00100", country: "IT",
    phone: "3331234567", email: "mario.rossi@example.com",
  };

  // 1) Preventivo (facoltativo: per mostrare il prezzo prima di confermare).
  const rate = await moov("/rates", { key, method: "POST", body: { packages, shipTo } });
  console.log(`Prezzo previsto: € ${rate.prezzo} (${rate.contratto})`);

  // 2) Crea la spedizione: torna id, tracking, prezzo effettivo e il link all'etichetta.
  const sped = await moov("/shipments", {
    key, method: "POST",
    body: { packages, shipFrom, shipTo, contenuto: "Abbigliamento", notes: "Citofonare Rossi" },
  });
  console.log(`Creata ${sped.tracking} — € ${sped.prezzo} — stato ${sped.stato}`);

  // 3) Scarica l'etichetta (PDF). label_url è già pronto nella risposta.
  const pdfRes = await fetch(BASE + `/shipments/${sped.id}/label`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  const etichettaPdf = Buffer.from(await pdfRes.arrayBuffer()); // salvala o inviala in stampa

  return { id: sped.id, tracking: sped.tracking, etichettaPdf };
}

Ricevere gli aggiornamenti (endpoint webhook)

Il tuo utente registra l'URL del tuo webhook dal suo portale MoovExpress e riceve un secret (whsec_…): conservalo per quel merchant e usalo per verificare la firma di ogni evento.

import express from "express";
import crypto from "crypto";

const app = express();
// Serve il CORPO GREZZO: la firma si calcola su di esso, prima di ogni parsing.
app.use("/webhook/moov", express.raw({ type: "application/json" }));

app.post("/webhook/moov", (req, res) => {
  const raw = req.body; // Buffer grezzo
  const parti = Object.fromEntries(
    (req.get("Webhook-Signature") || "").split(",").map((p) => p.split("="))
  );
  const t = Number(parti.t);

  // 1) Anti-replay (scarta oltre 5 minuti) + firma HMAC-SHA256 timing-safe.
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(401);
  const atteso = crypto
    .createHmac("sha256", secretDelMerchant) // il whsec_ di QUESTO merchant
    .update(`${t}.${raw.toString("utf8")}`)
    .digest("hex");
  const a = Buffer.from(atteso), b = Buffer.from(parti.v1 || "");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  // 2) Rispondi SUBITO 200, poi elabora in background (eviti timeout e retry).
  res.sendStatus(200);

  const { event, data } = JSON.parse(raw.toString("utf8"));
  switch (event) {
    case "tracking.updated":   /* aggiorna lo stato dell'ordine del merchant */ break;
    case "tracking.delivered": /* segna "consegnato": data.tracking_number, data.location */ break;
    case "tracking.exception": /* anomalia: avvisa il merchant */ break;
  }
});
✎

Il label_url nella risposta della creazione è già l'endpoint dell'etichetta di quella spedizione: puoi usarlo così com'è (prefissato con l'host) invece di ricomporre l'URL a mano.

Cosa NON vedrai (ed è voluto)

I corrieri effettivi e il trasporto dietro le quinte ti arrivano già con nomi neutri (il nome del contratto, es. "Poste", "Standard Nazionale"). I fornitori tecnici a monte non compaiono mai nelle risposte: né a te, né ai tuoi utenti.

Buone pratiche

✦

Parti sempre da GET /api/v1/account: è gratis, non consuma quota, e ti conferma in un colpo che la chiave è valida, quale corriere rappresenta e quanto credito ha l'utente.