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
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)
| Parametro | Valori | A cosa serve |
|---|---|---|
source | testo, es. shopify | Nome 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. |
event | contact, cart, order | Tipo 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
- Il parametro
event, se c'è. - L'header di topic della piattaforma:
X-Shopify-Topic(checkouts/*= carrello,orders/*= ordine,customers/*= contatto) oppureX-WC-Webhook-Topic(order.*= ordine,customer.*= contatto). - Le chiavi del payload:
order_number,order_id,financial_status,fulfillment_statusodate_paidindicano un ordine;abandoned_checkout_url,recovery_url,checkout_url,cart_url,cart_tokenocart_idun carrello. - 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.
| Dato | Chiavi riconosciute |
|---|---|
| Telefono | phone, telephone, telefono, mobile, cellulare, phone_number, mobile_phone, billing_phone, tel |
email, e_mail, email_address, billing_email, contact_email | |
| Nome | first_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) |
| Cognome | last_name, lastname, cognome, family_name, surname |
| Id cliente sulla piattaforma | customer.id, customer_id, customer.external_id, user_id |
| Consenso marketing | marketing_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 consigliato | Alternative riconosciute | Note |
|---|---|---|
cart_id | token, checkout_id, cart_token, id, cart_hash | Id stabile del carrello: lo stesso id aggiorna lo stesso carrello. Senza id il CRM tiene un solo carrello aperto per contatto. |
recovery_url | abandoned_checkout_url, recover_url, checkout_url, cart_url | Link che riapre il carrello del cliente. I messaggi usano un link tracciato del CRM che porta qui. |
total | total_price, grand_total, cart_total, amount | Importo in unità, non in centesimi: 89.90 (va bene anche "89,90"). |
currency | currency_code, presentment_currency | Codice ISO a 3 lettere, es. EUR. |
items | line_items, products, cart_items, lines | Articoli, vedi Articoli. |
coupon | coupon_code, discount_code, discount_codes[].code | Codice sconto applicato. |
status | state | abandoned (predefinito) oppure converted / completed se il carrello è diventato un ordine. Vale anche completed_at valorizzato. |
created_at | date_created | Data ISO 8601 o timestamp Unix. |
updated_at | abandoned_at, date_modified, last_activity | Ultima 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 }
]
}
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 consigliato | Alternative riconosciute | Note |
|---|---|---|
order_id | id | Id stabile dell'ordine (obbligatorio, altrimenti si usa il numero). |
order_number | number, name | Numero mostrato al cliente. |
cart_id | checkout_token, cart_token, checkout_id, cart_hash | Carrello da cui nasce l'ordine. |
status | order_status, state | pending, processing / paid, completed / shipped / delivered, cancelled / failed, refunded. Riconosce anche gli stati di Shopify (financial_status, fulfillment_status, cancelled_at) e di WooCommerce. |
payment_status | financial_status | Es. paid, pending. |
total | total_price, grand_total, amount | Importo in unità. |
subtotal, shipping_total, discount_total, tax_total | subtotal_price, shipping, total_discounts, total_tax | Facoltativi. |
currency | currency_code | Codice ISO, es. EUR. |
payment_method | payment_method_title, gateway | Facoltativo. |
coupon | discount_codes[].code, coupon_lines[].code | Facoltativo. |
shipping_address | shipping, billing_address, billing | Del CRM usa solo città, CAP (zip / postcode), provincia (province_code / state) e nazione: via e civico non vengono salvati. |
created_at | date_created, processed_at, order_date | Data dell'ordine: la usano le automazioni «Nuovo ordine» e «Post-acquisto». |
paid_at | date_paid | Facoltativo. |
items | line_items, products, order_items | Articoli, 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
| Campo | Alternative |
|---|---|
sku | — (serve alla regola delle automazioni «Contiene il prodotto») |
name | title, product_name |
variant | variant_title, variation |
quantity | qty |
price | unit_price (prezzo unitario) |
total | line_total, line_price (se manca: prezzo × quantità) |
product_id | variant_id, id |
category | product_type |
image_url, product_url | image.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' } }
Risposte
Il body accettato è JSON (Content-Type: application/json) oppure form-encoded. La risposta è sempre JSON.
| HTTP | Body | Significato |
|---|---|---|
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:
- l'id di consegna della piattaforma:
X-Shopify-Webhook-IdoX-WC-Webhook-Delivery-ID; - il campo
event_id(oidempotency_key) del payload: se invii JSON tuoi, valorizzalo con un id unico per ogni modifica; - 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
- Nell'admin di Shopify apri Impostazioni → Notifiche → Webhook e premi Crea webhook.
- 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.
- 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
- In WordPress apri WooCommerce → Impostazioni → Avanzate → Webhook e premi Aggiungi webhook.
- 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. - 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.