Torna alla guida

Paghe, spese e contabilità con l'API

Due modi di indirizzare una società, poi dipendenti, buste paga, assenze, spese, estratti bancari e rendiconto finanziario annuale, in chiamate REST.

Sviluppatori8 min di lettura
Indice+

Questa guida copre tutto ciò che non è fatturazione. Stessa chiave, stesso URL di base di Iniziare con l'API:

BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...

1. Due modi di indirizzare una società

Le rotte paghe sono indirizzate per società come tutte le altre risorse: /companies/{companyId}/employees e …/payslips. La stessa chiave fa le paghe di tutte le società a cui avete accesso — prendete 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/employees

La 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/employees

La 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; omesso, la busta segue la lingua del proprietario dell'account. Una modifica parziale ricalcola le buste non inviate del dipendente, 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.

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, dal più recente. year e month vanno sempre insieme (uno senza l'altro è un 400 VALIDATION_ERROR); employeeId filtra per persona — una pagina filtrata può essere corta, scorrete finché nextCursor è null. La creazione calcola la busta con tutti i controlli 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/payslips

Facoltativo: variableItems (bonus, straordinari, righe libere) e overrides. Risposte da prevedere: 409 PAYSLIP_ALREADY_EXISTS se il periodo esiste già per quel dipendente, 422 PAYSLIP_PERIOD_OUTSIDE_EMPLOYMENT fuori dalla finestra di impiego, 422 MISSING_IS_BAREME quando nessuna tariffa dell'imposta alla fonte è caricata per quel cantone e quell'anno, 403 PAYSLIP_LIMIT_REACHED esaurita la 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>/pdf

4. Assenze

Le assenze sono indirizzate per società (/companies/{companyId}/absences, modulo paghe), a differenza dei dipendenti. 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/absences

employeeId, 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; e notes.

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à, dividete 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>/status

Non c'è volutamente alcun filtro per periodo: un'assenza in corso non ha fine, e un filtro per mese la farebbe sparire. Scorrete l'anno e filtrate lato client, o restringete 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 '{ "vendorName": "Swisscom", "status": "toPay", "documentDate": "2026-02-28",
        "currency": "CHF", "total": 129.9, "category": "telecom" }' \
  $BASE/companies/$COMPANY/expenses

vendorName, status, documentDate, currency, total e category sono obbligatori; la ripartizione IVA (subtotal, vatRate, vatAmount) e gli identificativi del fornitore sono facoltativi. status vale toPay o paid. category è una delle 16 categorie fisse: rent, insurance, goodsPurchases, subcontracting, officeSupplies, itSoftware, telecom, vehicle, travel, meals, marketing, professionalFees, utilities, bankFees, training, other.

year è derivato da documentDate, e paidDate segue status: impostato a adesso quando la spesa è creata pagata senza data, cancellato appena lo stato non è più paid. Elencare un anno, poi segnare 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 memorizzato è esposto in sola lettura, con lo stesso redirect 302 dei PDF; una spesa creata senza giustificativo (hasDocument: false) risponde 404:

curl -L -H "Authorization: Bearer $KEY" -o receipt.pdf \
  $BASE/companies/$COMPANY/expenses/<expenseId>/document

Fuori perimetro in v1: il caricamento di un giustificativo (nessun endpoint multipart), l'estrazione via IA, e le bozze di importazione da Drive in attesa di convalida, invisibili qui. Codici da prevedere: 403 SUPPLIER_BILL_LIMIT_REACHED esaurita la quota mensile di spese, 403 CURRENCY_PRO_ONLY per una valuta diversa da CHF su un piano Free.

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": "Loyer janvier", "bankAccount": "CH93 0076 2011 6238 5295 7",
            "category": "rent" }
        ] }' \
  $BASE/companies/$COMPANY/bank-transactions/batch

transactions contiene da 1 a 100 righe (altrimenti 400 BATCH_SIZE_INVALID), ciascuna almeno con date, amount, currency, label e bankAccount. amount è con segno: un incasso è positivo, un addebito negativo. Il lotto è un'unica transazione di database, tutto viene scritto o nulla. importBatchId raggruppa le righe di uno stesso file; omesso, viene generato e la risposta ve lo restituisce. Una category inviata è registrata come scelta manuale che l'euristica di importazione non sovrascriverà mai.

{ "data": { "inserted": 1, "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 '{ "category": "insurance", "color": "#3c8fc3" }' \
  $BASE/companies/$COMPANY/bank-transactions/<transactionId>

year è l'anno civile della data di registrazione; un esercizio che non chiude a dicembre ne copre due, lasciate da parte il filtro per scorrere tutto lo storico. PATCH accetta category (segnata come manuale) e color, un'etichetta #rrggbb o "" per cancellarla; DELETE risponde 204.

7. Rendiconto finanziario

Il rendiconto finanziario annuale è una risorsa per società e per esercizio: fiscalYear è l'anno di chiusura (l'esercizio 2025/26 di una società che chiude a giugno è 2026) e funge da identificativo, da cui una coppia GET/PUT sull'anno invece di una collezione.

curl -H "Authorization: Bearer $KEY" \
  $BASE/companies/$COMPANY/financial-statements/2026

La lettura restituisce l'input memorizzato, l'istantanea ratesUsed e, una volta che il generatore dell'app l'ha prodotto, il documento calcolato sotto snapshot. 404 se nulla è memorizzato per quell'anno. La scrittura è un upsert su (società, esercizio):

curl -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "input": { "accounts": [ { "id": "CH93…", "currency": "CHF", "openingBalance": 12000 } ],
                   "manual": { "tradeReceivables": 0, "otherShortTermReceivables": 0,
                               "fixedAssets": 0, "ownerCurrentAccountBalance": 0,
                               "tradePayables": 0, "otherShortTermDebts": 0,
                               "avsBalance": 0, "lppBalance": 0, "accruedLiabilities": 0,
                               "taxProvision": 0, "shareCapital": 20000, "legalReserve": 0,
                               "retainedEarnings": 0, "depreciation": 0,
                               "proposedDividend": 0, "proposedReserveAllocation": 0,
                               "employeeCount": 2 },
                   "closingRates": { "EUR": 0.93 } } }' \
  $BASE/companies/$COMPANY/financial-statements/2026

Il corpo porta l'input del modulo di generazione: i conti bancari confermati con il saldo di apertura, le voci inserite a mano (manual), i tassi di cambio di chiusura e un period facoltativo. Lo snapshot è un dato derivato, prodotto dal generatore, mai nella richiesta: un PUT che lo omette lascia intatto il documento memorizzato. period serve solo per un esercizio che non è l'anno civile, e il suo to deve cadere in fiscalYear (altrimenti 422 PERIOD_FISCAL_YEAR_MISMATCH).

8. Mandati fiduciari

Per un fiduciario, /mandates è una rotta a livello di chiave, senza piano né modulo: elenca le società clienti che possedete. mandateId è semplicemente il companyId del mandato. Creare un mandato crea una società:

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Muster AG" }' $BASE/mandates

Da quel momento, ogni rotta /companies/{companyId}/… si applica al mandato con quello stesso identificativo — dipendenti, buste paga e assenze compresi: la stessa chiave elenca i dipendenti di un mandato e ne fa le paghe.

Per le regole valide ovunque (paginazione, date, corpi stretti, codici di errore, sandbox), leggete Convenzioni ed errori; il percorso di fatturazione è in Fatturare con l'API. Il dettaglio di ogni campo è nella Swagger UI del vostro deployment (…/api/docs).