CRM AD HOC Documentazione

Integrations webhook

Un unico URL a cui il tuo ecommerce (Shopify, WooCommerce, PrestaShop, Magento o un sistema su misura) o uno strumento come Zapier e Make invia contatti, carrelli abbandonati e ordini. Il CRM accetta qualsiasi JSON e ne ricava i dati da solo.

A ogni chiamata il CRM:

  • cerca nel payload telefono, email e nome del cliente, a qualsiasi profondità, e crea o aggiorna il contatto;
  • gli aggiunge un tag con il nome della piattaforma di origine (es. shopify);
  • se l'attività ha l'ecommerce collegato, registra anche il carrello o l'ordine, con i suoi articoli.

Il webhook non invia messaggi. Li mandano le automazioni del CRM («Carrello abbandonato», «Nuovo ordine», «Post-acquisto»), che lavorano sui carrelli e sugli ordini ricevuti.

Endpoint

POST https://crm.adhocweb.it/wh/integrations/{token}

Il {token} identifica l'attività che riceve i dati ed è l'unica credenziale richiesta: nessun header di autenticazione, così funziona anche con le piattaforme che non permettono di aggiungere header (Shopify, WooCommerce). Trattalo come una password.

L'URL completo si trova nel CRM in Aziende → Gestione Azienda → Integrazioni webhook. Da lì si genera, si rigenera (il vecchio URL smette subito di funzionare) o si disattiva, e si accende l'opzione Ecommerce collegato, che serve a registrare carrelli e ordini. Senza questa opzione il webhook registra solo i contatti.

Parametri nell'URL (facoltativi)

ParametroValoriA cosa serve
sourcetesto, es. shopifyNome della piattaforma: diventa il tag del contatto e l'origine di carrelli e ordini. Se manca si ricava dagli header di Shopify o WooCommerce, poi dallo User-Agent; in ultima istanza vale webhook.
eventcontact, cart, orderTipo di evento. Se manca si riconosce da solo (vedi sotto). Indicalo quando invii JSON scritti da te, così non ci sono ambiguità.

Esempio: https://crm.adhocweb.it/wh/integrations/{token}?source=shopify&event=cart

Come viene riconosciuto il tipo di evento

  1. Il parametro event, se c'è.
  2. L'header di topic della piattaforma: X-Shopify-Topic (checkouts/* = carrello, orders/* = ordine, customers/* = contatto) oppure X-WC-Webhook-Topic (order.* = ordine, customer.* = contatto).
  3. Le chiavi del payload: order_number, order_id, financial_status, fulfillment_status o date_paid indicano un ordine; abandoned_checkout_url, recovery_url, checkout_url, cart_url, cart_token o cart_id un carrello.
  4. Se nessuno di questi lo indica, l'evento è un contatto: viene registrato solo il cliente.

Dati del contatto

Il payload può avere qualsiasi forma: le chiavi qui sotto vengono cercate a qualsiasi profondità, partendo dai livelli più esterni (il telefono del cliente vince su quello di un indirizzo). Le chiavi non distinguono maiuscole e minuscole. Se non trova né un telefono né un'email, il CRM risponde 200 con "status": "ignored" e non registra niente.

DatoChiavi riconosciute
Telefonophone, telephone, telefono, mobile, cellulare, phone_number, mobile_phone, billing_phone, tel
Emailemail, e_mail, email_address, billing_email, contact_email
Nomefirst_name, firstname, nome, given_name; in alternativa name, full_name, customer_name dentro un oggetto customer, billing, shipping, contact, user o buyer (diviso in nome e cognome)
Cognomelast_name, lastname, cognome, family_name, surname
Id cliente sulla piattaformacustomer.id, customer_id, customer.external_id, user_id
Consenso marketingmarketing_consent, buyer_accepts_marketing, accepts_marketing, customer.accepts_marketing, customer.email_marketing_consent.state, newsletter

Il telefono viene normalizzato (solo cifre, prefisso 39 sui numeri italiani senza prefisso). Il contatto si ritrova per id cliente della piattaforma, poi per telefono, poi per email. I dati ricevuti riempiono o aggiornano i campi, ma non li svuotano mai: un checkout senza cognome non cancella il cognome già presente.

Carrello abbandonato

Invia il carrello ogni volta che il cliente lo crea o lo modifica e lascia un recapito (tipicamente al primo passo del checkout). Ogni invio aggiorna lo stesso carrello, riconosciuto dal suo id. Il carrello resta abbandonato finché non arriva l'ordine: è l'automazione «Carrello abbandonato» a decidere dopo quanto tempo dall'ultimo aggiornamento scrivere al cliente.

Ogni cliente ha un solo carrello abbandonato attivo: se ne arriva uno nuovo con un id diverso (altra sessione, altro dispositivo), i precedenti ancora abbandonati passano a scaduto e le automazioni scrivono solo per l'ultimo. Così due carrelli lasciati a pochi minuti di distanza non fanno partire due promemoria.

Campo consigliatoAlternative riconosciuteNote
cart_idtoken, checkout_id, cart_token, id, cart_hashId stabile del carrello: lo stesso id aggiorna lo stesso carrello. Senza id il CRM tiene un solo carrello aperto per contatto.
recovery_urlabandoned_checkout_url, recover_url, checkout_url, cart_urlLink che riapre il carrello del cliente. I messaggi usano un link tracciato del CRM che porta qui.
totaltotal_price, grand_total, cart_total, amountImporto in unità, non in centesimi: 89.90 (va bene anche "89,90").
currencycurrency_code, presentment_currencyCodice ISO a 3 lettere, es. EUR.
itemsline_items, products, cart_items, linesArticoli, vedi Articoli.
couponcoupon_code, discount_code, discount_codes[].codeCodice sconto applicato.
statusstateabandoned (predefinito) oppure converted / completed se il carrello è diventato un ordine. Vale anche completed_at valorizzato.
created_atdate_createdData ISO 8601 o timestamp Unix.
updated_atabandoned_at, date_modified, last_activityUltima attività del cliente: da qui si misura l'abbandono. Se manca vale l'ora di ricezione.
customer—Dati del cliente, vedi Dati del contatto.
{
  "event_id": "evt_8f2a1c",
  "cart_id": "C-1024",
  "status": "abandoned",
  "currency": "EUR",
  "total": 89.90,
  "coupon": "WELCOME10",
  "recovery_url": "https://negozio.it/cart/recover/C-1024?key=abc",
  "updated_at": "2026-10-20T10:12:00+02:00",
  "customer": {
    "id": "9876",
    "first_name": "Mario",
    "last_name": "Rossi",
    "email": "mario.rossi@example.com",
    "phone": "+39 333 123 4567",
    "marketing_consent": true
  },
  "items": [
    { "sku": "TSH-RED-M", "name": "T-shirt", "variant": "Rosso / M", "quantity": 2, "price": 29.95 },
    { "sku": "CAP-01", "name": "Cappellino", "quantity": 1, "price": 20.00 }
  ]
}
Se il cliente ha negato il consenso marketing (marketing_consent: false) il carrello viene salvato ma nessuna automazione di recupero gli scrive. Lo stesso vale per i contatti che si sono disiscritti dai messaggi.

Ordine

Invia l'ordine quando viene creato e a ogni cambio di stato (pagamento, spedizione, annullo, rimborso): ogni invio aggiorna lo stesso ordine, riconosciuto dal suo id. Quando arriva un ordine il CRM:

  • chiude il carrello da cui è nato, citato con cart_id / checkout_token / cart_token, oppure in mancanza l'ultimo carrello abbandonato dello stesso cliente nelle 72 ore precedenti. Gli altri carrelli abbandonati del cliente in quelle 72 ore passano a scaduto: chi ha comprato non riceve più promemoria. I carrelli lasciati dopo l'ordine restano attivi, perché sono un nuovo acquisto in sospeso;
  • ricalcola numero di ordini, totale speso, data del primo e dell'ultimo ordine del contatto. Gli ordini annullati e rimborsati non contano.
Campo consigliatoAlternative riconosciuteNote
order_ididId stabile dell'ordine (obbligatorio, altrimenti si usa il numero).
order_numbernumber, nameNumero mostrato al cliente.
cart_idcheckout_token, cart_token, checkout_id, cart_hashCarrello da cui nasce l'ordine.
statusorder_status, statepending, processing / paid, completed / shipped / delivered, cancelled / failed, refunded. Riconosce anche gli stati di Shopify (financial_status, fulfillment_status, cancelled_at) e di WooCommerce.
payment_statusfinancial_statusEs. paid, pending.
totaltotal_price, grand_total, amountImporto in unità.
subtotal, shipping_total, discount_total, tax_totalsubtotal_price, shipping, total_discounts, total_taxFacoltativi.
currencycurrency_codeCodice ISO, es. EUR.
payment_methodpayment_method_title, gatewayFacoltativo.
coupondiscount_codes[].code, coupon_lines[].codeFacoltativo.
shipping_addressshipping, billing_address, billingDel CRM usa solo città, CAP (zip / postcode), provincia (province_code / state) e nazione: via e civico non vengono salvati.
created_atdate_created, processed_at, order_dateData dell'ordine: la usano le automazioni «Nuovo ordine» e «Post-acquisto».
paid_atdate_paidFacoltativo.
itemsline_items, products, order_itemsArticoli, vedi sotto.
{
  "event_id": "evt_91bd0e",
  "order_id": "5550001",
  "order_number": "1001",
  "cart_id": "C-1024",
  "status": "processing",
  "payment_status": "paid",
  "currency": "EUR",
  "total": 89.90,
  "shipping_total": 10.00,
  "discount_total": 10.00,
  "payment_method": "Carta di credito",
  "created_at": "2026-10-20T11:00:00+02:00",
  "shipping_address": { "city": "Macerata", "zip": "62100", "province_code": "MC", "country_code": "IT" },
  "customer": { "id": "9876", "first_name": "Mario", "last_name": "Rossi", "phone": "+393331234567", "email": "mario.rossi@example.com" },
  "items": [
    { "sku": "TSH-RED-M", "name": "T-shirt", "quantity": 2, "price": 29.95 }
  ]
}

Articoli

CampoAlternative
sku— (serve alla regola delle automazioni «Contiene il prodotto»)
nametitle, product_name
variantvariant_title, variation
quantityqty
priceunit_price (prezzo unitario)
totalline_total, line_price (se manca: prezzo × quantità)
product_idvariant_id, id
categoryproduct_type
image_url, product_urlimage.src, permalink

Esempi di chiamata

curl -X POST "https://crm.adhocweb.it/wh/integrations/IL_TUO_TOKEN?source=mioshop&event=cart" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "evt_8f2a1c",
    "cart_id": "C-1024",
    "total": 89.90,
    "currency": "EUR",
    "recovery_url": "https://negozio.it/cart/recover/C-1024",
    "customer": { "first_name": "Mario", "phone": "+39 333 123 4567", "email": "mario.rossi@example.com" },
    "items": [ { "sku": "TSH-RED-M", "name": "T-shirt", "quantity": 2, "price": 29.95 } ]
  }'
$url = 'https://crm.adhocweb.it/wh/integrations/IL_TUO_TOKEN?source=mioshop&event=order';
$payload = [
    'event_id' => 'order-' . $order->id . '-' . $order->updated_at,
    'order_id' => (string) $order->id,
    'order_number' => $order->number,
    'cart_id' => $order->cart_id,
    'status' => $order->status,
    'total' => $order->total,
    'currency' => 'EUR',
    'created_at' => $order->created_at,
    'customer' => ['first_name' => $order->first_name, 'phone' => $order->phone, 'email' => $order->email],
    'items' => array_map(fn($r) => ['sku' => $r->sku, 'name' => $r->name, 'quantity' => $r->qty, 'price' => $r->price], $order->rows),
];

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
]);
$risposta = json_decode(curl_exec($ch), true);
$codice = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($codice >= 500) {
    // errore temporaneo: riprova piu' tardi con lo stesso event_id
}
const risposta = await fetch('https://crm.adhocweb.it/wh/integrations/IL_TUO_TOKEN?source=mioshop&event=cart', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    event_id: 'evt_8f2a1c',
    cart_id: 'C-1024',
    total: 89.90,
    currency: 'EUR',
    recovery_url: 'https://negozio.it/cart/recover/C-1024',
    customer: { first_name: 'Mario', phone: '+39 333 123 4567', email: 'mario.rossi@example.com' },
    items: [{ sku: 'TSH-RED-M', name: 'T-shirt', quantity: 2, price: 29.95 }]
  })
});
console.log(await risposta.json()); // { status: 'ok', contact: { id: 4023 }, event: 'cart', cart: { id: 17, status: 'ABBANDONATO' } }
Il token è una credenziale: chiama il webhook dal server, mai dal browser del cliente.

Risposte

Il body accettato è JSON (Content-Type: application/json) oppure form-encoded. La risposta è sempre JSON.

HTTPBodySignificato
200{"status": "ok", "contact": {"id": 4023}, "event": "cart", "cart": {"id": 17, "status": "ABBANDONATO"}}Elaborato. Per gli ordini c'è order al posto di cart; per i contatti nessuno dei due.
200{"status": "ok", ..., "duplicate": true}Evento già ricevuto (stesso id di consegna o stesso body): confermato senza rielaborarlo.
200{"status": "ignored", "message": "No phone or email found"}Nessun recapito nel payload: niente da registrare. La risposta è 200 apposta, così la piattaforma non disattiva il webhook (è il caso anche del ping di prova di WooCommerce).
200{"status": "ok", ..., "message": "Ecommerce not enabled, contact saved only"}Carrello o ordine ricevuto con l'ecommerce non collegato: salvato solo il contatto. L'evento resta nel log e si può rielaborare dopo aver acceso l'opzione.
404{"status": "error", "message": "Unknown webhook token"}Token errato, rigenerato o webhook disattivato.
500 / 503{"status": "error", "message": "Processing failed"}Errore temporaneo. L'evento è comunque salvato: ripeti l'invio (le piattaforme lo fanno da sole) e verrà rielaborato.

Reinvii e doppioni

Lo stesso evento può arrivare più volte (le piattaforme ritentano se la risposta tarda). Il CRM lo elabora una volta sola, riconoscendolo da, in ordine:

  1. l'id di consegna della piattaforma: X-Shopify-Webhook-Id o X-WC-Webhook-Delivery-ID;
  2. il campo event_id (o idempotency_key) del payload: se invii JSON tuoi, valorizzalo con un id unico per ogni modifica;
  3. in mancanza, l'impronta del body: due body identici sono lo stesso evento.

Gli aggiornamenti dello stesso carrello o ordine sono eventi diversi e lo riscrivono: la chiave del carrello e dell'ordine è il suo id sulla piattaforma.

Configurazione su Shopify

  1. Nell'admin di Shopify apri Impostazioni → Notifiche → Webhook e premi Crea webhook.
  2. Crea un webhook per ciascuno di questi eventi, tutti con formato JSON e come URL quello del CRM (con ?source=shopify):
    • Creazione checkout e Aggiornamento checkout: i carrelli abbandonati, con il link di recupero;
    • Creazione ordine, Aggiornamento ordine e Annullamento ordine: gli ordini e i loro stati.
  3. Per provarli usa Invia notifica di prova. La notifica di prova di Shopify contiene dati fittizi: comparirà nel log degli eventi del CRM.

Il CRM riconosce da solo gli header X-Shopify-Topic e X-Shopify-Webhook-Id: non serve indicare event.

Configurazione su WooCommerce

  1. In WordPress apri WooCommerce → Impostazioni → Avanzate → Webhook e premi Aggiungi webhook.
  2. Stato Attivo, argomento Ordine creato, URL di consegna quello del CRM (con ?source=woocommerce), versione API WP REST API v3. Il campo «Segreto» non serve.
  3. Ripeti con l'argomento Ordine aggiornato, così arrivano pagamenti, completamenti e annullamenti.

WooCommerce non traccia i carrelli abbandonati. Serve un plugin che li registri e possa inviarli a un webhook: configuralo con l'URL del CRM e ?source=woocommerce&event=cart, e assicurati che mandi il link di recupero del carrello.

Altre piattaforme

PrestaShop, Magento o un sistema su misura: invia i JSON di esempio di questa pagina dai punti in cui il carrello e l'ordine cambiano. Con Zapier o Make basta un passo «Webhook / HTTP request» in POST con body JSON verso l'URL del CRM.

Cosa succede dopo

  • Il contatto compare in anagrafica con il tag della piattaforma. Nella scheda contatto il riquadro Acquisti riassume ordini e totale speso; ordini e carrelli sono anche nelle attività, sotto il filtro «Acquisti».
  • Nelle Automazioni si sbloccano gli eventi Carrello abbandonato, Nuovo ordine e Post-acquisto, con i modelli pronti «Recupero carrello» e «Recensione dopo 7 giorni». Ogni automazione scrive una volta sola per carrello o per ordine.
  • Nei messaggi si usano i segnaposto {{carrello.url}}, {{carrello.prodotti}}, {{carrello.prodotti_breve}} (primi 2 prodotti e «e altri N prodotti», adatto ai template), {{carrello.totale}}, {{ordine.numero}}, {{ordine.totale}} e altri. carrello.url è un link del CRM (https://crm.adhocweb.it/r/c/…) che registra il clic e porta al link di recupero della piattaforma.
  • Nei template WhatsApp con pulsante a URL dinamico, imposta l'URL del pulsante su Meta a {url del CRM}/r/c/{{1}} e nel passo dell'automazione scegli il parametro Link carrello.
  • Quando arriva l'ordine il carrello diventa recuperato (se un'automazione gli aveva scritto) o convertito (se il cliente è tornato da solo).

Dati salvati e conservazione

  • Si salvano contatto, carrelli, ordini e articoli. Dell'indirizzo restano solo città, CAP, provincia e nazione; i dati di pagamento non vengono letti.
  • Il payload originale di ogni chiamata resta nel log degli eventi (Gestione Azienda → Integrazioni webhook → Eventi ricevuti) per 90 giorni, poi viene cancellato.
  • I carrelli abbandonati da più di 30 giorni passano a scaduto e nessuna automazione li riprende.