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.
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.
| Attributo | Tipo | Descrizione |
|---|---|---|
| id | integer | Identificativo del record. |
| <field_key> | any | Un attributo per ogni campo del modulo (es. titolo, priorita). Il tipo dipende dal campo. |
| owner_user_id | integer | null | Utente proprietario. In scrittura è assegnabile solo a sé o a un sottoposto nella gerarchia dei ruoli (admin: chiunque). Vedi Utenti e team. |
| assigned_user_id | integer | null | Utente assegnatario. |
| is_deleted | boolean | true se in cestino (soft delete). |
| created_at | string | Data/ora di creazione (ISO 8601). |
| updated_at | string | Data/ora ultima modifica (ISO 8601). |
{ "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:
| Metodo | Path | Cosa fa |
|---|---|---|
| GET | /api/record-modules | Elenco dei moduli-record visibili all'utente, con le capacità. |
| GET | /api/record-modules/:moduleKey | Descrittore di un modulo: campi, relazioni, operazioni ammesse. |
Il descrittore è metadato di sola struttura (mai dati dei record). Ha questa forma:
| Attributo | Tipo | Descrizione |
|---|---|---|
| key | string | Chiave del modulo. |
| kind | string | core (modulo base) o custom. |
| labels | object | { singular, plural }. |
| columns | string[] | Colonne esposte: id, i field_key del modulo, i campi di sistema. |
| relations | array | Relazioni lookup in uscita: { column, targetModule, label }. |
| capabilities | string[] | Operazioni supportate. Per i custom: create, read, update, delete, export, import, bulkUpdate, history. |
{ "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
| Metodo | Path | Azione | Cosa fa |
|---|---|---|---|
| GET | /api/records/:moduleKey | read | Elenco paginato e filtrabile. |
| GET | /api/records/:moduleKey/:id | read | Un singolo record. |
| GET | /api/records/:moduleKey/:id/history | read | Storico modifiche. |
| POST | /api/records/:moduleKey | create | Crea un record. |
| PATCH | /api/records/:moduleKey/:id | update | Modifica parziale. |
| PATCH | /api/records/:moduleKey/bulk | update | Aggiornamento massivo. |
| DELETE | /api/records/:moduleKey/:id | delete | Soft delete. |
| GET | /api/records/:moduleKey/export.csv · .xlsx | export | Esporta l'elenco filtrato. |
| POST | /api/records/:moduleKey/import/preview · /execute | create | Anteprima e import CSV. |
| GET | /api/records/:moduleKey/lookup | read | Type-ahead per i campi lookup che puntano a questo modulo. |
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.
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" }] }.
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
Restituisce i record del modulo visibili all'utente (secondo lo scope del ruolo), paginati.
Parametri path| Parametro | Tipo | Descrizione |
|---|---|---|
| moduleKey | string | Chiave del modulo custom (es. tickets). |
| Parametro | Tipo | Descrizione | |
|---|---|---|---|
| limit | integer | opzionale | Righe per pagina. Default 50, max 200. |
| offset | integer | opzionale | Righe da saltare (default 0). |
| sort_by | string | opzionale | Colonna di ordinamento (un campo del modulo o created_at). |
| sort_dir | string | opzionale | asc o desc. |
| advanced_filters | string (JSON) | opzionale | Array di condizioni sulle colonne del catalogo: vedi Filtri avanzati. |
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();
{ "success": true, "data": { "items": [ { "id": 51, "titolo": "Errore login", … }, { "id": 50, … } ] } }
| Stato | Corpo |
|---|---|
| 200 | { success: true, data: { items: [ … ] } } |
| 400 | La chiave è di un modulo base (non personalizzato). |
| 404 | NOT_FOUND: modulo inesistente. |
05Creare un record
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).
Richiestacurl -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();
{ "success": true, "data": { "item": { "id": 51, "titolo": "Errore login", "priorita": "alta", … } } }
| Stato | Codice | Quando |
|---|---|---|
| 201 | · | Record creato; nel corpo data.item. |
| 422 | VALIDATION_ERROR | Chiave sconosciuta, valore fuori formato, campo obbligatorio mancante o lookup inesistente. |
| 400 | · | La chiave è di un modulo base. |
| 403 | FORBIDDEN | Nessun 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.
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.
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).
| Parametro | Tipo | Descrizione |
|---|---|---|
| records | array | Da 1 a 100 record. Ogni elemento è un oggetto piatto, identico al body della creazione singola. |
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); }
{ "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" } ] } }
| Campo | Tipo | Descrizione |
|---|---|---|
| requestedCount | integer | Quanti record erano nella richiesta. |
| createdCount | integer | Quanti sono stati creati davvero. |
| failedCount | integer | Quanti sono stati scartati in validazione. |
| records | array | I record creati, nella stessa forma della lettura singola. |
| errors | array | Un elemento per record scartato: index (la posizione nella richiesta), code e message. |
| Stato | Codice | Quando |
|---|---|---|
| 201 | · | Almeno un record è stato creato. Non significa "tutti": leggi failedCount. |
| 422 | VALIDATION_ERROR | Nessun record valido, elenco vuoto, oppure più di 100 record. |
| 403 | FORBIDDEN | Nessun permesso create sul modulo. |
| 404 | NOT_FOUND | Modulo inesistente. |
| 413 | PAYLOAD_TOO_LARGE | Corpo oltre 2 MB. |
Cosa garantisce, e cosa no
La distinzione conta quando qualcosa va storto a metà di un import.
| Aspetto | Comportamento |
|---|---|
| Validazione | Per 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. |
| Scrittura | Una transazione sola. Se fallisce quella (un vincolo del database, non un dato sbagliato), non viene creato niente: non esiste un mezzo lotto. |
| Ordine | I 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. |
| Audit | Ogni record creato ha la sua riga nell'audit trail, non una cumulativa per il lotto. |
| Proprietario | Come 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 automatica | I campi autonumber ricevono valori consecutivi riservati in blocco: nessun buco e nessuna collisione anche con più lotti in parallelo. |
| Automazioni | I workflow sull'evento di creazione scattano per ogni record del lotto, non una volta per richiesta. |
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).