Skip to content

Connect other software with API and webhooks

Receive a webhook for every Nanus event (invoice issued, delivery note, order, received invoice…), read the event log without missing any, and sync orders, documents, customers and items with an API key.

Where to find it
Settings → Integrations

Open in Nanus

What it is for

Integrations connect Nanus to an online shop, a CRM, a sales agent app or a custom archive. With a webhook Nanus notifies another program of each chosen event; with an API key the other program reads (and, if its role allows, writes) Nanus data.

  • Every event goes into the event log at the same moment the document is saved, with a sequence number per company: whoever reads the log gets them all, in order and with no gaps.
  • Events carry the full data: for an order, the customer with its code, the sales agent, the price list, the payment terms and every line with the item (code, variant, category, barcode), the quantities delivered and still to deliver, and the amounts.
  • Events are also created by what Nanus does on its own (conversions, recurring invoices, invoices received from SDI, online shop orders): the author is then Nanus.
  • The event log keeps events for 90 days; the webhook delivery log keeps deliveries for 30 days.

Integrations are managed by whoever can change the settings. Webhooks exist in the cloud and in the desktop app; API keys only in the cloud and only for the owner. The full technical description (OpenAPI 3.1) is at /v1/openapi.json on your Nanus, also linked from OpenAPI specification in the Webhooks section.

Add a webhook

  1. Open Settings → Integrations and the Webhooks section.
  2. Under Receiving address enter the URL of the program that will receive the notifications and, if you like, a Description.
  3. Under Events to send open the groups you need (Issued invoices, Delivery notes, Customer orders, Invoices received from SDI…) and tick single events, or Whole group. All and None tick or clear the whole catalogue.
  4. Tap Add webhook and copy the secret shown into the receiving program straight away: afterwards only its last digits are shown.
  5. In the menu next to the webhook pick an event (for example Order created) or leave Test message, then tap Test: Nanus sends a realistic example of that event and shows the response.
  • Edit reopens address, description and events; Save changes updates them without changing the secret.
  • Pause and Resume stop and restart deliveries.
  • New secret generates another one: the current one stops working immediately.
  • Delete removes the webhook.

Deliveries, retries and Send again

For each event Nanus sends a POST request with the event in JSON. A delivery succeeds when the address answers with a 2xx code within 10 seconds. The headers tell you what is arriving:

  • Nanus-Evento: the type, for example ordine.creato.
  • Nanus-Evento-Id and Nanus-Progressivo: the unique event id and its number in the log. An event may arrive more than once (after a network error or with Send again): use the id to drop duplicates.
  • Nanus-Consegna and Nanus-Tentativo: the delivery and the attempt number.
  • Nanus-Firma: sha256= followed by the HMAC-SHA256 signature of the body made with the secret.

If the address does not answer or returns an error, Nanus retries after 30 seconds, 2 minutes, 10 minutes, 1 hour, 3, 6, 12 and 24 hours. Attempts are stored in the log, so they resume even after the server or the desktop app restarts.

  • Deliveries opens the Delivery log (last 30 days): for each event the status (Delivered, Pending, Failed), the attempts, the HTTP code, the start of the response and the time of the next attempt.
  • Send again resends a finished delivery straight away, whether it succeeded or not.

After 10 failed deliveries in a row the webhook is disabled and the list shows the Disabled warning. Check the address and tap Resume: missed deliveries stay in the log and you can send them with Send again, or fetch them from /v1/eventi.

Verify the signature

Compute the HMAC-SHA256 of the body exactly as received (before parsing it as JSON) with the webhook secret and compare it with Nanus-Firma using a constant-time comparison. If it does not match, answer 401 and do not use the data.

Node.js:

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

const secret = process.env.NANUS_SECRET

createServer((req, res) => {
  const chunks = []
  req.on('data', (c) => chunks.push(c))
  req.on('end', () => {
    const body = Buffer.concat(chunks)
    const signature = Buffer.from(req.headers['nanus-firma'] ?? '')
    const expected = Buffer.from(
      'sha256=' + createHmac('sha256', secret).update(body).digest('hex'),
    )
    if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {
      return res.writeHead(401).end()
    }
    const event = JSON.parse(body)
    // event.id is unique: if you have already handled it, just answer 200
    console.log(event.progressivo, event.evento, event.oggetto.id)
    res.writeHead(200).end('ok')
  })
}).listen(3000)

Python (Flask):

import hashlib, hmac, os
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["NANUS_SECRET"].encode()

@app.post("/nanus")
def receive():
    body = request.get_data()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("Nanus-Firma", "")):
        abort(401)
    event = request.get_json()
    print(event["progressivo"], event["evento"], event["oggetto"]["id"])
    return "ok"

PHP:

<?php
$secret = getenv('NANUS_SECRET');
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$signature = $_SERVER['HTTP_NANUS_FIRMA'] ?? '';
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
error_log($event['progressivo'] . ' ' . $event['evento']);
http_response_code(200);

Event catalogue

Names have the form object.action and stay in Italian, as in the API. creato, modificato and eliminato (created, updated, deleted, or their feminine forms) exist for every object; a change that changes nothing creates no event.

  • Issued invoices (credit notes, professional fees and self-invoices too): fattura.creata, fattura.modificata, fattura.eliminata, fattura.inviataSdi (sent to SDI), fattura.esitoSdi (SDI outcome).
  • Delivery notes: ddt.creato, ddt.modificato, ddt.eliminato, ddt.fatturato (invoiced).
  • Customer orders: ordine.creato, ordine.modificato, ordine.eliminato, ordine.statoCambiato (status changed), ordine.lavorazioneCambiata (progress changed), ordine.evasoParzialmente (partly fulfilled), ordine.evaso (fulfilled), ordine.annullato (cancelled). ordine.statoCambiato is about the fulfilment status (stato, calculated from deliveries); ordine.lavorazioneCambiata about the progress chosen by the user (lavorazione: ricevuto received, confermato confirmed, inAttesaCompletamento awaiting completion, spedito shipped), also when Nanus sets spedito after the delivery note that delivers everything. Both arrive together with ordine.modificato.
  • Quotes: preventivo.creato, preventivo.modificato, preventivo.eliminato, preventivo.inviato (sent), preventivo.accettato (accepted), preventivo.rifiutato (declined), preventivo.convertito (turned into an order or document).
  • Supplier orders: ordineFornitore.creato, ordineFornitore.modificato, ordineFornitore.eliminato, ordineFornitore.ricevutoParzialmente (partly received), ordineFornitore.ricevuto (received), ordineFornitore.annullato (cancelled).
  • Goods receipts: arrivo.registrato (recorded), arrivo.modificato, arrivo.eliminato.
  • Invoices received from SDI: fatturaRicevuta.arrivata (arrived), fatturaRicevuta.registrata (recorded), fatturaRicevuta.scartata (discarded), fatturaRicevuta.eliminata.
  • Purchases: acquisto.registrato, acquisto.modificato, acquisto.eliminato; acquisto.ricevutoPec and acquisto.ricevuto arrive together with fatturaRicevuta.arrivata for programs that already use them.
  • Collections and payments: pagamento.registrato (recorded), pagamento.annullato (cancelled).
  • Due dates: scadenza.creata, scadenza.modificata, scadenza.eliminata, scadenza.saldata (settled).
  • RiBa and SDD batches: distinta.presentata (submitted), distinta.modificata, distinta.accreditata (credited), distinta.insoluto (unpaid item), distinta.eliminata.
  • Stock movements: movimento.creato, movimento.modificato, movimento.eliminato.
  • Customers and suppliers, items, price lists: anagrafica.creata, anagrafica.modificata, anagrafica.eliminata, articolo.creato, articolo.modificato, articolo.eliminato, listino.creato, listino.modificato, listino.eliminato.

Every event has the same envelope: id, progressivo (sequence number), creatoIl (timestamp), versione (currently 1), autore (utente for a person, chiave for an API key, nanus for Nanus itself), evento, oggetto (type and id) and dati, the full snapshot of the document after saving (before deletion for eliminato events). Amounts are strings with 2 decimals, dates use YYYY-MM-DD. Here is ordine.creato, cut down to one line:

{
  "id": "0192f2c4-5b1e-7a3d-9f60-2d4e8c1a7b90",
  "progressivo": 1842,
  "creatoIl": "2026-10-28T09:14:03.512Z",
  "versione": 1,
  "autore": { "tipo": "utente", "id": "u1", "nome": "Anna Verdi" },
  "evento": "ordine.creato",
  "oggetto": { "tipo": "ordine", "id": "0192f0a1-0000-7000-8000-000000100001" },
  "dati": {
    "id": "0192f0a1-0000-7000-8000-000000100001",
    "numero": "2026/158",
    "data": "2026-10-28",
    "stato": "daEvadere",
    "lavorazione": "confermato",
    "lavorazioneDal": "2026-10-28",
    "deposito": "principale",
    "cliente": {
      "id": "0192f0a1-0000-7000-8000-00000000c001",
      "codice": "C0042",
      "denominazione": "Officine Rossi S.r.l.",
      "partitaIva": "12345678903",
      "indirizzo": { "indirizzo": "Via Industria 12", "cap": "25121", "comune": "Brescia", "provincia": "BS", "nazione": "IT" }
    },
    "destinazione": { "id": "d1", "descrizione": "Magazzino di Rezzato", "indirizzo": { "indirizzo": "Via dei Mille 4", "cap": "25086", "comune": "Rezzato", "provincia": "BS", "nazione": "IT" } },
    "agente": { "id": "0192f0a1-0000-7000-8000-0000000a9e01", "codice": "AG03", "nome": "Marco Bianchi" },
    "listino": { "codice": "RIV", "descrizione": "Rivenditori" },
    "pagamento": { "codice": "RB60", "descrizione": "Ricevuta bancaria 60 gg DF FM", "modalita": "MP12" },
    "consegnaPrevista": "2026-11-12",
    "riferimentoCliente": "ODA 4471",
    "totali": { "imponibile": "666.00", "imposta": "146.52", "totale": "812.52" },
    "ddt": [],
    "fatture": [],
    "righe": [
      {
        "numero": 1,
        "articolo": {
          "codice": "FL-120",
          "descrizione": "Flangia in acciaio DN120",
          "variante": { "codice": "ZN", "descrizione": "Zincata" },
          "categoria": "Flange",
          "codiceBarre": "8001234567901"
        },
        "descrizione": "Flangia in acciaio DN120 zincata",
        "unitaMisura": "PZ",
        "quantita": "40",
        "quantitaEvasa": "0",
        "quantitaResidua": "40",
        "prezzoUnitario": "18.50",
        "sconti": [{ "tipo": "SC", "percentuale": "10", "importo": null }],
        "prezzoNetto": "16.65",
        "importo": "666.00",
        "aliquotaIva": "22"
      }
    ]
  }
}

A complete, realistic example of every event in the catalogue is available from GET /v1/eventi/esempi/<event> (for example /v1/eventi/esempi/fatturaRicevuta.arrivata) and in the webhooks section of the OpenAPI specification. Delivery notes, invoices and received invoices have the same level of detail as orders; fields without a value are null.

Read the event log

With an API key you read events in sequence order with GET /v1/eventi?dopo=<sequence number>&tipi=<comma-separated events>&limite=<up to 1000>. The response contains dati (the events), prossimo (the value to pass as dopo on the next call), ultimo (the last sequence number written) and incompleto.

  1. The first time, call /v1/eventi?dopo=0.
  2. Handle the events received and store prossimo as your bookmark.
  3. Call again with dopo set to the bookmark: when prossimo equals ultimo you are up to date, so wait a little before asking again.

The log never skips a number and never returns events that are still being written: a program that follows prossimo misses nothing. If the bookmark is older than 90 days incompleto is true: run a full sync with the APIs below. A key only sees events from the areas its role can read.

Sync the data

For the first copy of the data, or to check it again, use GET /v1/sincronizzazione/<resource>: the resources are ordini (orders), preventivi (quotes), ddt (delivery notes), fatture (invoices), ricevute (invoices received from SDI), acquisti (purchases), anagrafiche (customers and suppliers) and articoli (items). Each record has the same shape as the event dati; GET /v1/sincronizzazione/<resource>/<id> reads one, with the time of its last change.

  • modificatoDal: only records changed since that moment (ISO date and time, for example 2026-10-01T00:00:00Z).
  • dal and al: document date (YYYY-MM-DD).
  • stato: for example daEvadere, parziale, evaso for orders.
  • lavorazione: orders only, ricevuto, confermato, inAttesaCompletamento or spedito (for example /v1/sincronizzazione/ordini?lavorazione=confermato).
  • cliente and agente: id or code, for example cliente=C0042 or agente=AG03.
  • limite (up to 500) and dopo: ask for the next page by passing the response’s prossimo value as dopo, until it is null.

Example: order history line by line

An external program wants a table with one row for each order line: order number and date, customer, sales agent, item, quantity ordered and delivered, amount. It keeps it up to date like this:

  1. In API keys create a key with the Read only role: it reads everything and cannot change anything.
  2. Read ultimo from /v1/eventi?limite=1 and keep it as your bookmark.
  3. Download every order from /v1/sincronizzazione/ordini, page by page, and write a table row for each order line.
  4. From then on read /v1/eventi from the bookmark with order events only: for each event replace that order’s rows with the new ones (delete them for ordine.eliminato).
  5. Instead of step 4 you can add a webhook with the Customer orders group: you get the same events as soon as they happen. Keep the bookmark anyway, to catch up on events missed while the program was off.
// Node.js 18 or later: NANUS_API = API address, NANUS_KEY = key
const get = (path) =>
  fetch(process.env.NANUS_API + path, {
    headers: { authorization: 'Bearer ' + process.env.NANUS_KEY },
  }).then((r) => r.json())

const history = new Map() // order id → table rows
const update = (o) =>
  history.set(o.id, o.righe.map((r) => ({
    order: o.numero, date: o.data, status: o.stato,
    customer: o.cliente.codice, agent: o.agente?.codice ?? null,
    line: r.numero, item: r.articolo?.codice ?? null,
    description: r.descrizione, quantity: r.quantita,
    delivered: r.quantitaEvasa, amount: r.importo,
  })))

let { ultimo: bookmark } = await get('/eventi?limite=1')
let page = ''
do {
  const r = await get('/sincronizzazione/ordini?limite=500&dopo=' + page)
  r.dati.forEach(update)
  page = r.prossimo
} while (page)

const types = ['creato', 'modificato', 'statoCambiato', 'evasoParzialmente',
  'evaso', 'annullato', 'eliminato'].map((a) => 'ordine.' + a).join(',')
for (;;) {
  const r = await get('/eventi?limite=500&tipi=' + types + '&dopo=' + bookmark)
  for (const e of r.dati) {
    if (e.evento === 'ordine.eliminato') history.delete(e.oggetto.id)
    else update(e.dati)
  }
  bookmark = r.prossimo // store it: on restart, resume from here
  if (bookmark === r.ultimo) await new Promise((ok) => setTimeout(ok, 60_000))
}

Create an API key (cloud)

  1. In the API keys section enter a Name (to remember who uses it).
  2. Choose the Role: the key works with that role’s permissions (Collaborator, Accounting, Sales, Warehouse or Read only). For a program that only reads, choose Read only.
  3. Tap Create key and copy the key straight away: for security it cannot be shown again.
  4. In the external program use the API address given in the section note, with the header Authorization: Bearer followed by the key.

A key cannot manage team members or other keys. The list shows the start of each key and the date it was last used; Revoke disables it immediately.

What Nanus checks

  • The webhook address must be an http or https URL.
  • In the cloud the webhook must use https to a public address: no localhost, IP addresses or internal networks. In the desktop app you can also point to a program on your own network.
  • A key only reads events and data from the areas its role allows: for example a Warehouse key does not see collections and due dates.

Frequently asked questions

I lost an API key, can I see it again?

No. Revoke it and create another one, then update the program that used it.

Are actions made with a key recorded?

Yes: they appear in the activity log like those of people, and in events the author is the key.

Can I receive the same event twice?

Yes, for example after a network error or with Send again. The Nanus-Evento-Id header (and the id field) is always the same: if you have already handled it, answer 200 and ignore it.

The receiving program was off for a day: do I lose events?

No. Nanus retries for about two days and deliveries stay in the log for 30 days; events can be read from /v1/eventi for 90 days.

See also