openapi: 3.1.0 info: title: Tymee API version: "1.0" summary: API in sola lettura per le aziende su Tymee, con webhook in uscita. description: | Le API di Tymee permettono di collegare l'azienda ai vostri software: paghe, gestionale, controllo di gestione, strumenti di analisi. **Accesso.** Le API sono incluse nel contratto aziendale e le abilita Tymee per la vostra azienda. Poi un amministratore crea le chiavi da *Impostazioni azienda › API*. Ogni richiesta porta la chiave nell'header `Authorization: Bearer tym_live_…`. La chiave appartiene all'azienda e si vede una volta sola: conservatela come una password e revocatela se esce dal vostro controllo. **Sola lettura.** Tutte le richieste sono `GET`. I dati sono quelli che l'azienda vede in Tymee: niente ore in bozza, niente dati di altri clienti delle persone. **Persona.** Una persona è la collaborazione con l'azienda (freelance o dipendente). Il suo `id` resta lo stesso anche se la collaborazione viene chiusa e poi riattivata. **Elenchi e paginazione.** Gli elenchi rispondono con `data` e `pagination` (`limit` fino a 200, `offset`, `total`, `hasMore`). Per sincronizzare solo le modifiche usate `updatedSince` (data e ora ISO 8601 con fuso, ad esempio `2026-10-01T00:00:00Z`). **Filtri e ordinamento.** Ogni elenco ha i suoi filtri; quelli a valori fissi (come `status`) accettano più valori separati da virgola. `q` cerca un testo nei campi principali, senza distinguere maiuscole e minuscole. `sort` accetta i campi indicati per ogni elenco, separati da virgola, con `-` davanti per l'ordine decrescente (es. `sort=-date,hours`). **Limiti.** 120 richieste al minuto per chiave. Ogni risposta riporta `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset` (secondi). Oltre il limite: `429` con `Retry-After`. **Errori.** Formato `application/problem+json` (RFC 9457) con un `code` stabile: `missing_api_key`, `invalid_api_key`, `api_not_enabled`, `rate_limited`, `not_found`, `invalid_` (anche `invalid_sort`), `range_too_long`, `method_not_allowed`. **Versioni.** Nella v1 si possono aggiungere campi ed eventi, mai togliere o cambiare quelli esistenti: ignorate i campi che non conoscete. **Webhook.** Vedi la sezione *Webhook* in fondo alla pagina. contact: name: Tymee url: https://tymee.it servers: - url: "{baseUrl}/api/public/v1" variables: baseUrl: default: https://tymee.io description: Indirizzo del server Tymee (lo trovate in Impostazioni azienda › API). security: - apiKey: [] tags: - name: Azienda - name: Persone - name: Progetti e clienti - name: Ore - name: Rapportini - name: Costi - name: Documenti paths: /company: get: tags: [Azienda] summary: Dati dell'azienda description: L'azienda a cui appartiene la chiave usata nella richiesta. Utile per verificare che la chiave funzioni. operationId: getCompany responses: "200": description: Azienda content: application/json: schema: { $ref: "#/components/schemas/Company" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /people: get: tags: [Persone] summary: Elenco delle persone description: Freelance e dipendenti che collaborano (o hanno collaborato) con l'azienda. operationId: listPeople parameters: - name: status in: query description: "invited, active, suspended, ended, declined. Anche più valori separati da virgola (es. `active,suspended`)." schema: { type: string, example: active } - name: kind in: query description: freelancer o employee. schema: { type: string, example: freelancer } - name: projectId in: query description: Solo le persone assegnate a questo progetto. schema: { type: string, format: uuid } - $ref: "#/components/parameters/q" - $ref: "#/components/parameters/updatedSince" - name: sort in: query description: "Ordinamento: `name`, `status`, `kind`, `startedAt`, `endedAt`, `updatedAt`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `name`." schema: { type: string, example: "name" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di persone content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Person" } } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /people/{id}: get: tags: [Persone] summary: Dettaglio di una persona operationId: getPerson parameters: - $ref: "#/components/parameters/id" responses: "200": description: Persona content: application/json: schema: { $ref: "#/components/schemas/Person" } "404": { $ref: "#/components/responses/NotFound" } "401": { $ref: "#/components/responses/Unauthorized" } /projects: get: tags: [Progetti e clienti] summary: Elenco dei progetti operationId: listProjects parameters: - name: archived in: query description: true per i soli archiviati, false per i soli attivi; senza, tutti. schema: { type: boolean } - name: clientId in: query description: Solo i progetti di questo cliente finale. schema: { type: string, format: uuid } - name: managerId in: query description: Solo i progetti di questo referente (id del referente in `manager`). schema: { type: string, format: uuid } - name: personId in: query description: Solo i progetti a cui è assegnata questa persona. schema: { type: string, format: uuid } - $ref: "#/components/parameters/q" - $ref: "#/components/parameters/updatedSince" - name: sort in: query description: "Ordinamento: `name`, `code`, `createdAt`, `updatedAt`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `name`." schema: { type: string, example: "name" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di progetti content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Project" } } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /projects/{id}: get: tags: [Progetti e clienti] summary: Dettaglio di un progetto operationId: getProject parameters: - $ref: "#/components/parameters/id" responses: "200": description: Progetto content: application/json: schema: { $ref: "#/components/schemas/Project" } "404": { $ref: "#/components/responses/NotFound" } /clients: get: tags: [Progetti e clienti] summary: Clienti finali description: I clienti dell'azienda a cui fanno capo i progetti. operationId: listClients parameters: - name: archived in: query schema: { type: boolean } - $ref: "#/components/parameters/q" - name: sort in: query description: "Ordinamento: `name`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `name`." schema: { type: string, example: "name" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di clienti content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Client" } } /time-entries: get: tags: [Ore] summary: Ore registrate description: | Le ore delle persone sui progetti dell'azienda. Di default solo quelle **approvate** degli ultimi 30 giorni. L'intervallo `from`–`to` non può superare 366 giorni. Costo e ricavo sono valorizzati sulle ore approvate (IVA esclusa). operationId: listTimeEntries parameters: - name: from in: query description: Primo giorno (incluso), AAAA-MM-GG. Default 30 giorni prima di `to`. schema: { type: string, format: date } - name: to in: query description: Ultimo giorno (incluso), AAAA-MM-GG. Default oggi. schema: { type: string, format: date } - name: status in: query description: "approved (default), submitted, rejected o all. Anche più valori separati da virgola (es. `submitted,approved`)." schema: { type: string, default: approved } - name: personId in: query schema: { type: string, format: uuid } - name: projectId in: query schema: { type: string, format: uuid } - name: clientId in: query description: Solo le ore per questo cliente finale. schema: { type: string, format: uuid } - name: reportId in: query description: Solo le ore di questo rapportino. schema: { type: string, format: uuid } - name: inReport in: query description: true per le ore già in un rapportino inviato all'azienda, false per quelle non ancora rendicontate. schema: { type: boolean } - $ref: "#/components/parameters/updatedSince" - name: sort in: query description: "Ordinamento: `date`, `hours`, `submittedAt`, `reviewedAt`, `updatedAt`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `date`." schema: { type: string, example: "date" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di ore content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/TimeEntry" } } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": { $ref: "#/components/responses/RateLimited" } /reports: get: tags: [Rapportini] summary: Rapportini description: I rapportini inviati all'azienda da freelance e dipendenti (da confermare, confermati o rimandati indietro). operationId: listReports parameters: - name: status in: query description: "sent, confirmed, returned o all (default). Anche più valori separati da virgola (es. `sent,returned`)." schema: { type: string, default: all } - name: personId in: query schema: { type: string, format: uuid } - name: from in: query description: Rapportini che finiscono da questo giorno in poi. schema: { type: string, format: date } - name: to in: query description: Rapportini che iniziano entro questo giorno. schema: { type: string, format: date } - name: paymentStatus in: query description: "Stato del pagamento: none (nessuna segnalazione), reported, confirmed, disputed, paid. Anche più valori separati da virgola (es. `reported,disputed`)." schema: { type: string, example: reported } - name: invoiced in: query description: true per i rapportini con fattura emessa, false per quelli ancora da fatturare. schema: { type: boolean } - $ref: "#/components/parameters/updatedSince" - name: sort in: query description: "Ordinamento: `periodStart`, `periodEnd`, `amount`, `sentAt`, `invoicedDate`, `paidDate`, `updatedAt`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `-periodStart`." schema: { type: string, example: "-periodStart" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di rapportini content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Report" } } "400": { $ref: "#/components/responses/BadRequest" } /reports/{id}: get: tags: [Rapportini] summary: Dettaglio di un rapportino operationId: getReport parameters: - $ref: "#/components/parameters/id" responses: "200": description: Rapportino content: application/json: schema: { $ref: "#/components/schemas/Report" } "404": { $ref: "#/components/responses/NotFound" } /costs: get: tags: [Costi] summary: Costi e margini del mese description: Ore approvate del mese con costo, ricavo e margine, per progetto e per persona (IVA esclusa). operationId: getCosts parameters: - name: month in: query description: Mese nel formato AAAA-MM. Default il mese in corso. schema: { type: string, pattern: "^\\d{4}-\\d{2}$", example: "2026-10" } responses: "200": description: Costi del mese content: application/json: schema: { $ref: "#/components/schemas/Costs" } "400": { $ref: "#/components/responses/BadRequest" } /documents: get: tags: [Documenti] summary: Documenti description: Contratti e documenti caricati dall'azienda o dalle persone. operationId: listDocuments parameters: - name: personId in: query schema: { type: string, format: uuid } - name: category in: query description: "contract, freelancer_document, timesheet_signed, other. Anche più valori separati da virgola (es. `contract,other`)." schema: { type: string, example: contract } - name: uploadedBy in: query description: company o person. schema: { type: string, example: company } - $ref: "#/components/parameters/q" - name: sort in: query description: "Ordinamento: `createdAt`, `name`, `category`, `sizeBytes`. Più campi separati da virgola, `-` davanti per l'ordine decrescente. Default `-createdAt`." schema: { type: string, example: "-createdAt" } - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" responses: "200": description: Pagina di documenti content: application/json: schema: allOf: - $ref: "#/components/schemas/PageInfo" - type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Document" } } /documents/{id}/download: get: tags: [Documenti] summary: Link per scaricare un documento description: Link temporaneo (pochi minuti) per scaricare il file. Chiedetene uno nuovo a ogni download. operationId: getDocumentDownload parameters: - $ref: "#/components/parameters/id" responses: "200": description: Link temporaneo content: application/json: schema: { $ref: "#/components/schemas/DownloadLink" } "404": { $ref: "#/components/responses/NotFound" } webhooks: event: post: summary: Evento inviato dai webhook description: | Gli amministratori aggiungono gli indirizzi (https) da *Impostazioni azienda › API* e scelgono gli eventi. Per ogni evento Tymee invia una `POST` con corpo JSON e questi header: - `Tymee-Event`: tipo dell'evento (es. `hours.approved`); - `Tymee-Delivery`: id della consegna (uguale nei nuovi tentativi); - `Tymee-Signature`: `t=,v1=`. **Firma.** `v1` è l'HMAC-SHA256 esadecimale, calcolato con il segreto dell'indirizzo sulla stringa `.`. Ricalcolatela, confrontatela in tempo costante e rifiutate le richieste con `t` più vecchio di 5 minuti. **Risposta.** Rispondete `2xx` entro 10 secondi e lavorate l'evento dopo. Altrimenti Tymee riprova dopo 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h. Dopo 5 eventi non consegnati di fila l'indirizzo viene disattivato (si riattiva dal pannello). Un evento può arrivare più di una volta: usate `id` per riconoscerlo. **Eventi:** `hours.submitted`, `hours.approved`, `hours.rejected`, `report.sent`, `report.confirmed`, `report.invoiced`, `report.paid`, `collaboration.started`, `collaboration.ended`, `document.uploaded`. Il pulsante *Invia una prova* manda `ping`. security: [] parameters: - name: Tymee-Signature in: header required: true schema: { type: string, example: "t=1792760000,v1=5f0c…" } - name: Tymee-Event in: header required: true schema: { type: string } - name: Tymee-Delivery in: header required: true schema: { type: string, format: uuid } requestBody: content: application/json: schema: { $ref: "#/components/schemas/WebhookEvent" } responses: "200": description: Evento ricevuto (qualsiasi 2xx) components: securitySchemes: apiKey: type: http scheme: bearer bearerFormat: tym_live_… parameters: id: name: id in: path required: true schema: { type: string, format: uuid } limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 50 } offset: name: offset in: query schema: { type: integer, minimum: 0, default: 0 } q: name: q in: query description: Testo da cercare nei campi principali (nome, email, partita IVA, codice), senza distinguere maiuscole e minuscole. schema: { type: string } updatedSince: name: updatedSince in: query description: Solo gli elementi modificati dopo questo istante (ISO 8601 con fuso). schema: { type: string, format: date-time } responses: BadRequest: description: Parametro non valido content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Unauthorized: description: Chiave mancante, non valida, revocata o scaduta content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } Forbidden: description: API non abilitate per l'azienda content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } NotFound: description: Non trovato content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } RateLimited: description: Limite di richieste superato headers: Retry-After: schema: { type: integer } content: application/problem+json: schema: { $ref: "#/components/schemas/Problem" } schemas: Problem: type: object properties: type: { type: string } title: { type: string } status: { type: integer } code: { type: string, example: invalid_api_key } detail: { type: string } PageInfo: type: object properties: pagination: type: object properties: limit: { type: integer } offset: { type: integer } total: { type: integer } hasMore: { type: boolean } Ref: type: [object, "null"] properties: id: { type: string, format: uuid } name: { type: string } Rate: type: [object, "null"] description: Tariffa o costo, orario o giornaliero (su hoursPerDay ore). IVA esclusa. properties: type: { type: string, enum: [hourly, daily] } value: { type: number } hoursPerDay: { type: [integer, "null"] } validFrom: { type: [string, "null"], format: date } Company: type: object properties: id: { type: string, format: uuid } name: { type: string } vatNumber: { type: [string, "null"] } apiKeyName: { type: string, description: Nome della chiave usata } Person: type: object properties: id: { type: string, format: uuid, description: Id della collaborazione } kind: { type: string, enum: [freelancer, employee] } status: { type: string, enum: [invited, active, suspended, ended, declined] } name: { type: string } email: { type: string } vatNumber: { type: [string, "null"] } employeeNumber: { type: [string, "null"] } startedAt: { type: [string, "null"], format: date-time } endedAt: { type: [string, "null"], format: date-time } approvalFrequency: { type: string, enum: [weekly, monthly] } rate: { $ref: "#/components/schemas/Rate", description: Tariffa concordata in vigore (freelance) } cost: { $ref: "#/components/schemas/Rate", description: Costo (dipendenti) } contract: type: [object, "null"] properties: start: { type: [string, "null"], format: date } end: { type: [string, "null"], format: date } reference: { type: [string, "null"] } noticeDays: { type: [integer, "null"] } projects: { type: array, items: { $ref: "#/components/schemas/Ref" } } updatedAt: { type: string, format: date-time } Project: type: object properties: id: { type: string, format: uuid } name: { type: string } code: { type: [string, "null"], description: Codice commessa } archived: { type: boolean } client: { $ref: "#/components/schemas/Ref" } manager: { $ref: "#/components/schemas/Ref" } saleRate: { $ref: "#/components/schemas/Rate", description: Tariffa al cliente in vigore } budget: type: [object, "null"] properties: type: { type: string } basis: { type: [string, "null"] } value: { type: [number, "null"] } period: { type: [string, "null"] } alertPercent: { type: [integer, "null"] } block: { type: boolean } people: type: array items: type: object properties: personId: { type: string, format: uuid } name: { type: string } startDate: { type: [string, "null"], format: date } saleRate: { $ref: "#/components/schemas/Rate", description: Tariffa al cliente della persona, se diversa da quella del progetto } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } Client: type: object properties: id: { type: string, format: uuid } name: { type: string } vatNumber: { type: [string, "null"] } fiscalCode: { type: [string, "null"] } archived: { type: boolean } TimeEntry: type: object properties: id: { type: string, format: uuid } date: { type: string, format: date } hours: { type: number } description: { type: [string, "null"] } status: { type: string, enum: [submitted, approved, rejected] } person: { $ref: "#/components/schemas/Ref" } project: type: [object, "null"] properties: id: { type: string, format: uuid } name: { type: string } code: { type: [string, "null"] } client: { $ref: "#/components/schemas/Ref" } cost: { type: [number, "null"], description: Costo delle ore (solo approvate) } revenue: { type: [number, "null"], description: Ricavo al cliente (solo approvate, se il progetto ha una tariffa) } rejectionReason: { type: [string, "null"] } submittedAt: { type: [string, "null"], format: date-time } reviewedAt: { type: [string, "null"], format: date-time } reportId: { type: [string, "null"], format: uuid } updatedAt: { type: string, format: date-time } Report: type: object properties: id: { type: string, format: uuid } person: { $ref: "#/components/schemas/Ref" } periodStart: { type: string, format: date } periodEnd: { type: string, format: date } hours: { type: number } amount: { type: number, description: Imponibile } vatRate: { type: number } status: { type: string, enum: [sent, confirmed, returned] } sentAt: { type: [string, "null"], format: date-time } invoicedDate: { type: [string, "null"], format: date, description: Data della fattura emessa per il rapportino } paidDate: { type: [string, "null"], format: date } paymentStatus: { type: [string, "null"], enum: [reported, confirmed, disputed, paid, null] } paymentReportedDate: { type: [string, "null"], format: date } updatedAt: { type: string, format: date-time } CostRow: type: object properties: id: { type: [string, "null"], format: uuid, description: Progetto o persona } name: { type: [string, "null"] } detail: { type: [string, "null"] } hours: { type: number } revenue: { type: [number, "null"] } cost: { type: [number, "null"] } margin: { type: [number, "null"] } marginPercent: { type: [integer, "null"] } Costs: type: object properties: month: { type: string, example: "2026-10" } approvedHours: { type: number } pendingHours: { type: number } revenue: { type: [number, "null"] } cost: { type: [number, "null"] } margin: { type: [number, "null"] } marginPercent: { type: [integer, "null"] } byProject: { type: array, items: { $ref: "#/components/schemas/CostRow" } } byPerson: { type: array, items: { $ref: "#/components/schemas/CostRow" } } Document: type: object properties: id: { type: string, format: uuid } name: { type: string } category: { type: string, enum: [contract, freelancer_document, timesheet_signed, other] } contentType: { type: [string, "null"] } sizeBytes: { type: [integer, "null"] } person: { $ref: "#/components/schemas/Ref" } uploadedBy: { type: string, enum: [company, person] } visibleToPerson: { type: boolean } createdAt: { type: string, format: date-time } DownloadLink: type: object properties: url: { type: string } expiresAt: { type: string, format: date-time } WebhookEvent: type: object properties: id: { type: string, format: uuid, description: Id dell'evento (uguale nei nuovi tentativi) } type: type: string enum: [hours.submitted, hours.approved, hours.rejected, report.sent, report.confirmed, report.invoiced, report.paid, collaboration.started, collaboration.ended, document.uploaded, ping] createdAt: { type: string, format: date-time } apiVersion: { type: string, example: v1 } companyId: { type: string, format: uuid } data: type: object description: | Sempre `personId`, `personName`, `kind` e `summary` (testo leggibile). In più: ore → `timeEntryIds`, `hours` (e `from`, `to` per hours.submitted); rapportini → `reportId`; documenti → `documentId`. properties: personId: { type: string, format: uuid } personName: { type: string } kind: { type: string, enum: [freelancer, employee] } summary: { type: string } timeEntryIds: { type: array, items: { type: string, format: uuid } } hours: { type: number } from: { type: string, format: date } to: { type: string, format: date } reportId: { type: string, format: uuid } documentId: { type: string, format: uuid }