Note e allegati
Note e allegati non sono moduli a sé: si agganciano a un record (di
qualsiasi modulo) tramite la coppia related_module +
related_record_id. Le note sono testo; gli allegati sono file.
Envelope, errori e paginazione sono comuni: vedi Introduzione alle API.
Ogni richiesta richiede Authorization: Bearer <accessToken>.
01L'oggetto Nota
Ogni nota restituita dall'API ha questa forma. Le date sono stringhe
ISO 8601 in UTC. La coppia related_module +
related_record_id è obbligatoria: una nota esiste sempre agganciata
a un record.
| Attributo | Tipo | Descrizione |
|---|---|---|
| id | integer | Identificativo univoco della nota. |
| content | string | Testo della nota. |
| related_module | string | Modulo del record collegato (moduli base o chiave di un modulo custom). |
| related_record_id | integer | Id del record collegato. |
| owner_user_id | integer | null | Utente proprietario del record. |
| created_by | integer | null | Utente che ha creato la nota. |
| updated_by | integer | null | Ultimo utente che l'ha modificata. |
| 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": 4021, "content": "Cliente interessato al rinnovo", "related_module": "accounts", "related_record_id": 128, "owner_user_id": 7, "created_by": 7, "updated_by": 7, "is_deleted": false, "created_at": "2026-07-07T15:24:00.000Z", "updated_at": "2026-07-07T15:24:00.000Z" }
02Endpoint · Note
| Metodo | Path | Azione | Cosa fa |
|---|---|---|---|
| GET | /api/notes | read | Elenco (filtrabile per record collegato). |
| GET | /api/notes/:id | read | Una singola nota. |
| POST | /api/notes | create | Crea una nota su un record. |
| PATCH | /api/notes/:id | update | Modifica il testo. |
| DELETE | /api/notes/:id | delete | Soft delete. |
| GET | /api/notes/export.csv · .xlsx | export | Esporta l'elenco filtrato. |
03Elencare le note
Restituisce le note visibili all'utente, paginate. Per le note di un record filtra su
related_module e related_record_id
con advanced_filters.
| 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 (es. created_at). |
| sort_dir | string | opzionale | asc o desc. |
| advanced_filters | string (JSON) | opzionale | Array di condizioni: vedi Filtri avanzati. |
curl "https://crm.tuodominio.it/api/notes?advanced_filters=%5B%7B%22field%22%3A%22related_module%22%2C%22op%22%3A%22eq%22%2C%22value%22%3A%22accounts%22%7D%5D" \ -H "Authorization: Bearer <accessToken>"
const filters = JSON.stringify([{ field: "related_module", op: "eq", value: "accounts" }]); const url = `https://crm.tuodominio.it/api/notes?advanced_filters=${encodeURIComponent(filters)}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }); const { data } = await res.json();
{ "success": true, "data": { "items": [ { "id": 4021, "content": "Cliente interessato al rinnovo", … } ], "pagination": { "limit": 50, "offset": 0, "count": 1 } } }
04Creare una nota
| Campo | Tipo | Note | |
|---|---|---|---|
| content | string | richiesto | Testo della nota. 1–10.000 caratteri. |
| related_module | string | richiesto | Modulo del record collegato (moduli base o chiave di un modulo custom). |
| related_record_id | integer | richiesto | Id del record collegato. Va insieme a related_module. |
| owner_user_id | integer | opzionale | Proprietario. Assegnabile solo a sé o a un sottoposto nella gerarchia dei ruoli (admin: chiunque). Vedi Utenti e team. |
curl -X POST https://crm.tuodominio.it/api/notes \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "content": "Cliente interessato al rinnovo", "related_module": "accounts", "related_record_id": 128 }'
const res = await fetch("https://crm.tuodominio.it/api/notes", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ content: "Cliente interessato al rinnovo", related_module: "accounts", related_record_id: 128 }), }); const { data } = await res.json();
{ "success": true, "data": { "id": 4021, "content": "Cliente interessato al rinnovo", "related_module": "accounts", "related_record_id": 128, … } }
| Stato | Codice | Quando |
|---|---|---|
| 201 | · | Nota creata; nel corpo l'oggetto completo. |
| 422 | VALIDATION_ERROR | content, related_module o related_record_id mancante o non valido. |
| 403 | FORBIDDEN | Campo non scrivibile dal ruolo. |
05Modificare ed eliminare una nota
Modifica parziale: invia i campi da cambiare (tutti opzionali; almeno uno
richiesto). Se cambi il collegamento devi inviare related_module e
related_record_id insieme. Risponde
200 con l'oggetto aggiornato, 404 se
fuori scope, 422 in validazione.
curl -X PATCH https://crm.tuodominio.it/api/notes/4021 \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "content": "Rinnovo confermato per 12 mesi" }'
Soft delete: la nota viene marcata eliminata (is_deleted: true), non cancellata. Risponde 200 { deleted: true, id }.
06Allegati
Gli allegati sono file agganciati a un record. A differenza delle note, l'id
di un allegato è un UUID e related_record_id è una
stringa. Solo gli allegati in stato ready sono elencabili e
scaricabili; gli upload presigned non ancora confermati restano pending.
| Attributo | Tipo | Descrizione |
|---|---|---|
| id | string (UUID) | Identificativo univoco dell'allegato. |
| related_module | string | Modulo del record collegato. |
| related_record_id | string | Id del record collegato (fino a 120 caratteri). |
| virtual_folder | string | null | Cartella logica opzionale (max 120 caratteri). |
| file_name | string | Nome file originale (sanitizzato). |
| mime_type | string | Tipo MIME dichiarato. |
| size_bytes | integer | Dimensione in byte. |
| storage_backend | string | Backend di storage (es. local, s3). |
| status | string | ready (scaricabile) o pending (presigned non confermato). |
| created_by | integer | null | Utente che ha caricato il file. |
| created_at | string | Data/ora di creazione (ISO 8601). |
| Metodo | Path | Cosa fa |
|---|---|---|
| GET | /api/attachments | Elenco allegati di un record. |
| POST | /api/attachments | Upload diretto del file (multipart). |
| POST | /api/attachments/uploads | Avvia un upload presigned verso S3 (nessun byte qui). |
| POST | /api/attachments/:id/complete | Conferma un upload presigned completato. |
| GET | /api/attachments/:id/download | Scarica il file. |
| DELETE | /api/attachments/:id | Elimina l'allegato. |
Elenco allegati di un record
Restituisce gli allegati in stato ready di un record, ordinati per
data (dal più recente).
| Parametro | Tipo | Descrizione | |
|---|---|---|---|
| related_module | string | opzionale | Modulo del record collegato. |
| related_record_id | string | opzionale | Id del record collegato. |
curl "https://crm.tuodominio.it/api/attachments?related_module=deals&related_record_id=88" \ -H "Authorization: Bearer <accessToken>"
const res = await fetch("https://crm.tuodominio.it/api/attachments?related_module=deals&related_record_id=88", { headers: { Authorization: `Bearer ${accessToken}` }, }); const { data } = await res.json();
{ "success": true, "data": { "items": [ { "id": "7c2a…-…", "file_name": "offerta.pdf", "mime_type": "application/pdf", "size_bytes": 84213, "status": "ready", … } ] } }
Upload diretto (multipart)
Il modo più semplice: invii il file e la destinazione in un'unica richiesta
multipart/form-data. Il campo file si chiama
file.
| Campo | Tipo | Note | |
|---|---|---|---|
| file | file | richiesto | Il file da caricare. |
| related_module | string | richiesto | Modulo del record collegato. |
| related_record_id | string | richiesto | Id del record collegato. |
| virtual_folder | string | opzionale | Cartella logica (max 120 caratteri). |
curl -X POST https://crm.tuodominio.it/api/attachments \ -H "Authorization: Bearer <accessToken>" \ -F "file=@offerta.pdf" \ -F "related_module=deals" \ -F "related_record_id=88"
const form = new FormData(); form.append("file", fileInput.files[0]); form.append("related_module", "deals"); form.append("related_record_id", "88"); const res = await fetch("https://crm.tuodominio.it/api/attachments", { method: "POST", headers: { Authorization: `Bearer ${accessToken}` }, body: form, });
| Stato | Corpo |
|---|---|
| 201 | { success: true, data: { item: { …attachment } } } |
| 422 | VALIDATION_ERROR: file mancante o destinazione non valida. |
Upload presigned (S3)
Per file grandi con storage S3-compatibile, il client carica direttamente sull'object storage, senza far passare i byte dall'app. Tre passi:
- 1.
POST /attachments/uploadsconfile_name,mime_type,size_bytese la destinazione → riceviattachmentIde un URL firmato. - 2. Il client fa la
PUTdel file su quell'URL (direttamente su S3), con gli header indicati. - 3.
POST /attachments/:id/completeper confermare: l'allegato passa areadye diventa visibile.
| Campo | Tipo | Note | |
|---|---|---|---|
| file_name | string | richiesto | Nome file. Max 255 caratteri. |
| mime_type | string | richiesto | Tipo MIME. Max 160 caratteri. |
| size_bytes | integer | richiesto | Dimensione in byte. Intero positivo. |
| related_module | string | richiesto | Modulo del record collegato. |
| related_record_id | string | richiesto | Id del record collegato. |
| virtual_folder | string | opzionale | Cartella logica (max 120 caratteri). |
curl -X POST https://crm.tuodominio.it/api/attachments/uploads \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "file_name": "video.mp4", "mime_type": "video/mp4", "size_bytes": 52428800, "related_module": "deals", "related_record_id": "88" }'
const res = await fetch("https://crm.tuodominio.it/api/attachments/uploads", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ file_name: "video.mp4", mime_type: "video/mp4", size_bytes: 52428800, related_module: "deals", related_record_id: "88", }), }); const { data } = await res.json(); // data.mode === "direct": PUT del file su data.upload.url con data.upload.headers, // poi POST /api/attachments/${data.attachmentId}/complete
{ "success": true, "data": { "mode": "direct", "attachmentId": "7c2a…-…", "upload": { "url": "https://bucket.s3…/…?X-Amz-Signature=…", "method": "PUT", "headers": { "Content-Type": "video/mp4" } } } }
Se il backend attivo non supporta il trasferimento diretto, la risposta è
200 { "mode": "proxy" }: usa
l'upload diretto multipart.
Conferma che il file è stato caricato sul bucket. Il server verifica l'oggetto
(HeadObject, che rileva anche la dimensione reale) e flippa la riga a
ready. Idempotente: una seconda chiamata su una riga già
ready la restituisce e basta.
curl -X POST "https://crm.tuodominio.it/api/attachments/7c2a…-…/complete" \ -H "Authorization: Bearer <accessToken>"
const res = await fetch(`https://crm.tuodominio.it/api/attachments/${attachmentId}/complete`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}` }, }); const { data } = await res.json();
{ "success": true, "data": { "item": { "id": "7c2a…-…", "status": "ready", "file_name": "video.mp4", … } } }
Gli upload avviati ma mai confermati (passo 3) restano pending e
vengono ripuliti automaticamente dal worker (il janitor riduce le righe pending scadute).
Su storage locale il flusso presigned non serve: usa l'upload diretto.
Download
Restituisce il contenuto del file. Per sicurezza la risposta porta sempre
X-Content-Type-Options: nosniff; l'anteprima inline nel
browser è consentita solo per un insieme sicuro di tipi
(image/png, image/jpeg,
image/webp, image/gif,
application/pdf), mentre tutto il resto viene servito come
download forzato (Content-Disposition: attachment).
Se l'allegato è su S3, il server risponde con un redirect 302 a un URL firmato a vita breve: i byte scendono dal bucket, mai dall'app.
curl -L -OJ "https://crm.tuodominio.it/api/attachments/7c2a…-…/download" \ -H "Authorization: Bearer <accessToken>"
Eliminare un allegato
Elimina l'allegato (riga e file). Risponde 200 { deleted: true, item: { …attachment } }.
Le chiavi di storage sono generate lato server (nessun path scelto dal client) e i nomi file sono sanitizzati. Anche caricando un file con un MIME dichiarato "immagine" ma contenente HTML, il download non lo esegue nell'origine dell'app.