API e webhook

Per collegare V-Engyne al programma di contabilità, a un sito, a un gestionale o a un'automazione.

Come si comincia

L'API è in JSON, all'indirizzo https://v-engyne.com/api/v1. Il titolare dello studio crea una chiave in Account, Integrazioni: solo lettura, oppure anche scrittura. La chiave si vede una volta sola; chi la perde ne crea un'altra, e revocandola smette di funzionare subito. Ogni chiave vede soltanto i dati del suo studio.

Ogni chiamata porta la chiave nell'intestazione Authorization: Bearer vk_…. Al massimo 300 chiamate al minuto per chiave (poi risponde 429). Chi prova chiavi sbagliate a raffica viene fermato per un'ora. La chiave è un segreto: va solo nei programmi sul server, mai in una pagina web o in un'app che gli altri possono aprire.

Leggere i dati

IndirizzoCosa restituisce
GET /api/v1/commesseCommesse
GET /api/v1/clientiClienti
GET /api/v1/fattureFatture emesse (senza il file XML) · filtro modificato_dopo
GET /api/v1/fatture-ricevuteFatture dei fornitori · filtro modificato_dopo
GET /api/v1/oreOre registrate · filtro modificato_dopo
GET /api/v1/speseNote spese (senza la foto della ricevuta)
GET /api/v1/opportunitaOpportunità commerciali · filtro modificato_dopo
GET /api/v1/preventiviPreventivi · filtro modificato_dopo
GET /api/v1/<risorsa>/<id>Una riga sola (le fatture con le righe del documento)
GET /api/v1/contabilitaI registri per il commercialista di un periodo

Gli elenchi vanno a pagine: limite (da 1 a 200, di base 50) e cursore, che è il prossimo_cursore dell'ultima risposta (quando è null sei in fondo). Dove c'è modificato_dopo (data e ora ISO) ottieni solo ciò che è cambiato da allora: così si tiene allineato un altro programma senza rileggere tutto.

curl "https://v-engyne.com/api/v1/fatture?limite=2&modificato_dopo=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer vk_LA_TUA_CHIAVE"
{
  "dati": [
    { "id": "5c1f…", "numero_fattura": "12/2026", "data_emissione": "2026-10-03",
      "totale_documento": 1220, "stato_pagamento": "incassata", "…": "…" },
    { "id": "9a42…", "numero_fattura": "13/2026", "…": "…" }
  ],
  "prossimo_cursore": "9a42…"
}

Gli errori hanno sempre la stessa forma, { "errore": { "codice": "…", "messaggio": "…" } }, con lo stato HTTP giusto: 401 chiave non valida, 403 chiave di sola lettura o abbonamento non attivo, 404 non trovato, 422 dati non validi, 429 troppe chiamate.

I limiti, per ogni chiave: 300 chiamate al minuto, di cui al massimo 30 scritture al minuto e 500 al giorno, e i registri per il commercialista 10 volte al minuto. Dopo 30 chiavi sbagliate in un'ora dalla stessa rete, quella rete aspetta un'ora. Oltre un limite la risposta è 429, con l'intestazione Retry-After che dice fra quanti secondi riprovare.

Creare un'opportunità

Con una chiave che può scrivere, POST /api/v1/opportunita crea un'opportunità da un altro programma (il modulo di un altro sito, un gestionale, un'automazione). Serve titolo e almeno referente o azienda; si possono aggiungere tipo ("incarico" o "preventivo"), email, telefono, valore in euro e descrizione. L'opportunità arriva con origine "API" e gli admin ricevono una notifica.

curl -X POST "https://v-engyne.com/api/v1/opportunita" \
  -H "Authorization: Bearer vk_LA_TUA_CHIAVE" \
  -H "Content-Type: application/json" \
  -d '{ "titolo": "Ristrutturazione villa", "tipo": "preventivo",
        "referente": "Mario Rossi", "email": "mario@esempio.it",
        "telefono": "333 1234567", "valore": 18000 }'

Registri per il commercialista

GET /api/v1/contabilita?da=AAAA-MM-GG&a=AAAA-MM-GG restituisce in JSON i registri del periodo: fatture emesse, fatture ricevute, incassi, costi di commessa, note spese e IVA mese per mese. Con registro= (fatture_emesse, fatture_ricevute, incassi, costi_commessa, note_spese, iva_mensile) e formato=csv scarichi un file per Excel. Il periodo è al massimo di 3 anni. Banca e compensi non sono inclusi.

curl "https://v-engyne.com/api/v1/contabilita?da=2026-07-01&a=2026-09-30&registro=fatture_emesse&formato=csv" \
  -H "Authorization: Bearer vk_LA_TUA_CHIAVE" -o fatture-emesse.csv

Webhook: essere avvisati

Un webhook è un indirizzo https del tuo programma che V-Engyne chiama quando succede qualcosa. Il titolare lo aggiunge in Account, Integrazioni, e sceglie gli eventi (o tutti). La chiamata è un POST con un contenuto corto: l'evento e gli identificativi, il resto si legge dall'API.

EventoQuando
commessa.creataNasce una commessa
commessa.stato_cambiatoUna commessa cambia stato
fattura.creataSi crea una fattura
fattura.emessaUna fattura prende il numero
fattura.pagataUna fattura risulta incassata
opportunita.creataArriva un'opportunità
opportunita.stato_cambiatoUn'opportunità cambia stato
preventivo.stato_cambiatoUn preventivo cambia stato
spesa.stato_cambiatoUna nota spese cambia stato
richiesta.ricevutaArriva una richiesta dal modulo del sito
contratto.firma_cambiataCambia lo stato di firma di un contratto
POST https://esempio.it/webhook/v-engyne
Content-Type: application/json
V-Engyne-Evento: fattura.pagata
V-Engyne-Consegna: 3f0c…
V-Engyne-Firma: t=1761300000,v1=9d2b…

{
  "id": "e1c7…",
  "tipo": "fattura.pagata",
  "creato": "2026-10-24T09:00:00.000Z",
  "dati": { "id": "5c1f…", "project_id": "77a0…", "numero_fattura": "12/2026" }
}

Rispondi con uno stato 2xx entro 8 secondi: la chiamata è consegnata. Altrimenti V-Engyne riprova, fino a 8 volte, dopo 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h. La stessa chiamata può arrivare più di una volta: l'id nel contenuto (e V-Engyne-Consegna) serve a riconoscerla. Gli indirizzi devono essere raggiungibili da internet: V-Engyne non chiama reti private e non segue i reindirizzamenti.

Controllare la firma

Ogni chiamata ha l'intestazione V-Engyne-Firma: t=<secondi>,v1=<firma>. La firma è l'HMAC SHA-256 di t, un punto e il contenuto così com'è arrivato, calcolato con il segreto del webhook. Controllala prima di fidarti della chiamata, e scarta quelle con t di più di 5 minuti fa.

import { createHmac, timingSafeEqual } from 'node:crypto'

// segreto: quello mostrato quando hai creato il webhook; corpo: il testo
// ricevuto, prima di leggerlo come JSON
function firmaValida(segreto, intestazione, corpo) {
  const t = /t=(\d+)/.exec(intestazione)?.[1]
  const v1 = /v1=([0-9a-f]{64})/.exec(intestazione)?.[1]
  if (!t || !v1) return false
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false // oltre 5 minuti
  const atteso = createHmac('sha256', segreto).update(t + '.' + corpo).digest()
  return timingSafeEqual(atteso, Buffer.from(v1, 'hex'))
}