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

Início rápido

1. Na app, abre Definições → API e cria uma chave com os âmbitos de que precisas (pedimos um código por email). O segredo aparece uma só vez: guarda-o num cofre.

2. Faz o primeiro pedido. Todas as rotas vivem em https://api.selfcare.work/v1 e respondem em JSON com data, error e, nas listas, meta.

curl https://api.selfcare.work/v1/me \
  -H "Authorization: Bearer sc_live_…"

3. Em TypeScript, o SDK (@selfcare/api-sdk, gerado da especificação OpenAPI) trata da autenticação, das repetições e da paginação. O pacote ainda não está publicado no npm; até lá, usa a especificação OpenAPI com o gerador da tua preferência.

import { SelfcareClient } from "@selfcare/api-sdk";

const api = new SelfcareClient({ token: process.env.SELFCARE_API_KEY! });

const me = await api.getMe();
console.log(me.tenant.name, me.role, me.credential.scopes);

// Every approved absence this month, page after page
for await (const a of api.paginate((cursor) =>
  api.listAbsences({ cursor, query: { from: "2026-11-01", to: "2026-11-30", status: "APPROVED" } }),
)) {
  console.log(a.employee.fullName, a.startDate, a.endDate);
}

Nunca sai pela API: o processamento salarial, os atestados e outros documentos de saúde, os documentos disciplinares e o IBAN completo (sempre mascarado).

Chaves de API

Cada pedido leva Authorization: Bearer seguido da chave (sc_live_… nas empresas reais, sc_test_… na empresa de teste). A chave pertence a uma pessoa numa empresa: a empresa vem sempre da chave e o papel recalcula-se em cada pedido, por isso a chave vê o que a pessoa vê hoje na app.

Mudar ou repor a palavra-passe revoga as chaves dessa pessoa. O dono e o RH veem e revogam as chaves de toda a empresa em Definições → Sistema → API da empresa, e podem bloquear a API inteira numa emergência. O cookie da app nunca é aceite. Para integrações de servidor sem pessoa associada, o dono ou o RH criam um utilizador de integração, que não entra na app.

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() });

Âmbitos

O acesso é a interseção entre os âmbitos da credencial e as permissões da pessoa na app: um âmbito nunca dá mais do que o papel. Os âmbitos sensíveis (contratos, remuneração, IBAN) marcam-se um a um, nunca entram nos atalhos, e uma chave com eles vale no máximo 90 dias. As escritas de contratos e remuneração são só para o dono e o RH.

ÂmbitoO que permite
me:readQuem sou: utilizador, empresa e papel
absences:readVer ausências, férias e saldos
absences:writeCriar, editar e cancelar ausências
absences:decideAprovar e rejeitar ausências
expenses:readVer despesas e anexos
expenses:writeCriar, editar e submeter despesas
expenses:decideAprovar e rejeitar despesas
trips:readVer deslocações (ajudas de custo)
trips:writeRegistar e cancelar deslocações
trips:decideAprovar e rejeitar deslocações
documents:readVer e descarregar documentos (nunca os de saúde)
documents:writeCarregar documentos e recibos
employees:readVer colaboradores
employees:writeCriar e editar colaboradores
teams:readVer equipas
teams:writeGerir equipas
webhooks:manageGerir webhooks
contracts:readsensívelVer contratos
contracts:writesensívelAlterar contratos
compensation:readsensívelVer remunerações
compensation:writesensívelAlterar remunerações
bank-details:writesensívelPedir mudanças de IBAN

Paginação

As listas devolvem meta.nextCursor e meta.hasMore. Para a página seguinte, repete o pedido com cursor igual ao nextCursor e os mesmos filtros (um cursor com outros filtros dá 400 INVALID_CURSOR). limit vale 50 por omissão e vai até 200. Para sincronizar só o que mudou, usa updatedSince.

{
  "data": [ … ],
  "error": null,
  "meta": { "nextCursor": "eyJj…", "hasMore": true }
}

Erros

Os erros vêm em error, com um code estável, a mensagem na língua do Accept-Language (PT por omissão, ou EN), details por campo nos erros de validação, o requestId (também no cabeçalho Selfcare-Request-Id) e o docUrl desta página. Um recurso que não podes ver responde 404, nunca 403, para não revelar que existe.

HTTP/1.1 403 Forbidden
Selfcare-Request-Id: 6f0e…

{
  "data": null,
  "error": {
    "code": "SCOPE_MISSING",
    "message": "A credencial não tem o âmbito necessário: absences:write.",
    "requestId": "6f0e…",
    "docUrl": "https://api.selfcare.work/docs#error-scope-missing"
  }
}
CódigoEstadoMensagem
BAD_REQUEST400O pedido está mal formado.
INVALID_CURSOR400O cursor de paginação é inválido ou não corresponde a estes filtros.
UNAUTHORIZED401É preciso uma chave de API ou um token válido no cabeçalho Authorization: Bearer.
PAYMENT_REQUIRED402A conta da empresa está bloqueada por falta de pagamento.
FORBIDDEN403Não tens permissão para fazer isto.
SCOPE_MISSING403A credencial não tem o âmbito necessário: ….
API_BLOCKED403A API está bloqueada nesta empresa.
NOT_FOUND404Não encontrado.
CONFLICT409O recurso está num estado que não permite esta operação, ou um pedido igual ainda está a correr.
CONFIRMATION_PENDING409A alteração fica pendente até o colaborador a confirmar na app.
PRECONDITION_FAILED412O recurso mudou desde que o leste (If-Match não corresponde ao ETag actual). Lê-o de novo.
PRECONDITION_REQUIRED428Esta escrita exige o cabeçalho indicado nos detalhes (If-Match com o ETag actual, ou Idempotency-Key).
VALIDATION_ERROR422Há campos inválidos no pedido.
IDEMPOTENCY_CONFLICT422Esta Idempotency-Key já foi usada com um pedido diferente.
CHECKSUM_MISMATCH422O sha256 do ficheiro recebido não corresponde ao declarado.
RATE_LIMITED429Demasiados pedidos. Tenta de novo daqui a pouco.
INTERNAL_ERROR500Ocorreu um erro inesperado.
SERVICE_UNAVAILABLE503Serviço temporariamente indisponível.

Limites

Por credencial: 300 pedidos por minuto, dos quais 60 escritas. Por empresa, somando todas as credenciais: 1200 por minuto. Por app OAuth, em todas as empresas: 3000 por minuto. Antes de validar a credencial, há ainda 600 pedidos por minuto por IP. As respostas trazem RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset; um 429 traz Retry-After em segundos. Espera esse tempo antes de repetir.

Idempotency-Key

Qualquer escrita (POST, PATCH, PUT, DELETE) aceita Idempotency-Key: um valor teu, único por operação (um UUID serve). Durante 24 horas, repetir o pedido com a mesma chave e o mesmo corpo devolve a mesma resposta sem repetir a ação, com o cabeçalho Idempotent-Replayed: true. A mesma chave com outro corpo dá 422 IDEMPOTENCY_CONFLICT; um pedido igual ainda a correr dá 409 CONFLICT. Nas escritas sensíveis (contratos e remuneração), a chave é obrigatória (428).

curl -X POST https://api.selfcare.work/v1/absences \
  -H "Authorization: Bearer sc_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2f6c1a52-1d7e-4b0c-9a51-0c3f0b5b8e11" \
  -d '{ "absenceTypeCode": "FERIAS", "startDate": "2026-12-21", "endDate": "2026-12-24" }'

# Same key, same body, within 24 h: the same answer, with "Idempotent-Replayed: true".
# Same key, another body: 422 IDEMPOTENCY_CONFLICT. Still running: 409 CONFLICT.

Para editar sem pisar uma alteração de outra pessoa, usa o ETag que vem nas leituras e nas escritas: manda-o em If-Match. Se o recurso mudou entretanto, a resposta é 412 PRECONDITION_FAILED. Nas edições sensíveis, If-Match é obrigatório.

curl -i https://api.selfcare.work/v1/absences/3f1c…   # ETag: W/"…"
curl -X PATCH https://api.selfcare.work/v1/absences/3f1c… \
  -H "Authorization: Bearer sc_live_…" -H "If-Match: W/\"…\"" \
  -H "Content-Type: application/json" -d '{ "endDate": "2026-12-23" }'
# If the absence changed in the meantime: 412 PRECONDITION_FAILED

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.

Empresa de teste

Para desenvolver sem tocar em dados reais, cria uma empresa de teste em Definições → API: vem com colaboradores, duas equipas, ausências e despesas fictícios. Nessa empresa as chaves começam por sc_test_ e só funcionam lá; as sc_live_ só funcionam nas empresas reais. Os tokens OAuth servem a empresa escolhida no consentimento, seja de teste ou real.

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ÂmbitoTipo
get_meme:readLeitura
get_my_summaryme:readLeitura
get_vacation_balanceabsences:readLeitura
list_absence_typesabsences:readLeitura
list_absencesabsences:readLeitura
request_absenceabsences:writeEscrita
cancel_absenceabsences:writeEscrita
get_absenceabsences:readLeitura
list_pending_approvalsabsences:readLeitura
approve_absenceabsences:decideEscrita
reject_absenceabsences:decideEscrita
list_expensesexpenses:readLeitura
get_expenseexpenses:readLeitura
list_expense_categoriesexpenses:readLeitura
create_expenseexpenses:writeEscrita
get_expense_receipt_upload_urlexpenses:writeEscrita
add_expense_receiptexpenses:writeEscrita
submit_expenseexpenses:writeEscrita
approve_expenseexpenses:decideEscrita
reject_expenseexpenses:decideEscrita
list_documentsdocuments:readLeitura
get_document_linkdocuments:readLeitura
search_employeesemployees:readLeitura

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.

Versões

A versão vai no caminho (/v1). Dentro de /v1 só entram mudanças compatíveis: campos, rotas, eventos e valores novos. Ignora os campos que não conheces e trata valores de enumeração novos com um caso por omissão. Uma mudança incompatível só numa /v2, anunciada com antecedência nesta página.

Perguntas frequentes

A API está incluída no meu plano?
Sim, em todos os planos, sem custo extra. Com a conta bloqueada por falta de pagamento, a API responde 402, como o resto da app.
Posso ler os salários ou os recibos de vencimento?
O processamento salarial fica fora da API e do MCP. Pela API REST, os recibos de vencimento já emitidos são documentos: cada pessoa vê os seus (e o RH os de quem gere), como na app. Pelo MCP, recibos, contratos e documentos de identificação ou de IBAN nunca saem. Contratos e remuneração (âmbitos sensíveis) só para quem os vê na app.
Porque é que recebo 404 num registo que existe?
Porque a pessoa da credencial não o vê na app. A API responde 404 em vez de 403 para não revelar que o registo existe.
Como vejo o IBAN de um colaborador?
Não vês: sai sempre mascarado (****0154). Podes registar o primeiro IBAN de uma ficha; alterar um existente fica pendente até o colaborador confirmar na app.
Uma chave serve para várias empresas?
Não. Cada chave é de uma pessoa numa empresa. Para várias empresas, cria uma chave em cada uma ou usa OAuth, em que a pessoa escolhe a empresa ao autorizar.
Onde ficam os dados?
Na União Europeia. A API é servida a partir do mesmo sistema da app, com o mesmo isolamento por empresa.
Como sei quem fez o quê pela API?
Cada escrita fica no registo de auditoria da empresa com a origem (API ou MCP), a chave ou a app e o id do pedido. Cada pessoa vê o registo de acessos das suas chaves em Definições → API.
Os contabilistas podem usar a API?
Podem usar chaves com o que o papel deles permite na app. O servidor MCP não está disponível para contabilistas.
Encontrei um problema ou preciso de um endpoint novo.
Escreve para suporte@selfcare.work com o requestId da resposta.

Alterações

Mudanças na API v1. Acrescentar campos, endpoints ou eventos não muda de versão; remover ou mudar o significado de algo só numa v2, com aviso prévio.

  • 2026-10-19Guias completos no portal (Idempotency-Key, webhooks com verificação em código, MCP passo a passo, app móvel, perguntas frequentes). A especificação corrige os campos anuláveis que apontam para um esquema (Document.currentVersion, o IBAN pendente e o saldo do resumo passam a anyOf com null) e passa no redocly lint sem erros nem avisos.
  • 2026-10-19Servidor MCP em https://api.selfcare.work/mcp (Streamable HTTP), com registo dinâmico de clientes (/oauth/register, RFC 7591), metadados do recurso protegido (RFC 9728) e tokens presos ao recurso (RFC 8707). GET /v1/employees aceita q para procurar por nome.
  • 2026-10-19OAuth 2.1 para apps usadas por várias pessoas: authorization code com PKCE S256, /oauth/token, /oauth/revoke e /.well-known/oauth-authorization-server. App móvel: GET/POST /v1/me/devices, DELETE /v1/me/devices/{id}, notificações push e GET /v1/me/summary. SDK TypeScript gerado da especificação.
  • 2026-10-18Webhooks assinados (/v1/webhook-endpoints, entregas, reenvio, teste e rotação do segredo). Escritas: ausências, despesas, deslocações, documentos, colaboradores, equipas, contratos, remuneração e IBAN (a alteração fica pendente até o colaborador confirmar). Idempotency-Key em todas as escritas e If-Match nas edições.
  • 2026-10-17Primeira versão: chaves pessoais (sc_live_, sc_test_), âmbitos, limites, paginação por cursor, erros estáveis e leituras de ausências, aprovações, despesas, deslocações, documentos, colaboradores e equipas.

Ausências e férias

get/v1/absence-types

Tipos de ausência da empresa

Âmbito: absences:read.

Parâmetros

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
fromquerynãodate
toquerynãodate
statusquerynãostring
employeeIdquerynãouuid
teamIdquerynãouuid
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500

get/v1/absences/{id}

Uma ausência

Âmbito: absences:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
yearquerynãointeger

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

Aprovações

get/v1/approvals

A caixa de Aprovações do utilizador

O que o utilizador tem para decidir, como na app. Despesas, deslocações e documentos só aparecem se a credencial tiver expenses:read, trips:read e documents:read.

Âmbito: absences:read.

Parâmetros

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

Resposta · Exemplo

{
  "data": [
    {
      "id": "string",
      "kind": "absence",
      "resourceId": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "employee": {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string"
      },
      "summary": "string",
      "viaTeams": [
        "string"
      ],
      "createdAt": "2026-10-16T09:30:00.000Z"
    }
  ],
  "error": null,
  "meta": {
    "nextCursor": "string",
    "hasMore": true
  }
}

Erros: 400, 401, 402, 403, 422, 429, 500

Eu

get/v1/me/devices

Os meus dispositivos

Os telefones de quem pede registados para notificações push, nesta empresa. O token de push nunca volta.

Âmbito: me:read.

Parâmetros

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

Resposta · Exemplo

{
  "data": [
    {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "platform": "ios",
      "locale": "pt",
      "appVersion": "string",
      "app": {
        "clientId": "string",
        "name": "string"
      },
      "lastSeenAt": "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/me/devices

Registar um dispositivo para push

Só com um token OAuth: o dispositivo fica preso a essa sessão, e terminar a sessão (revogar o refresh token) ou revogar a app desliga-o. Chame-o em cada arranque da app: o mesmo token de push actualiza o registo. O token guarda-se cifrado; sem cifra configurada, 503. Um token de push que passe para outra pessoa deixa de receber os avisos da anterior.

Âmbito: me:read.

Parâmetros

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500, 503

delete/v1/me/devices/{id}

Apagar um dispositivo

Deixa de receber push. O dispositivo de outra pessoa é 404.

Âmbito: me:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "deleted": true
  },
  "error": null
}

Erros: 401, 402, 403, 404, 409, 422, 429, 500

get/v1/me/summary

Resumo para o ecrã inicial

Saldo de férias, próximas ausências, pedidos à espera da minha decisão, documentos para ler, IBAN por confirmar e avisos por ler, numa só chamada. Cada bloco segue o âmbito do endpoint que o lê por inteiro e vem null sem ele.

Âmbito: me:read.

Resposta · Exemplo

{
  "data": {
    "vacationBalance": {
      "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"
    },
    "upcomingAbsences": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "startDate": "string",
        "endDate": "string",
        "status": "string",
        "workingDays": 0
      }
    ],
    "approvalsPending": 0,
    "documentsToAcknowledge": 0,
    "bankDetailsConfirmationPending": true,
    "unreadNotifications": 0
  },
  "error": null
}

Erros: 401, 402, 403, 429, 500

get/v1/me

Quem é a credencial

Utilizador, empresa (a da credencial), papel, ficha, equipas que lidera e âmbitos.

Âmbito: me:read.

Resposta · Exemplo

{
  "data": {
    "user": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "email": "string",
      "name": "string"
    },
    "tenant": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "name": "string",
      "slug": "string",
      "timeZone": "string",
      "sandbox": true
    },
    "role": "HR_ADMIN",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "preferredName": "string",
      "employeeNumber": "string",
      "jobTitle": "string",
      "photoUrl": "string",
      "status": "ACTIVE",
      "kind": "string",
      "workEmail": "string",
      "teams": [
        {
          "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
          "name": "string"
        }
      ]
    },
    "ledTeams": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "name": "string"
      }
    ],
    "credential": {
      "type": "api_key",
      "scopes": [
        "me:read"
      ],
      "environment": "LIVE"
    }
  },
  "error": null
}

Erros: 401, 402, 403, 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

NomeOndeObrigatório
statusquerynãostring
employeeIdquerynãouuid
fromquerynãodate
toquerynãodate
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 404, 409, 422, 429, 500

get/v1/expenses/{id}

Uma despesa

Âmbito: expenses:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
attachmentIdpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
attachmentIdpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
attachmentIdpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

Ajudas de custo

get/v1/business-trips

Deslocações (ajudas de custo) que o utilizador vê na app

Lista vazia com o módulo desligado.

Âmbito: trips:read.

Parâmetros

NomeOndeObrigatório
statusquerynãostring
employeeIdquerynãouuid
fromquerynãodate
toquerynãodate
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

Resposta · Exemplo

{
  "data": [
    {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "employee": {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string"
      },
      "destination": "string",
      "purpose": "string",
      "departureAt": "2026-10-05T09:00",
      "returnAt": "2026-10-06T18:30",
      "abroad": true,
      "status": "SUBMITTED",
      "mode": "string",
      "nights": 0,
      "legalPercent": 0,
      "currency": "EUR",
      "amountCents": 0,
      "exemptAmountCents": 0,
      "taxableAmountCents": 0,
      "submittedAt": "2026-10-16T09:30:00.000Z",
      "decidedAt": "2026-10-16T09:30:00.000Z",
      "decidedByName": "string",
      "rejectionNote": "string",
      "paidAt": "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/business-trips

Registar uma deslocação

Do próprio. Registar é submeter; o valor sai da política da empresa ou à mão.

Âmbito: trips:write.

Parâmetros

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500

get/v1/business-trips/{id}

Uma deslocação

Âmbito: trips:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "destination": "string",
    "purpose": "string",
    "departureAt": "2026-10-05T09:00",
    "returnAt": "2026-10-06T18:30",
    "abroad": true,
    "status": "SUBMITTED",
    "mode": "string",
    "nights": 0,
    "legalPercent": 0,
    "currency": "EUR",
    "amountCents": 0,
    "exemptAmountCents": 0,
    "taxableAmountCents": 0,
    "submittedAt": "2026-10-16T09:30:00.000Z",
    "decidedAt": "2026-10-16T09:30:00.000Z",
    "decidedByName": "string",
    "rejectionNote": "string",
    "paidAt": "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/business-trips/{id}

Corrigir uma deslocação

Ainda por decidir, ou rejeitada (volta a ser submetida). Aceita If-Match.

Âmbito: trips:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "destination": "string",
    "purpose": "string",
    "departureAt": "2026-10-05T09:00",
    "returnAt": "2026-10-06T18:30",
    "abroad": true,
    "status": "SUBMITTED",
    "mode": "string",
    "nights": 0,
    "legalPercent": 0,
    "currency": "EUR",
    "amountCents": 0,
    "exemptAmountCents": 0,
    "taxableAmountCents": 0,
    "submittedAt": "2026-10-16T09:30:00.000Z",
    "decidedAt": "2026-10-16T09:30:00.000Z",
    "decidedByName": "string",
    "rejectionNote": "string",
    "paidAt": "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

post/v1/business-trips/{id}/cancel

Cancelar uma deslocação

Âmbito: trips:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "destination": "string",
    "purpose": "string",
    "departureAt": "2026-10-05T09:00",
    "returnAt": "2026-10-06T18:30",
    "abroad": true,
    "status": "SUBMITTED",
    "mode": "string",
    "nights": 0,
    "legalPercent": 0,
    "currency": "EUR",
    "amountCents": 0,
    "exemptAmountCents": 0,
    "taxableAmountCents": 0,
    "submittedAt": "2026-10-16T09:30:00.000Z",
    "decidedAt": "2026-10-16T09:30:00.000Z",
    "decidedByName": "string",
    "rejectionNote": "string",
    "paidAt": "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

post/v1/business-trips/{id}/approve

Aprovar uma deslocação

Âmbito: trips:decide.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "destination": "string",
    "purpose": "string",
    "departureAt": "2026-10-05T09:00",
    "returnAt": "2026-10-06T18:30",
    "abroad": true,
    "status": "SUBMITTED",
    "mode": "string",
    "nights": 0,
    "legalPercent": 0,
    "currency": "EUR",
    "amountCents": 0,
    "exemptAmountCents": 0,
    "taxableAmountCents": 0,
    "submittedAt": "2026-10-16T09:30:00.000Z",
    "decidedAt": "2026-10-16T09:30:00.000Z",
    "decidedByName": "string",
    "rejectionNote": "string",
    "paidAt": "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

post/v1/business-trips/{id}/reject

Rejeitar uma deslocação

Com a nota obrigatória.

Âmbito: trips:decide.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "employee": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "destination": "string",
    "purpose": "string",
    "departureAt": "2026-10-05T09:00",
    "returnAt": "2026-10-06T18:30",
    "abroad": true,
    "status": "SUBMITTED",
    "mode": "string",
    "nights": 0,
    "legalPercent": 0,
    "currency": "EUR",
    "amountCents": 0,
    "exemptAmountCents": 0,
    "taxableAmountCents": 0,
    "submittedAt": "2026-10-16T09:30:00.000Z",
    "decidedAt": "2026-10-16T09:30:00.000Z",
    "decidedByName": "string",
    "rejectionNote": "string",
    "paidAt": "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, 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

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
employeeIdquerynãouuid
spaceIdquerynãouuid
typeKeyquerynãostring
periodquerynãostring
expiresBeforequerynãodate
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500

get/v1/documents/{id}

Um documento

Âmbito: documents:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
versionIdpathsimuuid

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

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
qquerynãostring
statusquerynãostring
teamIdquerynãouuid
employeeNumberquerynãostring
nifquerynãostring
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500

get/v1/employees/{id}

Um colaborador

Âmbito: employees:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadersimstring

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

NomeOndeObrigatório
idpathsimuuid
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadersimstring

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

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
contractIdpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadersimstring

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

NomeOndeObrigatório
idpathsimuuid
entryIdpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadersimstring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

Equipas

get/v1/teams

Equipas

Âmbito: teams:read.

Parâmetros

NomeOndeObrigatório
updatedSincequerynãodate-time
limitquerynãointeger
cursorquerynãostring

Resposta · Exemplo

{
  "data": [
    {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "name": "string",
      "description": "string",
      "color": "string",
      "leader": {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string"
      },
      "members": [
        {
          "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
          "fullName": "string",
          "employeeNumber": "string",
          "jobTitle": "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/teams

Criar uma equipa

Dono, RH ou gestor. Uma equipa tem sempre líder, que não tem de ser membro; o líder aprova as ausências de quem está na equipa.

Âmbito: teams:write.

Parâmetros

NomeOndeObrigatório
Idempotency-Keyheadernãostring

Erros: 201, 400, 401, 402, 403, 409, 422, 429, 500

get/v1/teams/{id}

Uma equipa

Âmbito: teams:read.

Parâmetros

NomeOndeObrigatório
idpathsimuuid

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "name": "string",
    "description": "string",
    "color": "string",
    "leader": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "members": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string",
        "jobTitle": "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/teams/{id}

Editar uma equipa

Nome, descrição, cor ou líder. Mudar o líder muda as cadeias de aprovação. If-Match opcional.

Âmbito: teams:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "name": "string",
    "description": "string",
    "color": "string",
    "leader": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "members": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string",
        "jobTitle": "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/teams/{id}

Apagar uma equipa

Como na app: com documentos no espaço da equipa, 409. As pessoas ficam, só deixam de pertencer à equipa.

Âmbito: teams:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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/teams/{id}/members

Juntar uma pessoa a uma equipa

Uma pessoa pode estar em várias equipas. Se já for membro, fica igual. Devolve a equipa.

Âmbito: teams:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "name": "string",
    "description": "string",
    "color": "string",
    "leader": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "members": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string",
        "jobTitle": "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

delete/v1/teams/{id}/members/{employeeId}

Tirar uma pessoa de uma equipa

Quem não é membro dá 404. Devolve a equipa.

Âmbito: teams:write.

Parâmetros

NomeOndeObrigatório
idpathsimuuid
employeeIdpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

Resposta · Exemplo

{
  "data": {
    "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
    "name": "string",
    "description": "string",
    "color": "string",
    "leader": {
      "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
      "fullName": "string",
      "employeeNumber": "string"
    },
    "members": [
      {
        "id": "3f1c2b8e-5d0a-4c1e-9f8a-2b7d6e5c4a10",
        "fullName": "string",
        "employeeNumber": "string",
        "jobTitle": "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

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

NomeOndeObrigatório
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring
If-Matchheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

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

NomeOndeObrigatório
idpathsimuuid
limitquerynãointeger
cursorquerynãostring

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

NomeOndeObrigatório
idpathsimuuid
Idempotency-Keyheadernãostring

Erros: 201, 401, 402, 403, 404, 409, 422, 429, 500

API do Selfcare