Questa guida copre tutto ciò che non è fatturazione, tranne lo svolgimento mensile delle buste paga, descritto in dettaglio in Ciclo paghe con l'API. Stessa chiave e stesso URL di base di Iniziare con l'API:
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. Indirizzare una società
Le rotte delle paghe sono indirizzate per società, come tutte le altre risorse: /companies/{companyId}/employees e …/payslips. La stessa chiave gestisce le paghe di tutte le società a cui hai accesso: prendi il companyId da GET /companies.
Tutto il resto è indirizzato esplicitamente: assenze, spese, transazioni bancarie e rendiconti vivono sotto /companies/{companyId}/…, e una stessa chiave vi serve per tutte le società accessibili, a condizione che il modulo corrispondente sia attivo.
COMPANY=<companyId>2. Dipendenti
curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/employeesLa lista è paginata (?limit, ?cursor, ordine di creazione) e restituisce ogni dipendente con identità, indirizzo, contratto, salario e tassi assicurativi. La creazione riprende l'insieme di campi del modulo dell'app:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"firstName": "Anna", "lastName": "Muster",
"address": { "kind": "swiss", "country": "CH", "street": "Rue du Lac", "streetNumber": "1",
"postalCode": "1700", "city": "Fribourg", "canton": "FR", "communeBfs": 2196 },
"permitType": "swiss", "contractType": "cdi", "maritalStatus": "single",
"grossMonthlySalary": 6500, "workPercentage": 100,
"startDate": "2026-01-01", "vacationWeeks": 5,
"thirteenthSalaryEnabled": true,
"language": "de"
}' $BASE/companies/$COMPANY/employeesLa risposta 201 contiene id e slug. Il campo facoltativo language (fr, de, en, it) è la lingua della busta paga: etichette memorizzate, PDF ed e-mail di invio; se omesso, la busta paga segue la lingua del proprietario dell'account. Una modifica parziale ricalcola le buste paga del dipendente ancora in bozza, del mese in corso e dei mesi successivi, come una modifica a schermo:
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "grossMonthlySalary": 6800 }' $BASE/companies/$COMPANY/employees/<employeeId>Da prevedere: 403 EMPLOYEE_LIMIT_REACHED quando la quota dipendenti del piano è raggiunta, 422 END_DATE_REQUIRED_FOR_CDD per un contratto a tempo determinato senza data di fine, 422 END_DATE_BEFORE_START_DATE se precede l'inizio.
DELETE …/employees/{employeeId} risponde 204 ed elimina anche le sue buste paga, le sue assenze e i suoi link dell'area dipendente. Viene rifiutato (409 PAYSLIP_DECLARED) se una delle sue buste paga è dichiarata.
3. Buste paga
curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payslips?year=2026&month=7"
curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payslips?employeeId=<employeeId>"Lista paginata, dalla più recente alla più vecchia. year e month vanno sempre insieme (uno senza l'altro è un 400 VALIDATION_ERROR); employeeId filtra per persona: una pagina filtrata può essere corta, quindi scorri le pagine finché nextCursor non vale null. La creazione calcola la busta paga con tutte le verifiche dell'app:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "employeeId": "<employeeId>", "year": 2026, "month": 7, "canton": "FR" }' \
$BASE/companies/$COMPANY/payslipsFacoltativi: variableItems (bonus, ore straordinarie, righe libere) e overrides. hoursWorked è obbligatorio per un dipendente con salario orario. Risposte da prevedere: 409 PAYSLIP_ALREADY_EXISTS se il periodo esiste già per quel dipendente, 422 PAYSLIP_PERIOD_OUTSIDE_EMPLOYMENT fuori dal periodo di impiego, 422 MISSING_IS_BAREME quando per quel cantone e quell'anno non è caricata alcuna tariffa dell'imposta alla fonte, 403 PAYSLIP_LIMIT_REACHED al raggiungimento della quota mensile.
Per un mese intero, il lotto accetta da 1 a 100 buste e non è atomico: ogni elemento è elaborato da solo, e la risposta riporta l'esito di ciascuno. Un errore a metà lotto non annulla le buste già create.
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "payslips": [
{ "employeeId": "<id1>", "year": 2026, "month": 7, "canton": "FR" },
{ "employeeId": "<id2>", "year": 2026, "month": 7, "canton": "FR" }
] }' $BASE/companies/$COMPANY/payslips/batch{
"data": {
"results": [
{ "index": 0, "status": "created", "id": "..." },
{ "index": 1, "status": "error", "code": "PAYSLIP_ALREADY_EXISTS" }
]
}
}Il PDF si scarica come quello di una fattura, con -L per seguire il redirect 302 verso un URL valido 15 minuti; il primo download di una busta può richiedere qualche secondo, il tempo di generarla.
curl -L -H "Authorization: Bearer $KEY" -o payslip.pdf $BASE/companies/$COMPANY/payslips/<payslipId>/pdfLe anteprime, la convalida, l'invio, la correzione, la duplicazione e i PDF in lotto sono descritti in dettaglio in Ciclo paghe con l'API.
4. Assenze
Le assenze sono indirizzate per società (/companies/{companyId}/absences, modulo paghe). Dichiarare una malattia al 50 % su due settimane:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "employeeId": "<employeeId>", "motif": "maladie",
"startDate": "2026-03-02", "endDate": "2026-03-13",
"incapacityPercent": 50 }' \
$BASE/companies/$COMPANY/absencesemployeeId, motif e startDate sono obbligatori. motif vale maladie, accident, apg_maternite, apg_paternite, apg_adoption, apg_prise_en_charge, apg_service, maintien_salaire, conge_court o conge_non_paye. Le date qui sono stringhe YYYY-MM-DD, non millisecondi, perché è così che le assenze sono memorizzate e ordinate; un'assenza senza endDate è in corso.
dailyAmountOverride: indennità giornaliera comunicata dall'assicuratore, che scavalca sia il tasso sia il tetto legale.waitingDaysOverride: giorni di attesa della polizza malattia.ratePercentOverride: tasso da 0 a 100, predefinito 80 o quello configurato per il dipendente.takenAsIsolatedDays: paternità o adozione presa in giorni isolati (integrazione IPG 7 per 5).incapacityPercent: da 1 a 100, predefinito 100; enotes.
La risposta 201 restituisce l'assenza, sempre in stato declared. 409 ABSENCE_OVERLAPS_EXISTING segnala che un'altra assenza dello stesso dipendente copre già alcuni di quei giorni: una sovrapposizione raddoppierebbe le righe di trattenuta e indennità, dividi i periodi.
curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "motif": "maladie", "startDate": "2026-03-02", "endDate": "2026-03-20" }' \
$BASE/companies/$COMPANY/absences/<absenceId>Lo stato ha la propria sotto-rotta: declaredapprovedpaid, un semplice marcatore di seguito, qualsiasi destinazione è accettata da qualsiasi stato.
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "status": "approved" }' \
$BASE/companies/$COMPANY/absences/<absenceId>/statusNon c'è volutamente alcun filtro per periodo: un'assenza in corso non ha fine, e un filtro per mese la farebbe sparire. Scorri l'anno e filtra lato client, o restringi per employeeId. Eliminare un'assenza non ritira le righe già contabilizzate su una busta paga; solo un ricalcolo della busta lo fa.
5. Spese
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "kind": "supplierInvoice", "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
"currency": "CHF", "total": 129.9, "accountId": "<accountId>" }' \
$BASE/companies/$COMPANY/expenseskind, vendorName, status, documentDate, currency e total sono obbligatori; la ripartizione dell'IVA (subtotal, vatRate, vatAmount) e gli identificativi del fornitore sono facoltativi. status vale toPay o paid. kind vale supplierInvoice, expenseReport, vatReturn, taxAssessment, socialStatement, shareholderResolution, financingContract o other. taxAssessment e socialStatement richiedono un documentNumber che indichi la natura o il regime e il periodo.
year deriva da documentDate e paidDate segue status: impostato alla data e ora correnti quando la spesa viene creata già pagata e senza data, cancellato non appena lo stato non è più paid. Per impostazione predefinita la lista restituisce le fatture fornitore, ?kind= sceglie un altro tipo. Elencare un anno, poi contrassegnare una spesa come pagata:
curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/expenses?year=2026"
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "status": "paid" }' \
$BASE/companies/$COMPANY/expenses/<expenseId>Il giustificativo si scarica con lo stesso redirect 302 dei PDF; una spesa senza giustificativo (hasDocument: false) risponde 404:
curl -L -H "Authorization: Bearer $KEY" -o receipt.pdf \
$BASE/companies/$COMPANY/expenses/<expenseId>/documentPer allegarlo, invia il file stesso come corpo della richiesta, senza multipart (PDF, PNG, JPEG o WebP, 10 MB al massimo). L'operazione si può fare una sola volta: una spesa che ha già un giustificativo risponde 409 DOCUMENT_ALREADY_ATTACHED.
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
--data-binary @receipt.pdf $BASE/companies/$COMPANY/expenses/<expenseId>/documentPOST …/expenses/bulk-status e POST …/expenses/bulk-delete elaborano da 1 a 100 identificativi e rispondono affected e skipped: un identificativo sconosciuto viene conteggiato in skipped, senza bloccare gli altri. Se il tuo abbonamento non copre il modulo Contabilità, prevedi anche 403 SUPPLIER_BILL_LIMIT_REACHED al raggiungimento della quota mensile di spese e 403 CURRENCY_PRO_ONLY per una valuta diversa dal CHF.
Bozze di spesa
Una bozza è una spesa in attesa di revisione: non compare in GET …/expenses e consuma la quota di spese solo una volta accettata. Una società conserva al massimo 100 bozze in attesa, importazione da Google Drive compresa (409 EXPENSE_DRAFT_LIMIT_REACHED). Due modi per crearne:
# Lettura del documento con IA, in una sola chiamata
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
--data-binary @receipt.pdf "$BASE/companies/$COMPANY/expense-drafts/extract?locale=it&filename=receipt.pdf"
# Senza IA: carica il file, poi crea la bozza con i tuoi valori
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/pdf" \
--data-binary @receipt.pdf $BASE/companies/$COMPANY/uploads/expense
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "kind": "supplierInvoice", "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
"currency": "CHF", "total": 129.9, "uploadId": "<uploadId>" }' \
$BASE/companies/$COMPANY/expense-drafts…/extract risponde 201 con draft, readabilityScore e quota. Ogni lettura riuscita consuma un'unità della quota mensile di estrazione (200 con Business), una lettura fallita non la consuma. Codici da prevedere: 403 EXPENSE_AI_QUOTA_REACHED, 422 SCAN_UNREADABLE (invia una scansione migliore), 429 AI_RATE_LIMITED e 502 AI_REQUEST_FAILED (riprova).
…/uploads/expense restituisce un uploadId valido per un'ora. POST …/expense-drafts lo consuma, senza IA né quota di estrazione, e DELETE …/uploads/expense/{uploadId} scarta un file non utilizzato. Un identificativo scaduto risponde 403 UPLOAD_EXPIRED, un identificativo creato da un altro utente o per un'altra società 403 FILE_ACCESS_DENIED.
Per la revisione: GET …/expense-drafts (paginato), GET …/expense-drafts/{draftId}, GET …/expense-drafts/{draftId}/document per il file e PATCH …/expense-drafts/{draftId} per correggere un campo. Poi accetta o rifiuta:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "ids": ["<draftId>"] }' $BASE/companies/$COMPANY/expense-drafts/accept…/accept e …/reject accettano da 1 a 100 ids e rispondono affected e skipped. Un identificativo sconosciuto viene conteggiato in skipped, ma una fattura fornitore senza conto fa fallire l'intera chiamata con 422 CLASSIFICATION_ACCOUNT_REQUIRED: correggila prima con PATCH. Il rifiuto è definitivo, il file viene eliminato insieme alla bozza.
6. Transazioni bancarie
Non c'è un POST singolo: l'app crea transazioni solo a lotti, un file importato è un lotto, e il lotto è il punto d'ingresso.
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "fileName": "ubs-2026-01.csv",
"transactions": [
{ "date": "2026-01-15", "amount": -1250, "currency": "CHF",
"label": "Affitto gennaio", "bankAccount": "CH93 0076 2011 6238 5295 7",
"bankReference": "CH9300762011623852957:2026-0001" }
] }' \
$BASE/companies/$COMPANY/bank-transactions/batchtransactions contiene da 1 a 100 righe (altrimenti 400 BATCH_SIZE_INVALID), ciascuna con almeno date, amount, currency, label e bankAccount. amount è con segno: un incasso è positivo, un addebito negativo. Il lotto è una sola transazione di database: tutto viene scritto oppure niente. importBatchId raggruppa le righe di uno stesso file; se omesso, viene generato e la risposta te lo restituisce. Un accountId inviato classifica la riga, come una scelta manuale che l'euristica di importazione non sovrascriverà mai.
{ "data": { "inserted": 1, "skipped": 0, "importBatchId": "9f1c…" } }curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/bank-transactions?year=2026"
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "comment": "Affitto da verificare", "color": "#3c8fc3" }' \
$BASE/companies/$COMPANY/bank-transactions/<transactionId>year è l'anno civile della data di contabilizzazione (date); un esercizio che non termina a dicembre si estende su due anni civili, quindi lascia da parte il filtro per scorrere l'intero storico. PATCH accetta i campi della riga, color, un'etichetta #rrggbb oppure "" per cancellarla, e accountId, null per rimuovere la classificazione; DELETE risponde 204.
Per più righe: POST …/bank-transactions/bulk-delete, …/bulk-color e …/bulk-account accettano da 1 a 100 ids e rispondono affected e skipped; …/bulk-accounts accetta coppie { id, accountId }. …/matching/delete e …/matching/account agiscono su ciò che un filtro seleziona in un anno (year obbligatorio), a blocchi: ripeti lo stesso corpo finché done vale false. …/categorize fa classificare da 1 a 100 righe tramite IA e consuma la quota mensile di precompilazione (100 con Business).
7. Conto annuale
Il conto annuale è una risorsa di sola lettura per società e per esercizio: fiscalYear è l'anno di chiusura (l'esercizio 2025/26 di una società che chiude a giugno è 2026). L'API fornisce la versione che la società ha allestito nell'app, una volta chiuso l'esercizio nel libro mastro.
curl -H "Authorization: Bearer $KEY" \
$BASE/companies/$COMPANY/financial-statements/2026La lettura restituisce la versione in vigore così come la stampa il suo PDF: le righe balanceSheet e incomeStatement con le denominazioni fissate e gli importi dell'esercizio e dell'esercizio precedente, la proposta d'impiego dell'utile appropriation (SA, Sagl) o la ripartizione tra i soci partnerAllocation (società di persone), con il numero di version, la language del documento e il pdfStatus. 404 finché il conto non è allestito, e di nuovo dopo la riapertura dell'esercizio.
8. Mandati fiduciari
Per una fiduciaria, /mandates è una rotta a livello di chiave, senza piano né modulo. GET /mandates elenca i tuoi mandati senza paginazione: le società che possiedi (kind: owned, id uguale al companyId), poi le tue richieste e i tuoi accessi a società gestite da un altro account (kind: external, id della richiesta, companyId della società). ?includeClosed=true aggiunge i mandati archiviati e le richieste chiuse. Creare un mandato crea una società:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Muster AG" }' $BASE/mandatesLa risposta 201 restituisce il mandato. Il corpo accetta name (con ide facoltativo), ide da solo se la società è già su Bill Alps, oppure companyId da solo. Una società che appartiene già a un altro account non viene duplicata: la risposta è 202, con una richiesta pending, e i suoi amministratori ricevono un'e-mail. Da prevedere: 402 SUBSCRIPTION_REQUIRED senza un abbonamento fiduciario attivo, 403 FIDUCIAIRE_ONLY su un account che non è fiduciario, 409 IDE_ALREADY_USED e 422 INVALID_IDE sul numero IDI.
Quattro azioni senza corpo completano la lista, su /mandates/{mandateId}/…: archive e restore per un mandato che possiedi, cancel per ritirare una richiesta in sospeso, leave per restituire un accesso accettato.
Da quel momento ogni rotta /companies/{companyId}/… si applica al mandato con il suo companyId, dipendenti, buste paga e assenze comprese, finché il plan del mandato è business: la stessa chiave elenca i dipendenti di un mandato e ne gestisce le paghe.
Per le regole valide ovunque (paginazione, date, corpi stretti, codici di errore, sandbox) leggi Convenzioni ed errori; il percorso di fatturazione è in Fatturare con l'API, il mese di paga in Ciclo paghe con l'API. Il dettaglio di ogni campo è nella Swagger UI del tuo deployment (…/api/docs).