Torna alla guida

Ciclo paghe con l'API

Lo svolgimento di un mese di paghe in chiamate REST, dal calendario ai PDF, con ciò che ogni passaggio fa allo stato della busta paga.

Sviluppatori9 min di lettura
Indice+

Questa guida percorre un mese di paghe completo: preparare il mese, creare le buste paga, convalidarle, inviarle, correggerle se serve e infine recuperare i PDF. La creazione dei dipendenti e il dettaglio dei campi di una busta paga sono in Paghe, spese e contabilità con l'API. Stessa chiave e stesso URL di base di Iniziare con l'API:

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

1. Gli stati della busta paga

Tutte queste rotte richiedono il piano Business e il modulo paghe (modules.payroll in GET /me). Il campo status di una busta paga determina il resto di questa guida:

  • draft: modificabile ed eliminabile. Viene ricalcolata automaticamente se riguarda il mese in corso o un mese successivo, quando cambiano i dati del dipendente.
  • validated: bloccata, i ricalcoli automatici non la toccano più. Una busta paga vi arriva con validate o con il suo invio. Una correzione la riporta in draft.
  • declared: immutabile, la convalida, la correzione e l'eliminazione rispondono 409 PAYSLIP_DECLARED. Nessuna rotta dell'API imposta questo stato.

2. Preparare il mese

Queste rotte richiedono solo la lettura e non scrivono nulla. Il calendario riporta i giorni festivi infrasettimanali di ogni mese, per il cantone indicato oppure, senza workCanton, per quello della società:

curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payroll/calendar?year=2026"

curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payroll/holiday-variants?year=2026&canton=FR"

weekdayHolidaysByMonth ha un elemento per mese (l'indice 0 è gennaio) e resta vuoto quando hasCalendar vale false. holiday-variants elenca i regimi di un cantone, ed è vuoto se il cantone ne ha uno solo; la società sceglie il proprio con publicHolidayVariant in PATCH /companies/{companyId}. Per un dipendente con salario orario, l'anteprima mostra ciò che la busta paga pagherà:

curl -H "Authorization: Bearer $KEY" \
  "$BASE/companies/$COMPANY/employees/<employeeId>/holiday-preview?year=2026&month=8&canton=FR"

source vale calendar (con days, hours e la lista holidays), percentage (con rate) oppure none. Per un dipendente con salario mensile, data vale null. Prima di assumere o di cambiare un contratto, l'anteprima legale verifica condizioni non ancora registrate:

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "year": 2026,
        "employee": { "salaryMode": "monthly", "grossMonthlySalary": 3200, "workPercentage": 100,
                      "weeklyHours": 42, "vacationWeeks": 4, "dateOfBirth": "2003-04-12",
                      "thirteenthSalaryEnabled": false, "workCanton": "GE" } }' \
  $BASE/companies/$COMPANY/payroll/statutory-preview

La risposta è una lista di avvisi (code, level, params), ad esempio vacationBelowStatutoryMinimum o cantonalMinimumWageBelow. Non impedisce nulla: sta a te tenerne conto. Infine, le assenze del mese:

curl -H "Authorization: Bearer $KEY" \
  "$BASE/companies/$COMPANY/employees/<employeeId>/absence-lines?year=2026&month=7"

lines propone le righe da riportare sulla busta paga, con label, amount, type e gli indicatori subjectToCharges, subjectToIS e subjectToInsurance. Non viene scritto nulla: ricopia in variableItems quelle che vuoi mantenere. Se missingParams vale true, non è caricato alcun parametro d'indennità, quindi una lista vuota non significa che non ci sia nulla da pagare. Due altri controlli completano il quadro: GET …/employees/{employeeId}/absence-compliance?year=2026&month=7 e GET …/employees/{employeeId}/avs-gap?year=2026.

3. Creare le buste paga del mese

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "payslips": [
        { "employeeId": "<id1>", "year": 2026, "month": 7, "canton": "FR",
          "variableItems": [{ "label": "Premio di risultato", "amount": 500, "type": "bonus" }] },
        { "employeeId": "<id2>", "year": 2026, "month": 7, "canton": "FR", "hoursWorked": 120 }
      ] }' $BASE/companies/$COMPANY/payslips/batch

curl -H "Authorization: Bearer $KEY" "$BASE/companies/$COMPANY/payslips?year=2026&month=7"

Il lotto non è atomico: la risposta riporta il risultato di ogni riga (status vale created oppure error, con un code). Una riga di variableItems richiede label, amount e type (overtime, bonus, commission, custom, absence, deduction). Le buste paga partono in draft; rileggile, con le loro righe, tramite GET …/payslips/{payslipId}.

4. Convalidare

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{}' $BASE/companies/$COMPANY/payslips/<payslipId>/validate

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "reason": "Ore da rivedere" }' $BASE/companies/$COMPANY/payslips/<payslipId>/unvalidate

validate blocca la busta paga senza inviarla. Il suo corpo è obbligatorio, anche se vuoto, e reason vi è facoltativo. La risposta riporta changed: false se la busta paga era già convalidata. unvalidate la riporta in draft, con un reason obbligatorio, registrato nello storico. Nessuna delle due ricalcola la busta paga: una busta paga tornata in bozza viene ricalcolata solo alla prossima modifica sul dipendente o sulla società.

5. Inviare

curl -X POST -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/payslips/<payslipId>/send

Senza corpo, risposta 204. Il PDF parte per e-mail all'indirizzo del dipendente, nella lingua della sua busta paga, e una bozza passa a validated; sentStatus e sentDate vengono valorizzati. Una busta paga già inviata può essere inviata di nuovo. Da prevedere: 422 EMPLOYEE_EMAIL_MISSING, 503 PDF_UNAVAILABLE e 502 EMAIL_SEND_FAILED, questi ultimi due da riprovare. Nella sandbox non viene consegnato nulla, ma lo stato cambia allo stesso modo.

6. Correggere

Inizia misurando l'effetto di una correzione. preview-update richiede solo la lettura e non scrive nulla:

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "canton": "FR", "variableItems": [{ "label": "Premio di risultato", "amount": 800, "type": "bonus" }] }' \
  $BASE/companies/$COMPANY/payslips/<payslipId>/preview-update

La risposta riporta currentNetSalary, newNetSalary e netDelta, oppure null se il calcolo non va a buon fine, ad esempio in assenza di una tariffa dell'imposta alla fonte. La correzione vera e propria accetta gli stessi campi, più un motivo e il dettaglio delle modifiche:

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "canton": "FR",
        "variableItems": [{ "label": "Premio di risultato", "amount": 800, "type": "bonus" }],
        "reason": "Premio corretto",
        "changedFields": [{ "field": "Premio di risultato", "previousValue": "500", "newValue": "800" }],
        "createCorrection": true, "correctionLabel": "Correzione premio di giugno" }' \
  $BASE/companies/$COMPANY/payslips/<payslipId>
  • Risposta 204. canton, reason e changedFields sono obbligatori; un reason vuoto risponde 422 REASON_REQUIRED. Rinvia il canton letto sulla busta paga.
  • Senza variableItems o overrides, i valori salvati restano. Un array li sostituisce, [] li cancella. overrides accetta coppie { key, value }, dove key è la chiave di una riga della busta paga, ad esempio aanp. Le righe di correzione provenienti da un mese precedente vengono sempre mantenute.
  • Una busta paga convalidata torna in draft, e il suo PDF e il suo indicatore di invio vengono azzerati: richiama validate o send.
  • changedFields non viene calcolato: è la tua descrizione della modifica, copiata nello storico.

Lo storico si legge, dal più recente al più vecchio, con reason, changedFields e createdAt:

curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/payslips/<payslipId>/modifications

Se a cambiare è stata un'assenza, GET …/employees/{employeeId}/payslips-to-update?startDate=2026-03-01&endDate=2026-03-31 elenca le buste paga emesse le cui righe di assenza non corrispondono più (proposedCount a 0: la riga deve semplicemente sparire). Questa rotta non scrive nulla, la correzione resta da fare.

Per ripartire da zero, DELETE …/payslips/{payslipId} elimina una busta paga, con le sue righe e il suo storico. POST …/payslips/bulk-delete accetta da 1 a 100 ids ed è atomico: un identificativo sconosciuto o di un'altra società risponde 404 NOT_FOUND e non viene eliminato nulla. La risposta riporta affected e skipped, che conta gli identificativi ripetuti.

7. Duplicare su un altro mese

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "year": 2026, "month": 8 }' $BASE/companies/$COMPANY/payslips/<payslipId>/duplicate

La risposta 200 riporta l'id della copia, un draft dello stesso dipendente, ricalcolato per il suo mese con le righe variabili dell'originale. PDF, stato di invio e convalida non vengono ripresi. Da prevedere: 409 PAYSLIP_ALREADY_EXISTS se quel mese esiste già, 422 PAYSLIP_PERIOD_OUTSIDE_EMPLOYMENT fuori dal periodo di impiego.

8. Recuperare i PDF in lotto

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "year": 2026, "month": 7, "limit": 25 }' $BASE/companies/$COMPANY/payslips/pdf-batch

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "year": 2026, "month": 7, "limit": 25, "cursor": "<nextCursor>" }' \
  $BASE/companies/$COMPANY/payslips/pdf-batch
{
  "data": [
    { "payslipId": "...", "url": "https://...", "error": null },
    { "payslipId": "...", "url": null, "error": "PDF_UNAVAILABLE" }
  ],
  "nextCursor": "..."
}

limit va da 1 a 25 (10 per impostazione predefinita) e questa rotta richiede l'accesso in scrittura, perché genera i file mancanti. Ripeti la chiamata con lo stesso periodo e il nextCursor ricevuto finché non vale null. Una riga in errore non ferma il lotto: rilanciala da sola con GET …/payslips/{payslipId}/pdf. Gli url hanno vita breve (15 minuti), scaricali subito.

9. Monitorare l'area dipendente

curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/employee-portal-links

curl -H "Authorization: Bearer $KEY" $BASE/companies/$COMPANY/employees/<employeeId>/portal-link

La lista è paginata e riporta, per dipendente, employeeId e status. status contiene lastSentAt, expiresAt e active, oppure vale null se non è stato inviato alcun link. Queste rotte si limitano a leggere e non restituiscono mai il link stesso: l'invio si fa nell'app, come spiega Gestire l'area dipendente.

10. Riprese, LAINF e AVS: cosa risponde oggi

In produzione ogni mese si calcola da solo: il cumulo annuale del massimale dell'assicurazione contro la disoccupazione (AD), i periodi datati dell'assicurazione contro gli infortuni (LAINF) e la scelta AVS annuale non entrano nel calcolo delle buste paga. Le rotte corrispondenti figurano comunque nel contratto e rispondono così:

  • GET …/payslips/{payslipId}/cumulative-status risponde { "enabled": false, "unqualifiedMonths": [] }. validate e send non si bloccano quindi per un mese precedente mancante.
  • …/employees/{employeeId}/year-openings/{year} legge (null senza ripresa), salva (PUT, con throughMonth, acGross e acBase) ed elimina una ripresa dei cumuli. Non modifica nessuna busta paga ordinaria: la leggono solo i pagamenti legati a un anno di acquisizione, versati dopo l'uscita del dipendente o acquisiti prima dell'età di riferimento.
  • …/employees/{employeeId}/laa-periods restituisce [] finché non è registrato alcun periodo, e …/laa-periods/simulate calcola senza scrivere nulla. …/laa-summary/{year} resta vuoto in questo caso.
  • …/employees/{employeeId}/avs-choices/{year} legge e salva una scelta annuale, senza effetto sul calcolo. Richiede un numero AVS valido (422 PAYROLL_AVS_IDENTITY_REQUIRED) e un anno a partire dal 2023 (422 PAYROLL_AVS_CHOICE_INVALID). La rinuncia alla franchigia che conta oggi è il campo avsFranchiseWaived del dipendente.

Per la paginazione, i corpi delle richieste e i codici di errore, continua con Convenzioni ed errori. Il dettaglio di ogni campo è nella Swagger UI del tuo deployment (…/api/docs).