API Reference / Record generici

Record generici

Un unico set di endpoint, /api/records/:moduleKey, per leggere e scrivere i record di qualsiasi modulo custom creato con le API dei moduli. Stessa grammatica dei moduli base, ma parametrizzata sulla chiave del modulo.

Prima di iniziare

Envelope, errori, paginazione e advanced_filters sono comuni: vedi Introduzione alle API. I permessi (azione / scope / campo) sono valutati sul modulo indicato nell'URL. Ogni richiesta richiede Authorization: Bearer <accessToken>.

01Il record custom

Un record custom è un oggetto dinamico: i suoi attributi sono i campi definiti dal modulo (proprietà piatte, chiave = field_key) più alcuni campi di sistema comuni a tutti i moduli. Leggi il descrittore del modulo per conoscere chiavi e tipi. I valori seguono il tipo del campo: testo, numero, data ISO 8601, booleano, id per i lookup.

AttributoTipoDescrizione
idintegerIdentificativo del record.
<field_key>anyUn attributo per ogni campo del modulo (es. titolo, priorita). Il tipo dipende dal campo.
owner_user_idinteger | nullUtente proprietario. In scrittura è assegnabile solo a sé o a un sottoposto nella gerarchia dei ruoli (admin: chiunque). Vedi Utenti e team.
assigned_user_idinteger | nullUtente assegnatario.
is_deletedbooleantrue se in cestino (soft delete).
created_atstringData/ora di creazione (ISO 8601).
updated_atstringData/ora ultima modifica (ISO 8601).
json · un record del modulo "tickets"
{
  "id": 51,
  "titolo": "Errore login",
  "priorita": "alta",
  "account_ref": 128,
  "owner_user_id": 7,
  "assigned_user_id": 7,
  "is_deleted": false,
  "created_at": "2026-07-07T15:24:00.000Z",
  "updated_at": "2026-07-07T15:24:00.000Z"
}

02Scoprire i moduli

Prima di operare su un modulo puoi leggerne la descrizione (campi, capacità supportate) senza conoscerla in anticipo:

MetodoPathCosa fa
GET/api/record-modulesElenco dei moduli-record visibili all'utente, con le capacità.
GET/api/record-modules/:moduleKeyDescrittore di un modulo: campi, relazioni, operazioni ammesse.

Il descrittore è metadato di sola struttura (mai dati dei record). Ha questa forma:

AttributoTipoDescrizione
keystringChiave del modulo.
kindstringcore (modulo base) o custom.
labelsobject{ singular, plural }.
columnsstring[]Colonne esposte: id, i field_key del modulo, i campi di sistema.
relationsarrayRelazioni lookup in uscita: { column, targetModule, label }.
capabilitiesstring[]Operazioni supportate. Per i custom: create, read, update, delete, export, import, bulkUpdate, history.
json · GET /api/record-modules/tickets
{
  "success": true,
  "data": {
    "descriptor": {
      "key": "tickets",
      "kind": "custom",
      "labels": { "singular": "Ticket", "plural": "Ticket" },
      "columns": ["id", "titolo", "priorita", "account_ref", "owner_user_id", ],
      "relations": [ { "column": "account_ref", "targetModule": "accounts", "label": "Account" } ],
      "capabilities": ["create", "read", "update", "delete", "export", "import", "bulkUpdate", "history"]
    }
  }
}

03CRUD sui record

MetodoPathAzioneCosa fa
GET/api/records/:moduleKeyreadElenco paginato e filtrabile.
GET/api/records/:moduleKey/:idreadUn singolo record.
GET/api/records/:moduleKey/:id/historyreadStorico modifiche.
POST/api/records/:moduleKeycreateCrea un record.
PATCH/api/records/:moduleKey/:idupdateModifica parziale.
PATCH/api/records/:moduleKey/bulkupdateAggiornamento massivo.
DELETE/api/records/:moduleKey/:iddeleteSoft delete.
GET/api/records/:moduleKey/export.csv · .xlsxexportEsporta l'elenco filtrato.
POST/api/records/:moduleKey/import/preview · /executecreateAnteprima e import CSV.
GET/api/records/:moduleKey/lookupreadType-ahead per i campi lookup che puntano a questo modulo.
Campi lookup nelle letture

Nelle risposte di lista e dettaglio, ogni campo lookup valorizzato è accompagnato da <field_key>_label con l'etichetta leggibile del record collegato (es. account_ref: 128 + account_ref_label: "ACME S.r.l."). In scrittura si passa solo l'id; un id inesistente sul modulo target → 422.

GET /api/records/:moduleKey/lookup bearer richiesto

Ricerca type-ahead per il picker dei campi lookup che puntano a questo modulo: query q (contains sul campo display) e limit (default 20, max 50). Scope-aware: propone solo record che il chiamante può leggere. Risponde { "items": [{ "id", "label" }] }.

Nota

Questi endpoint servono solo i moduli custom: passando la chiave di un modulo base (es. accounts) rispondono 400 (Questo non è un modulo personalizzato); una chiave inesistente dà 404. Per i moduli base usa le loro rotte dedicate (Account, Contatti…).

04Elencare i record

GET /api/records/:moduleKey bearer richiesto

Restituisce i record del modulo visibili all'utente (secondo lo scope del ruolo), paginati.

Parametri path
ParametroTipoDescrizione
moduleKeystringChiave del modulo custom (es. tickets).
Parametri query
ParametroTipoDescrizione
limitintegeropzionaleRighe per pagina. Default 50, max 200.
offsetintegeropzionaleRighe da saltare (default 0).
sort_bystringopzionaleColonna di ordinamento (un campo del modulo o created_at).
sort_dirstringopzionaleasc o desc.
advanced_filtersstring (JSON)opzionaleArray di condizioni sulle colonne del catalogo: vedi Filtri avanzati.
Richiesta
curl "https://crm.tuodominio.it/api/records/tickets?limit=2&sort_by=created_at&sort_dir=desc" \
  -H "Authorization: Bearer <accessToken>"
const res = await fetch("https://crm.tuodominio.it/api/records/tickets?limit=2&sort_by=created_at&sort_dir=desc", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const { data } = await res.json();
Risposta · 200
json
{
  "success": true,
  "data": {
    "items": [ { "id": 51, "titolo": "Errore login",  }, { "id": 50,  } ]
  }
}
Esiti
StatoCorpo
200{ success: true, data: { items: [ … ] } }
400La chiave è di un modulo base (non personalizzato).
404NOT_FOUND: modulo inesistente.

05Creare un record

POST /api/records/:moduleKey bearer richiesto

Il body è un oggetto piatto: una proprietà per ogni campo del modulo che vuoi valorizzare. La validazione è guidata dalla definizione: chiavi sconosciute, valori fuori formato o campi obbligatori mancanti danno 422. I campi non scrivibili dal tuo ruolo vengono ignorati (filtrati dal payload).

Richiesta
curl -X POST https://crm.tuodominio.it/api/records/tickets \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{ "titolo": "Errore login", "priorita": "alta", "account_ref": 128 }'
const res = await fetch("https://crm.tuodominio.it/api/records/tickets", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ titolo: "Errore login", priorita: "alta", account_ref: 128 }),
});
const { data } = await res.json();
Risposta · 201
json
{ "success": true, "data": { "item": { "id": 51, "titolo": "Errore login", "priorita": "alta",  } } }
Esiti
StatoCodiceQuando
201·Record creato; nel corpo data.item.
422VALIDATION_ERRORChiave sconosciuta, valore fuori formato, campo obbligatorio mancante o lookup inesistente.
400·La chiave è di un modulo base.
403FORBIDDENNessun permesso create sul modulo.

06Creare più record in una richiesta

Quando devi caricare molti record (una migrazione, un import periodico, una sincronizzazione da un altro sistema) una richiesta HTTP per record è lo spreco più grosso: autenticazione, permessi e transazione si pagano ogni volta. Questo endpoint ne accetta fino a 100 e li scrive in una sola transazione.

Quanto rende

Misurato su hardware di sviluppo: da 4.100 a oltre 9.000 record al secondo, con il costo per record che scende da 1.385 a 478 microsecondi di CPU. Il limite di 100 non è arbitrario: oltre quella soglia il guadagno si ferma al 2% e la transazione si allunga.

POST /api/records/:moduleKey/batch bearer richiesto

Il body è un oggetto con la proprietà records: un elenco di record nella stessa forma della creazione singola. Disponibile anche sugli alias dei moduli base (/api/leads/batch, /api/accounts/batch, e così via).

Parametri body
ParametroTipoDescrizione
recordsarrayDa 1 a 100 record. Ogni elemento è un oggetto piatto, identico al body della creazione singola.
Richiesta
curl -X POST https://crm.tuodominio.it/api/records/tickets/batch \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{ "records": [ { "titolo": "Errore login", "priorita": "alta" }, { "titolo": "Stampante offline", "priorita": "media" } ] }'
// A lotti di 100: l'endpoint rifiuta le richieste piu' grandi.
for (let i = 0; i < tutti.length; i += 100) {
  const res = await fetch("https://crm.tuodominio.it/api/records/tickets/batch", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ records: tutti.slice(i, i + 100) }),
  });
  const { data } = await res.json();
  // Va sempre letto: 201 non significa "tutti".
  if (data.failedCount > 0) console.warn(data.errors);
}
Risposta · 201
json · referto del lotto
{
  "success": true,
  "data": {
    "requestedCount": 3,
    "createdCount": 2,
    "failedCount": 1,
    "records": [ { "id": 51, "titolo": "Errore login",  } ],
    "errors": [
      { "index": 1, "code": "BAD_REQUEST", "message": "Campo non riconosciuto per questo modulo: colore" }
    ]
  }
}
Il referto
CampoTipoDescrizione
requestedCountintegerQuanti record erano nella richiesta.
createdCountintegerQuanti sono stati creati davvero.
failedCountintegerQuanti sono stati scartati in validazione.
recordsarrayI record creati, nella stessa forma della lettura singola.
errorsarrayUn elemento per record scartato: index (la posizione nella richiesta), code e message.
Esiti
StatoCodiceQuando
201·Almeno un record è stato creato. Non significa "tutti": leggi failedCount.
422VALIDATION_ERRORNessun record valido, elenco vuoto, oppure più di 100 record.
403FORBIDDENNessun permesso create sul modulo.
404NOT_FOUNDModulo inesistente.
413PAYLOAD_TOO_LARGECorpo oltre 2 MB.

Cosa garantisce, e cosa no

La distinzione conta quando qualcosa va storto a metà di un import.

AspettoComportamento
ValidazionePer record. Un record sbagliato viene riportato con il suo indice e saltato: gli altri passano. È la semantica che serve a un import, dove su diecimila righe qualcuna è sempre storta.
ScritturaUna transazione sola. Se fallisce quella (un vincolo del database, non un dato sbagliato), non viene creato niente: non esiste un mezzo lotto.
OrdineI record in records tornano nello stesso ordine in cui sono stati inviati, saltati esclusi. L'indice negli errori si riferisce sempre alla richiesta, non alla risposta.
AuditOgni record creato ha la sua riga nell'audit trail, non una cumulativa per il lotto.
ProprietarioCome nella creazione singola: owner_user_id e assigned_user_id non si impostano dal payload, li decide la gerarchia dei ruoli. Valgono per tutti i record del lotto.
Numerazione automaticaI campi autonumber ricevono valori consecutivi riservati in blocco: nessun buco e nessuna collisione anche con più lotti in parallelo.
AutomazioniI workflow sull'evento di creazione scattano per ogni record del lotto, non una volta per richiesta.
Consiglio pratico

Per un caricamento massivo il punto migliore misurato è lotti da 100 con poche richieste in volo (sei in parallelo). Alzare le richieste contemporanee non aumenta la velocità: aggiunge solo coda e allunga i tempi di risposta.

07Filtri ed export

La lista accetta gli stessi advanced_filters dei moduli base, sulle colonne del catalogo del modulo (vedi Filtri avanzati). Export (GET …/export.csv · .xlsx) e import CSV (POST …/import/preview · /execute) funzionano allo stesso modo dei moduli base, rispettando i permessi di campo (le colonne non leggibili vengono escluse dall'export; l'import rifiuta i campi non scrivibili).