API do Selfcare
Liga o Selfcare ao teu ERP, ao software de ponto, ao BI, a uma app móvel ou a um assistente de IA. A API age em nome de uma pessoa e vê exatamente o que ela vê na app.
URL base: https://api.selfcare.work/v1Especificação OpenAPI 3.1 OAuth 2.1
Para uma app usada por várias pessoas (uma app móvel, um assistente, um produto teu), usa OAuth 2.1 em vez de chaves: cada pessoa entra com a sua conta, escolhe a empresa e vê o que a app pede. Regista a app em Definições → API → Apps que desenvolvi. Uma app nativa ou de browser é pública (sem segredo); um servidor é confidencial e autentica-se com client_secret, mostrado só ao criar ou ao rodar.
Só há authorization code com PKCE S256. O state é obrigatório e o redirect_uri tem de ser exatamente um dos registados: um esquema próprio com ponto (work.selfcare.app:/oauth), https, ou loopback por IP (http://127.0.0.1/callback, com qualquer porta, RFC 8252). Os domínios do Selfcare (selfcare.work e subdomínios) não servem de redirect. Cada resposta ao teu redirect, com código ou com erro, traz iss=https://api.selfcare.work (RFC 9207): confirma-o junto com o state antes de trocar o código. Os metadados estão em /.well-known/oauth-authorization-server.
GET https://app.selfcare.work/oauth/authorize?response_type=code
&client_id=sc_app_…&redirect_uri=work.selfcare.app:/oauth
&scope=me:read%20absences:read%20absences:write&state=…
&code_challenge=BASE64URL(SHA256(code_verifier))&code_challenge_method=S256
POST https://api.selfcare.work/oauth/token
grant_type=authorization_code&client_id=sc_app_…&code=sc_ac_…
&redirect_uri=work.selfcare.app:/oauth&code_verifier=…
POST https://api.selfcare.work/oauth/token
grant_type=refresh_token&client_id=sc_app_…&refresh_token=sc_rt_…
O access_token (sc_at_…) dura 1 hora e usa-se como Authorization: Bearer, tal como uma chave. O refresh_token (sc_rt_…) dura 90 dias e roda em cada uso: guarda sempre o novo e descarta o antigo. Reutilizar um refresh token já usado revoga a sessão inteira, porque só acontece se alguém o copiou.
A app age sempre como a pessoa: o acesso efetivo é o âmbito concedido cruzado com o que o papel dela permite na empresa. Para terminar a sessão, chama POST /oauth/revoke com o refresh token. A pessoa revoga a app em Definições → API → Apps autorizadas, e o dono e o RH veem as apps autorizadas na empresa.
import { OAuthSession, SelfcareClient, buildAuthorizeUrl, createPkcePair, exchangeCode, parseAuthorizationResponse, randomString } from "@selfcare/api-sdk";
const clientId = "sc_app_…";
const redirectUri = "work.selfcare.app:/oauth";
const { verifier, challenge } = await createPkcePair();
const state = randomString();
openSystemBrowser(buildAuthorizeUrl({ clientId, redirectUri, scopes: ["me:read", "absences:write"], state, codeChallenge: challenge }));
// On the redirect: check "state" and "iss" (RFC 9207), then exchange the code
const { code } = parseAuthorizationResponse(redirectedUrl, { state, issuer: "https://api.selfcare.work" });
const tokens = await exchangeCode({ clientId, code, redirectUri, codeVerifier: verifier });
const session = new OAuthSession(tokens, { clientId }, (t) => saveToKeychain(t)); // t === null: signed out
const api = new SelfcareClient({ token: () => session.accessToken(), onUnauthorized: () => session.forceRefresh() });Webhooks
Os webhooks avisam um servidor teu quando algo muda: ausências, despesas, deslocações, documentos, pessoas e equipas. Cada endpoint é de uma pessoa e só recebe o que ela vê no momento da entrega. Cria-os em Definições → API (cartão Webhooks) ou pela API (POST /v1/webhook-endpoints, âmbito webhooks:manage), até 10 por pessoa, só em https público. O segredo (whsec_…) aparece ao criar e ao rodar.
Cada entrega é um POST JSON com o id do evento, o tipo, a empresa e os ids do recurso, da ficha e da pessoa, com o estado e as datas. Nunca leva o IBAN, motivos, tipos de ausência, nomes de documentos nem valores: lê o resto pela API, com as permissões de quem lê.
POST https://erp.example.pt/selfcare/webhooks
Selfcare-Signature: t=1760000000,v1=5257a869…
Selfcare-Event-Id: 0b8e…
Selfcare-Event-Type: absence.approved
Selfcare-Delivery-Attempt: 1
{
"id": "0b8e…",
"type": "absence.approved",
"apiVersion": "v1",
"createdAt": "2026-11-03T09:12:00.000Z",
"occurredAt": "2026-11-03T09:12:00.000Z",
"tenantId": "aaaa…",
"data": { "object": "absence", "id": "3f1c…", "employeeId": "…", "userId": "…", "status": "APPROVED" }
}Verifica sempre a assinatura antes de confiar no corpo. Selfcare-Signature traz t= (o instante em segundos) e um ou mais v1= (o HMAC-SHA256 em hex) do segredo sobre t.corpo (o corpo tal como chegou). Recusa assinaturas com mais de 5 minutos e deduplica pelo id do evento: a entrega é pelo menos uma vez, sem ordem garantida.
import { verifyWebhookSignature } from "@selfcare/api-sdk";
export async function POST(req: Request) {
const body = await req.text(); // the raw body, before JSON.parse
const ok = await verifyWebhookSignature({
header: req.headers.get("Selfcare-Signature"),
body,
secret: process.env.SELFCARE_WEBHOOK_SECRET!, // whsec_…
});
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(body);
if (await alreadyProcessed(event.id)) return new Response(null, { status: 200 }); // at least once
await handle(event); // then read the resource through the API
return new Response(null, { status: 200 });
}Sem o SDK, em Node:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(header: string | null, body: string, secret: string): boolean {
if (!header) return false;
const parts = header.split(",").map((p) => p.trim());
const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2));
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
return parts
.filter((p) => p.startsWith("v1="))
.some((p) => {
const got = Buffer.from(p.slice(3), "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
});
}Responde 2xx em menos de 10 segundos (trata o evento depois, numa fila). Sem 2xx, há até 8 tentativas em cerca de 24 horas (1 min, 5 min, 30 min, 2 h, 5 h, 8 h e 8 h). Ao fim de 3 dias seguidos a falhar, o endpoint desliga-se e o dono é avisado na app e por email. As entregas ficam 30 dias no registo, com os primeiros 2 KB da resposta, e podem ser reenviadas (POST /v1/webhook-deliveries/{id}/redeliver). Para experimentar, POST /v1/webhook-endpoints/{id}/test envia um webhook.test. Depois de rodar o segredo, o anterior continua a assinar (segundo v1) durante 24 horas.
Eventos: absence.created, absence.updated, absence.approved, absence.rejected, absence.cancellation_requested, absence.cancelled, expense.submitted, expense.approved, expense.rejected, expense.paid, expense.cancelled, business_trip.submitted, business_trip.approved, business_trip.rejected, business_trip.cancelled, document.created, document.version_created, document.deleted, document.acknowledged, document.expiring, document.expired, document.review_requested, document.reviewed, employee.created, employee.updated, employee.status_changed, employee.contract_changed, employee.compensation_changed, employee.bank_details_change_requested, employee.bank_details_changed, employee.bank_details_change_rejected, team.created, team.updated, team.deleted, team.member_added, team.member_removed.
MCP (assistentes de IA)
O servidor MCP liga o Selfcare a assistentes de IA como o Claude, o ChatGPT ou o Cursor. Cada pessoa entra com a sua conta e o assistente passa a poder marcar férias, registar despesas, ver documentos e tratar aprovações, sempre a agir como ela e a ver só o que ela vê na app. As ferramentas usam os mesmos endpoints da API, com os mesmos âmbitos, limites e registo de auditoria (com a origem MCP).
https://api.selfcare.work/mcp
Claude
1. Em Definições → Conectores, escolhe Adicionar conector personalizado.
2. Dá-lhe um nome (Selfcare) e cola o URL acima.
3. Carrega em Ligar: abre o ecrã de autorização do Selfcare. Entra, escolhe a empresa e confirma o que o assistente pode fazer.
ChatGPT
1. Em Definições → Apps e conectores, ativa o modo de programador (nas definições avançadas) e escolhe Criar.
2. Cola o URL acima e escolhe OAuth como autenticação.
3. Confirma no ecrã de autorização do Selfcare, como no Claude.
Cursor
1. Abre as definições do Cursor, secção MCP, e acrescenta o servidor ao mcp.json (do projeto ou global).
2. Na primeira utilização, o Cursor abre o browser para entrares no Selfcare e autorizares.
{
"mcpServers": {
"selfcare": { "url": "https://api.selfcare.work/mcp" }
}
}Clientes que não fazem OAuth podem usar uma chave de API pessoal no cabeçalho Authorization. Cria-a com os âmbitos das ferramentas que vais usar:
{
"mcpServers": {
"selfcare": {
"url": "https://api.selfcare.work/mcp",
"headers": { "Authorization": "Bearer sc_live_…" }
}
}
}Nos três casos, o cliente regista-se sozinho (registo dinâmico, RFC 7591) e o Selfcare mostra um aviso de que se trata de um assistente de IA. Transporte Streamable HTTP sem sessão. Sem token, a resposta é 401 com WWW-Authenticate e resource_metadata (RFC 9728); os tokens são emitidos para o recurso https://api.selfcare.work/mcp (RFC 8707) e não servem na API REST, nem o contrário. Ferramentas:
| Ferramenta | Âmbito | Tipo |
|---|
| get_me | me:read | Leitura |
| get_my_summary | me:read | Leitura |
| get_vacation_balance | absences:read | Leitura |
| list_absence_types | absences:read | Leitura |
| list_absences | absences:read | Leitura |
| request_absence | absences:write | Escrita |
| cancel_absence | absences:write | Escrita |
| get_absence | absences:read | Leitura |
| list_pending_approvals | absences:read | Leitura |
| approve_absence | absences:decide | Escrita |
| reject_absence | absences:decide | Escrita |
| list_expenses | expenses:read | Leitura |
| get_expense | expenses:read | Leitura |
| list_expense_categories | expenses:read | Leitura |
| create_expense | expenses:write | Escrita |
| get_expense_receipt_upload_url | expenses:write | Escrita |
| add_expense_receipt | expenses:write | Escrita |
| submit_expense | expenses:write | Escrita |
| approve_expense | expenses:decide | Escrita |
| reject_expense | expenses:decide | Escrita |
| list_documents | documents:read | Leitura |
| get_document_link | documents:read | Leitura |
| search_employees | employees:read | Leitura |
Dos documentos, o assistente só vê o regulamento interno, as políticas e os manuais da empresa e os comprovativos de despesa da própria pessoa, cada um na sua pasta. Nunca vê recibos de vencimento, recibos de ajudas de custo, tipos de documento criados pela empresa, contratos e adendas, documentos de identificação (cartão de cidadão, passaporte, autorização de residência), comprovativos de NIF, NISS ou IBAN, documentos de saúde ou disciplinares, atestados, o IBAN completo (sempre mascarado) nem dados de processamento salarial, e não está disponível para contabilistas. Cada escrita fica no registo de auditoria da empresa com a origem MCP e a app. A pessoa revoga o acesso em Definições → API → Apps autorizadas, e os clientes registados que ninguém autoriza em 7 dias são apagados.
Guia: app móvel
Este guia percorre uma app móvel de férias e despesas: login nativo com OAuth, push, ecrã inicial, pedir férias e registar uma despesa com a foto do recibo. A app é uma app pública (sem segredo). Regista-a em Definições → API → Apps que desenvolvi, com o tipo pública e o redirect no esquema da app.
1. Login nativo com PKCE
import { OAuthSession, SelfcareClient, buildAuthorizeUrl, createPkcePair, exchangeCode, parseAuthorizationResponse, randomString } from "@selfcare/api-sdk";
const clientId = "sc_app_…";
const redirectUri = "work.selfcare.app:/oauth";
const { verifier, challenge } = await createPkcePair();
const state = randomString();
openSystemBrowser(buildAuthorizeUrl({ clientId, redirectUri, scopes: ["me:read", "absences:write"], state, codeChallenge: challenge }));
// On the redirect: check "state" and "iss" (RFC 9207), then exchange the code
const { code } = parseAuthorizationResponse(redirectedUrl, { state, issuer: "https://api.selfcare.work" });
const tokens = await exchangeCode({ clientId, code, redirectUri, codeVerifier: verifier });
const session = new OAuthSession(tokens, { clientId }, (t) => saveToKeychain(t)); // t === null: signed out
const api = new SelfcareClient({ token: () => session.accessToken(), onUnauthorized: () => session.forceRefresh() });2. Registar o telefone e mostrar o ecrã inicial
Abre o authorization_endpoint no browser do sistema (ASWebAuthenticationSession no iOS, Custom Tabs no Android; nunca numa WebView), com um code_verifier e um state novos em cada login. Guarda os tokens no Keychain ou no Keystore. Depois do login, e em cada arranque, regista o telefone com POST /v1/me/devices: o dispositivo fica preso a esta sessão. Os push levam só type, resourceType, resourceId e uma frase curta; tipos: absence.approved, absence.rejected, approval.pending, expense.approved, expense.rejected, expense.reimbursed, document.requested, bank_details.confirmation_pending.
// After signing in, and on every launch
await api.registerMyDevice({ body: { platform: "ios", pushToken: apnsToken, locale: "pt-PT", appVersion: "1.0.0" } });
// Home screen: balance, upcoming absences, approvals, documents, unread notifications
const home = await api.getMySummary();// Push payload (APNs / FCM data): never names, amounts or absence types
{ "type": "absence.approved", "resourceType": "absence", "resourceId": "3f1c…" }
// On tap: api.getAbsence({ params: { id: resourceId } })3. Pedir férias
Mostra o saldo antes de pedir. O pedido verifica o saldo, os feriados, os dias de descanso da pessoa e os períodos bloqueados como a app, e segue para os aprovadores (as equipas da pessoa) se o tipo pedir aprovação. Usa uma Idempotency-Key por pedido para que uma repetição por falha de rede não crie dois.
const { data: types } = await api.listAbsenceTypes();
const balance = await api.getVacationBalance({ params: { id: me.employee!.id } });
const absence = await api.createAbsence({
body: { absenceTypeCode: "FERIAS", startDate: "2026-12-21", endDate: "2026-12-24" },
idempotencyKey: crypto.randomUUID(), // keep it if you retry
});
// absence.status: "PENDING" (goes to the approvers) or "APPROVED"4. Despesa com a foto do recibo
A despesa nasce em rascunho. O recibo vai direto para o armazenamento por um URL pré-assinado de 15 minutos (o bilhete vale 30); a confirmação verifica o sha256 e o tipo real do ficheiro. Depois, submete a despesa para aprovação. O estado de reembolso vê-se em status (APPROVED, PAID) e paidAt.
const expense = await api.createExpense({ body: { date: "2026-11-03", description: "Almoço com cliente", category: "MEALS" } });
// The receipt photo: PDF, JPEG, PNG or WebP, up to 10 MB
const upload = await api.createExpenseAttachmentUpload({
params: { id: expense.id },
body: { fileName: "recibo.jpg", contentType: "image/jpeg", sizeBytes: bytes.length, sha256: sha256Hex(bytes) },
});
await fetch(upload.url, { method: upload.method, headers: upload.headers, body: bytes });
await api.addExpenseAttachment({ params: { id: expense.id }, body: { ticket: upload.ticket, amountCents: 2450 } });
await api.submitExpense({ params: { id: expense.id } });No logout, chama POST /oauth/revoke com o refresh token (no SDK, session.signOut()): a sessão e os dispositivos dela deixam de receber push. Se um refresh falhar com invalid_grant, volta ao login.
Ausências e férias
get/v1/absence-types
Tipos de ausência da empresa
Âmbito: absences:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "FERIAS",
"name": "Férias",
"color": "string",
"isPaid": true,
"countsCalendarDays": true,
"countsAgainstVacation": true,
"requiresApproval": true,
"maxDaysPerYear": 0,
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
get/v1/absences
Ausências que o utilizador vê na app
Sobreposição com o intervalo from–to. Os rascunhos só aparecem ao próprio.
Âmbito: absences:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| from | query | não | date |
| to | query | não | date |
| status | query | não | string |
| employeeId | query | não | uuid |
| teamId | query | não | uuid |
| updatedSince | query | não | date-time |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
post/v1/absences
Marcar uma ausência
Para si (sem employeeId) ou, se for RH, dono ou líder do visado, para outra pessoa. A mesma validação da app: saldo de férias, dias úteis pela escala e feriados, dias bloqueados, máximo anual do tipo. Não deixa marcar por cima de outra ausência da mesma pessoa.
Âmbito: absences:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500
get/v1/absences/{id}
Uma ausência
Âmbito: absences:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
patch/v1/absences/{id}
Corrigir um pedido pendente
Só enquanto ninguém decidiu nada. Valida tudo de novo e recomeça a cadeia de aprovação. Aceita If-Match.
Âmbito: absences:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 429, 500
post/v1/absences/{id}/approve
Aprovar uma ausência
A regra da app: o aprovador decide a sua linha (ALL ou ANY); RH e dono por cima, nunca as próprias (excepto o dono).
Âmbito: absences:decide.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
post/v1/absences/{id}/reject
Rejeitar uma ausência
A mesma regra de aprovar.
Âmbito: absences:decide.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
post/v1/absences/{id}/cancel
Cancelar uma ausência
Um pedido pendente cancela-se logo. Uma ausência aprovada cancela-se logo ou fica com o cancelamento à espera de aprovação (PENDING_CANCELLATION), como na app.
Âmbito: absences:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"absenceType": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string",
"color": "string"
},
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"startHalfDay": true,
"endHalfDay": true,
"workingDays": 0,
"reason": "string",
"status": "DRAFT",
"source": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"cancellationRequestedAt": "2026-10-16T09:30:00.000Z",
"approvals": [
{
"approverId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"viaTeams": "string",
"decision": "string",
"decidedAt": "2026-10-16T09:30:00.000Z",
"comment": "string"
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
get/v1/employees/{id}/vacation-balance
Saldo de férias de um colaborador
Para quem vê as ausências da pessoa na app.
Âmbito: absences:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| year | query | não | integer |
Resposta · Exemplo
{
"data": {
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"year": 0,
"entitled": 0,
"carriedOver": 0,
"used": 0,
"scheduled": 0,
"available": 0,
"carryRemaining": 0,
"carryWindowOpen": true,
"carryDeadline": "30/04"
},
"error": null
}Erros: 401, 402, 403, 404, 422, 429, 500
Despesas
get/v1/expenses
Despesas que o utilizador vê na app
As do próprio (incluindo rascunhos); as da equipa para quem lidera; as da empresa para dono, RH e contabilista. Lista vazia com o módulo desligado.
Âmbito: expenses:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| status | query | não | string |
| employeeId | query | não | uuid |
| from | query | não | date |
| to | query | não | date |
| updatedSince | query | não | date-time |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
post/v1/expenses
Criar uma despesa (rascunho)
Do próprio, ou em nome de alguém por quem gere as despesas. Depois juntam-se os recibos e submete-se.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500
get/v1/expenses/{id}
Uma despesa
Âmbito: expenses:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
patch/v1/expenses/{id}
Editar uma despesa
Rascunhos e rejeitadas. Aceita If-Match.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 429, 500
get/v1/expenses/{id}/attachments/{attachmentId}/content
Descarregar um anexo de despesa
Devolve um URL pré-assinado válido durante 5 minutos, nunca um redireccionamento.
Âmbito: expenses:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| attachmentId | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"url": "https://example.com",
"expiresAt": "2026-10-16T09:30:00.000Z",
"fileName": "string",
"contentType": "string"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
get/v1/expense-categories
Categorias de despesa
O nome vem na língua do Accept-Language.
Âmbito: expenses:read.
Resposta · Exemplo
{
"data": [
{
"code": "MEALS",
"name": "string"
}
],
"error": null
}Erros: 401, 402, 403, 429, 500
post/v1/expenses/{id}/attachments/upload-url
Pedir o URL para um recibo
Devolve um URL de PUT (15 minutos) e um bilhete (30 minutos). PDF, JPEG, PNG ou WebP, até 10 MB, como na app.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"url": "string",
"method": "PUT",
"headers": {},
"ticket": "string",
"expiresAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
post/v1/expenses/{id}/attachments
Juntar um recibo
Depois do PUT. Confere o sha256 declarado e o tipo real pelos bytes.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500
patch/v1/expenses/{id}/attachments/{attachmentId}
Corrigir o valor de um recibo
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| attachmentId | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
delete/v1/expenses/{id}/attachments/{attachmentId}
Tirar um recibo
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| attachmentId | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
post/v1/expenses/{id}/submit
Submeter para reembolso
As mesmas verificações da app e o mesmo aviso a quem decide.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
post/v1/expenses/{id}/cancel
Cancelar uma despesa
Enquanto não está paga nem num lote de pagamento.
Âmbito: expenses:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
post/v1/expenses/{id}/approve
Aprovar uma despesa
RH, dono, contabilista e líderes dos membros; as próprias, só o dono.
Âmbito: expenses:decide.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
post/v1/expenses/{id}/reject
Rejeitar uma despesa
Com a nota obrigatória, que o colaborador vê.
Âmbito: expenses:decide.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employee": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fullName": "string",
"employeeNumber": "string"
},
"date": "2026-10-16",
"description": "string",
"category": "MEALS",
"amountCents": 0,
"currency": "EUR",
"status": "DRAFT",
"rejectionNote": "string",
"submittedAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z",
"decidedByName": "string",
"paidAt": "2026-10-16T09:30:00.000Z",
"attachments": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"amountCents": 0
}
],
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
Documentos
get/v1/document-types
Tipos de documento
Sem os tipos de saúde nem os disciplinares.
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "PAYSLIP",
"name": "string",
"scope": "PERSON",
"hasExpiry": true,
"requiresAck": true,
"system": true,
"active": true
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
get/v1/documents
Documentos que o utilizador vê na app
Cada documento passa pela mesma regra de acesso da app. Os de saúde e os disciplinares nunca aparecem.
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| employeeId | query | não | uuid |
| spaceId | query | não | uuid |
| typeKey | query | não | string |
| period | query | não | string |
| expiresBefore | query | não | date |
| updatedSince | query | não | date-time |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"title": "string",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"space": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"kind": "PERSON",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teamId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10"
},
"ownerEmployeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"folder": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
},
"visibility": "PRIVATE",
"status": "PENDING_REVIEW",
"origin": "string",
"expiresAt": "2026-10-16",
"period": "2026-09",
"ackRequested": true,
"ackDueDate": "2026-10-16",
"currentVersion": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"number": 0,
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"sha256": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
post/v1/documents
Criar um documento
Confirma o upload com o bilhete, confere o sha256, o tamanho e o tipo real. Um recibo de vencimento da mesma pessoa e mês fica como versão nova.
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500
get/v1/documents/{id}
Um documento
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"title": "string",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"space": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"kind": "PERSON",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teamId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10"
},
"ownerEmployeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"folder": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
},
"visibility": "PRIVATE",
"status": "PENDING_REVIEW",
"origin": "string",
"expiresAt": "2026-10-16",
"period": "2026-09",
"ackRequested": true,
"ackDueDate": "2026-10-16",
"currentVersion": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"number": 0,
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"sha256": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
patch/v1/documents/{id}
Editar um documento
Título ou validade, quando a app o deixa. Aceita If-Match.
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"title": "string",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"space": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"kind": "PERSON",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teamId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10"
},
"ownerEmployeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"folder": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
},
"visibility": "PRIVATE",
"status": "PENDING_REVIEW",
"origin": "string",
"expiresAt": "2026-10-16",
"period": "2026-09",
"ackRequested": true,
"ackDueDate": "2026-10-16",
"currentVersion": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"number": 0,
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"sha256": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 429, 500
delete/v1/documents/{id}
Apagar um documento
Quando a app o deixa. Aceita If-Match.
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"deleted": true
},
"error": null
}Erros: 401, 402, 403, 404, 409, 412, 422, 429, 500
get/v1/documents/{id}/versions
Versões de um documento
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"number": 0,
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"sha256": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 404, 422, 429, 500
post/v1/documents/{id}/versions
Carregar uma versão nova
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500
get/v1/documents/{id}/versions/{versionId}/content
Descarregar uma versão
Devolve um URL pré-assinado válido durante 5 minutos e regista o acesso (origem API).
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| versionId | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"url": "https://example.com",
"expiresAt": "2026-10-16T09:30:00.000Z",
"fileName": "string",
"contentType": "string"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
get/v1/document-requirements
Documentos obrigatórios da empresa
Sem os de saúde nem os disciplinares.
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"requiresExpiry": true,
"active": true
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
get/v1/employees/{id}/document-checklist
Estado dos documentos obrigatórios de um colaborador
Para o próprio e para quem gere documentos (dono, RH, contabilista). Sem itens de saúde.
Âmbito: documents:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": [
{
"requirementId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"state": "MISSING",
"documentId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"expiresAt": "2026-10-16",
"daysLeft": 0
}
],
"error": null
}Erros: 401, 402, 403, 404, 429, 500
post/v1/uploads
Pedir o URL para um documento
Documento novo, versão nova ou recibo de vencimento (só quem gere documentos). Tipos de saúde e disciplinares são recusados.
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"url": "string",
"method": "PUT",
"headers": {},
"ticket": "string",
"expiresAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500
post/v1/documents/{id}/acknowledge
Confirmar a leitura
Da versão atual, quando foi pedida.
Âmbito: documents:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"title": "string",
"type": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"code": "string",
"name": "string"
},
"space": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"kind": "PERSON",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teamId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10"
},
"ownerEmployeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"folder": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
},
"visibility": "PRIVATE",
"status": "PENDING_REVIEW",
"origin": "string",
"expiresAt": "2026-10-16",
"period": "2026-09",
"ackRequested": true,
"ackDueDate": "2026-10-16",
"currentVersion": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"number": 0,
"fileName": "string",
"contentType": "string",
"sizeBytes": 0,
"sha256": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
Colaboradores
get/v1/employees
Colaboradores (diretório)
Procurar por nif é só para quem vê dados pessoais.
Âmbito: employees:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| q | query | não | string |
| status | query | não | string |
| teamId | query | não | uuid |
| employeeNumber | query | não | string |
| nif | query | não | string |
| updatedSince | query | não | date-time |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeNumber": "string",
"externalCode": "string",
"fullName": "string",
"preferredName": "string",
"workEmail": "string",
"workPhone": "string",
"jobTitle": "string",
"status": "ACTIVE",
"kind": "string",
"photoUrl": "string",
"workLocation": "string",
"locationId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teams": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
}
],
"personalEmail": "string",
"phone": "string",
"personalData": {
"dateOfBirth": "2026-10-16",
"nationality": "string",
"nif": "string",
"niss": "string",
"citizenCardNumber": "string",
"citizenCardExpiry": "2026-10-16",
"iban": "string",
"swiftBic": "string",
"address": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"fiscalAddress": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"emergencyContact": {
"name": "string",
"relation": "string",
"phone": "string"
}
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
post/v1/employees
Criar um colaborador
A mesma acção e a mesma validação do ecrã Novo colaborador (dono, RH ou gestor). Cria o contrato inicial com a data de entrada e, com convite, envia-o por email. O salário não se define aqui: usa POST /employees/{id}/compensation.
Âmbito: employees:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500
get/v1/employees/{id}
Um colaborador
Âmbito: employees:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeNumber": "string",
"externalCode": "string",
"fullName": "string",
"preferredName": "string",
"workEmail": "string",
"workPhone": "string",
"jobTitle": "string",
"status": "ACTIVE",
"kind": "string",
"photoUrl": "string",
"workLocation": "string",
"locationId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teams": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
}
],
"personalEmail": "string",
"phone": "string",
"personalData": {
"dateOfBirth": "2026-10-16",
"nationality": "string",
"nif": "string",
"niss": "string",
"citizenCardNumber": "string",
"citizenCardExpiry": "2026-10-16",
"iban": "string",
"swiftBic": "string",
"address": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"fiscalAddress": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"emergencyContact": {
"name": "string",
"relation": "string",
"phone": "string"
}
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
patch/v1/employees/{id}
Editar a ficha de um colaborador
Muda só os campos enviados, pelas regras da ficha na app. Quem não pode escrever nada na pessoa recebe 404. If-Match opcional (412 se a ficha mudou).
Âmbito: employees:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeNumber": "string",
"externalCode": "string",
"fullName": "string",
"preferredName": "string",
"workEmail": "string",
"workPhone": "string",
"jobTitle": "string",
"status": "ACTIVE",
"kind": "string",
"photoUrl": "string",
"workLocation": "string",
"locationId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teams": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
}
],
"personalEmail": "string",
"phone": "string",
"personalData": {
"dateOfBirth": "2026-10-16",
"nationality": "string",
"nif": "string",
"niss": "string",
"citizenCardNumber": "string",
"citizenCardExpiry": "2026-10-16",
"iban": "string",
"swiftBic": "string",
"address": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"fiscalAddress": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"emergencyContact": {
"name": "string",
"relation": "string",
"phone": "string"
}
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 429, 500
get/v1/employees/{id}/contracts
Histórico de contratos de um colaborador
Âmbito sensível, marcado à parte. Para dono, RH, contabilista e o próprio.
Âmbito: contracts:read (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"effectiveFrom": "2026-10-16",
"effectiveTo": "2026-10-16",
"reason": "HIRING",
"contractType": "PERMANENT",
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"probationEndDate": "2026-10-16",
"weeklyHours": 0,
"vacationDaysPerYear": 0,
"ccts": "string",
"notes": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 404, 422, 429, 500
post/v1/employees/{id}/contracts
Registar uma alteração de contrato
Âmbito contracts:write, só dono e RH. Motivo obrigatório e Idempotency-Key obrigatória. A entrada vigente fecha na véspera da data de efeito, que tem de ser posterior. Fica no registo de auditoria com o antes e o depois, e o colaborador é avisado.
Âmbito: contracts:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | sim | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 428, 429, 500
get/v1/employees/{id}/compensation
Histórico de remuneração de um colaborador
Âmbito sensível, marcado à parte. Para dono, RH, contabilista e o próprio. Não é processamento salarial.
Âmbito: compensation:read (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"effectiveFrom": "2026-10-16",
"effectiveTo": "2026-10-16",
"reason": "HIRING",
"currency": "EUR",
"baseSalaryMonthlyCents": 0,
"salaryPaidMonths": 0,
"mealAllowanceDailyCents": 0,
"mealAllowanceForm": "string",
"seniorityBonusCents": 0,
"coverflexMonthlyCents": 0,
"expenseAllowanceCents": 0,
"workingTimeExemption": {
"enabled": true,
"modality": "NOT_SUBJECT_TO_PNT_LIMITS",
"compensationMode": "PERCENT_OF_BASE",
"compensationValue": 0,
"agreementDate": "2026-10-16"
},
"external": {
"rateType": "string",
"rateAmountCents": 0,
"unitsPerMonth": 0,
"vacationPaid": true,
"absencesPaid": true,
"monthsPaidPerYear": 0
},
"notes": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 404, 422, 429, 500
post/v1/employees/{id}/compensation
Registar uma alteração de remuneração
Âmbito compensation:write, só dono e RH. Motivo obrigatório e Idempotency-Key obrigatória. O subsídio de alimentação é sempre o da empresa. Fica no registo de auditoria com o antes e o depois; o colaborador é avisado na app e por email, sem valores.
Âmbito: compensation:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | sim | string |
Erros: 201, 400, 401, 402, 403, 404, 409, 422, 428, 429, 500
get/v1/employees/{id}/bank-details/pending
Mudança de IBAN à espera de confirmação
Para quem vê os dados pessoais da pessoa (dono, RH, contabilista e o próprio). A mudança só entra em vigor quando o colaborador a confirma na app. data é null quando não há nenhuma pendente.
Âmbito: employees:read.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"status": "PENDING",
"ibanMasked": "****0154",
"requestedAt": "2026-10-16T09:30:00.000Z",
"expiresAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
delete/v1/employees/{id}/bank-details/pending
Cancelar a mudança de IBAN pendente
O próprio, o dono ou o RH. O IBAN da ficha não muda. Sem pedido pendente, 404.
Âmbito: bank-details:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"status": "PENDING",
"ibanMasked": "****0154",
"requestedAt": "2026-10-16T09:30:00.000Z",
"expiresAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
post/v1/employees/{id}/archive
Arquivar um colaborador
Dono e RH. Tira a pessoa das listas e dos seletores sem cessar o contrato, como na app. Um colaborador cessado ou já arquivado dá 409.
Âmbito: employees:write.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeNumber": "string",
"externalCode": "string",
"fullName": "string",
"preferredName": "string",
"workEmail": "string",
"workPhone": "string",
"jobTitle": "string",
"status": "ACTIVE",
"kind": "string",
"photoUrl": "string",
"workLocation": "string",
"locationId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"teams": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"name": "string"
}
],
"personalEmail": "string",
"phone": "string",
"personalData": {
"dateOfBirth": "2026-10-16",
"nationality": "string",
"nif": "string",
"niss": "string",
"citizenCardNumber": "string",
"citizenCardExpiry": "2026-10-16",
"iban": "string",
"swiftBic": "string",
"address": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"fiscalAddress": {
"street": "string",
"city": "string",
"postalCode": "string",
"country": "string"
},
"emergencyContact": {
"name": "string",
"relation": "string",
"phone": "string"
}
},
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500
patch/v1/employees/{id}/contracts/{contractId}
Corrigir uma entrada de contrato
Âmbito contracts:write, só dono e RH. If-Match e motivo obrigatórios. Como na app, só se corrige enquanto não há recibos emitidos (ou num prestador); depois disso regista-se uma entrada nova.
Âmbito: contracts:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| contractId | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | sim | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"effectiveFrom": "2026-10-16",
"effectiveTo": "2026-10-16",
"reason": "HIRING",
"contractType": "PERMANENT",
"startDate": "2026-10-16",
"endDate": "2026-10-16",
"probationEndDate": "2026-10-16",
"weeklyHours": 0,
"vacationDaysPerYear": 0,
"ccts": "string",
"notes": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 428, 429, 500
patch/v1/employees/{id}/compensation/{entryId}
Corrigir uma entrada de remuneração
Âmbito compensation:write, só dono e RH. If-Match e motivo obrigatórios. Muda só os campos enviados; o colaborador é avisado.
Âmbito: compensation:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| entryId | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | sim | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"effectiveFrom": "2026-10-16",
"effectiveTo": "2026-10-16",
"reason": "HIRING",
"currency": "EUR",
"baseSalaryMonthlyCents": 0,
"salaryPaidMonths": 0,
"mealAllowanceDailyCents": 0,
"mealAllowanceForm": "string",
"seniorityBonusCents": 0,
"coverflexMonthlyCents": 0,
"expenseAllowanceCents": 0,
"workingTimeExemption": {
"enabled": true,
"modality": "NOT_SUBJECT_TO_PNT_LIMITS",
"compensationMode": "PERCENT_OF_BASE",
"compensationValue": 0,
"agreementDate": "2026-10-16"
},
"external": {
"rateType": "string",
"rateAmountCents": 0,
"unitsPerMonth": 0,
"vacationPaid": true,
"absencesPaid": true,
"monthsPaidPerYear": 0
},
"notes": "string",
"createdAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 428, 429, 500
put/v1/employees/{id}/bank-details
Registar ou pedir a mudança do IBAN
Pode o próprio, o dono ou o RH. Sem IBAN na ficha (onboarding), o primeiro grava logo (APPLIED). A mudança de um IBAN existente nunca muda a ficha: cria um pedido pendente, cifrado, que só tem efeito quando o colaborador o confirma na app com um código (PENDING_CONFIRMATION); substitui um pedido anterior por confirmar. Para uma mudança: sem conta na app, 409; sem a cifra configurada, 503. A resposta mostra só os últimos 4 dígitos.
Âmbito: bank-details:write (sensível, marcado à parte).
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"outcome": "APPLIED",
"ibanMasked": "****0154",
"pending": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"employeeId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"status": "PENDING",
"ibanMasked": "****0154",
"requestedAt": "2026-10-16T09:30:00.000Z",
"expiresAt": "2026-10-16T09:30:00.000Z",
"decidedAt": "2026-10-16T09:30:00.000Z"
}
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 422, 429, 500, 503
Avisos assinados quando algo muda (ausências, despesas, documentos, pessoas, equipas). Cada endpoint é de uma pessoa e só recebe o que ela vê no momento da entrega.
get/v1/webhook-endpoints
Listar os meus endpoints de webhooks
Só os de quem pede, nesta empresa. O segredo nunca sai aqui.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"url": "https://erp.example.pt/selfcare/webhooks",
"description": "string",
"events": [
"absence.created"
],
"status": "ACTIVE",
"failingSince": "2026-10-16T09:30:00.000Z",
"disabledAt": "2026-10-16T09:30:00.000Z",
"disabledReason": "string",
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 422, 429, 500
post/v1/webhook-endpoints
Criar um endpoint de webhooks
Cada entrega é um POST JSON assinado com HMAC-SHA256: Selfcare-Signature: t=<segundos>,v1=<hex de HMAC(segredo, ${t}.${corpo})>. Recusa assinaturas com mais de 5 minutos e deduplica pelo id do evento (entrega pelo menos uma vez, sem ordem garantida). Até 8 tentativas em cerca de 24 horas. O evento só leva ids, estado, datas e a empresa; o resto lê-se pela API. O segredo só aparece nesta resposta. Sem cifra configurada, 503.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| Idempotency-Key | header | não | string |
Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500, 503
get/v1/webhook-endpoints/{id}
Ver um endpoint de webhooks
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"url": "https://erp.example.pt/selfcare/webhooks",
"description": "string",
"events": [
"absence.created"
],
"status": "ACTIVE",
"failingSince": "2026-10-16T09:30:00.000Z",
"disabledAt": "2026-10-16T09:30:00.000Z",
"disabledReason": "string",
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 401, 402, 403, 404, 429, 500
patch/v1/webhook-endpoints/{id}
Alterar um endpoint de webhooks
URL, eventos, descrição, ou ligar e desligar. If-Match opcional.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"url": "https://erp.example.pt/selfcare/webhooks",
"description": "string",
"events": [
"absence.created"
],
"status": "ACTIVE",
"failingSince": "2026-10-16T09:30:00.000Z",
"disabledAt": "2026-10-16T09:30:00.000Z",
"disabledReason": "string",
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
},
"error": null
}Erros: 400, 401, 402, 403, 404, 409, 412, 422, 429, 500
delete/v1/webhook-endpoints/{id}
Apagar um endpoint de webhooks
Apaga também o registo de entregas dele.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
| If-Match | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"deleted": true
},
"error": null
}Erros: 401, 402, 403, 404, 409, 412, 422, 429, 500
post/v1/webhook-endpoints/{id}/rotate-secret
Rodar o segredo
Devolve o segredo novo, uma vez. Durante 24 horas cada entrega leva duas assinaturas v1 (a nova e a anterior), para trocares sem perder nada.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Resposta · Exemplo
{
"data": {
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"url": "https://erp.example.pt/selfcare/webhooks",
"description": "string",
"events": [
"absence.created"
],
"status": "ACTIVE",
"failingSince": "2026-10-16T09:30:00.000Z",
"disabledAt": "2026-10-16T09:30:00.000Z",
"disabledReason": "string",
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z",
"secret": "whsec_…"
},
"error": null
}Erros: 401, 402, 403, 404, 409, 422, 429, 500, 503
post/v1/webhook-endpoints/{id}/test
Enviar um evento de teste
Manda um webhook.test só a este endpoint (tem de estar ligado; senão 409) e devolve a entrega.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Erros: 201, 401, 402, 403, 404, 409, 422, 429, 500
get/v1/webhook-endpoints/{id}/deliveries
Registo de entregas
Os últimos 30 dias: estado, tentativas, código e primeiros 2 KB da resposta, duração.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| limit | query | não | integer |
| cursor | query | não | string |
Resposta · Exemplo
{
"data": [
{
"id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"endpointId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"eventId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
"eventType": "absence.created",
"status": "PENDING",
"attempts": 0,
"lastAttemptAt": "2026-10-16T09:30:00.000Z",
"nextAttemptAt": "2026-10-16T09:30:00.000Z",
"responseStatus": 0,
"responseBodyExcerpt": "string",
"durationMs": 0,
"error": "string",
"createdAt": "2026-10-16T09:30:00.000Z",
"updatedAt": "2026-10-16T09:30:00.000Z"
}
],
"error": null,
"meta": {
"nextCursor": "string",
"hasMore": true
}
}Erros: 400, 401, 402, 403, 404, 422, 429, 500
post/v1/webhook-deliveries/{id}/redeliver
Reenviar uma entrega
Cria uma entrega nova do mesmo evento (o mesmo id: deduplica). A visibilidade volta a ser verificada.
Âmbito: webhooks:manage.
Parâmetros
| Nome | Onde | Obrigatório | |
|---|
| id | path | sim | uuid |
| Idempotency-Key | header | não | string |
Erros: 201, 401, 402, 403, 404, 409, 422, 429, 500