API Pública

A API pública do Associados.app permite que sistemas externos — como um software de ponto de venda (POS) — consultem sócios e liquidem quotas em nome da sua associação. É uma API REST sobre HTTPS, com JSON em ambos os sentidos e autenticação por chave.

URL base

https://associados.app/api/public/v1

Como começar

A API pública está disponível no plano Profissional. Nos restantes planos as chaves não podem ser criadas e os pedidos são recusados com 403 plan_required.

Há duas formas de obter uma chave:

  • Integrações próprias — um administrador cria a chave em Definições → API, escolhendo o nome, as permissões e (se quiser) uma validade.
  • Aplicações parceiras — se está a ligar uma aplicação já integrada com o Associados.app, use o fluxo de ligação. A chave é emitida automaticamente após o administrador dar consentimento, e aparece na mesma lista.

Uma chave dá acesso aos dados de uma única associação — a que a emitiu. Não é preciso identificar a associação nos pedidos: ela é determinada pela chave.

Autenticação

Todos os pedidos exigem uma chave de API, no cabeçalho Authorization ou x-api-key. As chaves começam por assoc_live_.

curl https://associados.app/api/public/v1/associates?search=silva \
  -H "Authorization: Bearer assoc_live_a1b2c3..."

# em alternativa
curl https://associados.app/api/public/v1/associates?search=silva \
  -H "x-api-key: assoc_live_a1b2c3..."

A chave é apresentada uma única vez, no momento em que é criada — guardamos apenas um hash, pelo que não é recuperável depois. Se a perder, é preciso emitir uma nova. Trate-a como uma palavra-passe: mantenha-a no servidor, nunca no código de uma aplicação cliente ou no navegador.

Uma chave pode ser revogada a qualquer momento em Definições → API, com efeito imediato no pedido seguinte. É o que deve fazer se suspeitar que a chave foi exposta.

Um pedido sem chave, ou com uma chave desconhecida, revogada ou expirada, devolve 401 unauthorized.

Âmbitos

Cada chave tem um conjunto de âmbitos (scopes) que delimita o que pode fazer. Um pedido a um endpoint fora dos âmbitos da chave devolve 403 forbidden.

ÂmbitoPermite
associates:readConsultar sócios, fotografias e quotas em dívida
quotas:readConsultar campanhas de pagamento
quotas:writeLiquidar quotas e reverter liquidações

Erros

Os erros usam sempre o mesmo formato. Programe contra o campo code, que é estável — a message é destinada a humanos e pode mudar.

{
  "error": {
    "code": "validation_error",
    "message": "Dados inválidos",
    "issues": [ /* detalhe por campo, apenas em erros de validação */ ]
  }
}
CódigoHTTPSignificado
unauthorized401Chave em falta, inválida, revogada ou expirada
forbidden403A chave não tem o âmbito necessário
plan_required403A associação não está no plano Profissional (ou deixou de estar)
rate_limited429Excedeu o limite de pedidos; ver Limites
bad_request400Corpo JSON malformado
validation_error422Corpo válido mas com campos incorretos (ver issues)
not_found404O recurso não existe nesta associação
no_picture404O sócio não tem fotografia — só em /avatar
unsupported_media415A fotografia guardada não está num formato que se consiga processar — só em /avatar
settlement_in_progress409Já há uma liquidação a decorrer para esta referência; repita daqui a instantes
internal_error500Erro do nosso lado; pode repetir o pedido

Limites

Os pedidos são contados por chave, numa janela de um minuto. Os limites diferem consoante o custo do endpoint:

EndpointsPor minuto
Leituras (sócios, quotas, campanhas)300
Fotografias (/avatar)120
Escritas (mark-paid, unmark-paid)60

As respostas bem-sucedidas — e os erros 403 e 429 — trazem X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset (epoch em segundos), para que possa abrandar antes de ser cortado. Um 401 não os traz: sem chave válida não há contador a reportar. Ao exceder o limite recebe 429 rate_limited com Retry-After em segundos — respeite-o.

Um pedido recusado conta na mesma para o limite. Um 403 por falta de permissão não é gratuito: se insistir num endpoint que a chave não pode usar, acabará por receber 429.

Ao repetir um pagamento após um 429, reutilize sempre a mesma externalReference. Um 429 não diz que o pagamento falhou — pode ter sido apenas recusado à entrada. Reutilizar a referência deixa a idempotência resolver o resto. Espere o Retry-After antes de tentar: repetir de imediato pode colidir com o seu próprio pedido ainda em curso e devolver 409 settlement_in_progress.

Sócios

GET/associatesassociates:read

Pesquisa sócios por nome, número, NIF ou telefone. Sem search, devolve todos os sócios paginados.

Parâmetros: search (texto livre), page (por omissão 1), pageSize (por omissão 25, máximo 100).

GET /associates?search=silva&page=1&pageSize=25

{
  "data": [
    {
      "id": "clx7a2b...",
      "associateNumber": 142,
      "name": "Maria Silva",
      "status": "ACTIVE",
      "hasPicture": true
    }
  ],
  "page": 1,
  "pageSize": 25,
  "total": 1,
  "totalPages": 1
}

A fotografia não vem no corpo da listagem. Use hasPicture para saber se vale a pena pedir o avatar.

GET/associates/{id}/avatarassociates:read

A fotografia do sócio, redimensionada para uma miniatura quadrada em image/webp (poucos KB). Devolve a imagem em bruto, não JSON.

Parâmetro size — o lado do quadrado em pixels; por omissão 64, limitado ao intervalo 16–256.

GET /associates/clx7a2b.../avatar?size=64
→ 200 image/webp

Devolve 404 no_picture se o sócio não tiver fotografia, e 415 unsupported_media se a fotografia guardada não for legível. A resposta traz Cache-Control: private — é a fotografia de um sócio, não a coloque em caches partilhadas.

GET/associates/{id}/payable-quotasassociates:read

As quotas por liquidar de um sócio — quotas periódicas e pagamentos pontuais — com o valor em dívida de cada uma. É o que precisa para montar um ecrã de pagamento.

GET /associates/clx7a2b.../payable-quotas

{
  "associate": {
    "id": "clx7a2b...",
    "name": "Maria Silva",
    "associateNumber": 142,
    "status": "ACTIVE"
  },
  "quotas": [
    {
      "id": "clx9f4d...",
      "kind": "RECURRING",
      "label": null,
      "referenceDate": "2026-01-01T00:00:00.000Z",
      "dueDate": "2026-01-31T00:00:00.000Z",
      "amount": 10,
      "status": "OVERDUE"
    },
    {
      "id": "clxa1e7...",
      "kind": "ONE_OFF",
      "label": "Jantar de aniversário",
      "referenceDate": "2026-03-01T00:00:00.000Z",
      "dueDate": "2026-03-15T00:00:00.000Z",
      "amount": 25,
      "status": "PENDING"
    }
  ]
}

kind é RECURRING (quota periódica) ou ONE_OFF (pagamento pontual, com label preenchida). status é PENDING ou OVERDUE — quotas já pagas não aparecem aqui.

Quotas e pagamentos

POST/quotas/mark-paidquotas:write

Marca uma ou mais quotas como pagas, registando a transação e emitindo fatura se a associação tiver faturação ativa.

externalReference é a sua chave de idempotência — veja Idempotência. paymentMethod é texto livre e aparece no recibo.

POST /quotas/mark-paid
{
  "quotaIds": ["clx9f4d...", "clxa1e7..."],
  "paymentMethod": "POS — Multibanco",
  "externalReference": "order-item-8891"
}

{
  "idempotent": false,
  "status": "completed",
  "paid": 2,
  "quotaIds": ["clx9f4d...", "clxa1e7..."],
  "externalReference": "order-item-8891"
}

O quotaIds da resposta são as quotas que este pedido realmente liquidou — pode ser um subconjunto do que enviou, porque as quotas que já tinham sido pagas entretanto (por um administrador, por exemplo) são ignoradas. paid conta essas mesmas quotas, pelo que paid pode ser inferior ao número de ids enviados, ou até 0 se nenhuma estava por pagar. Isso não é um erro: o resultado pretendido — as quotas estão pagas — verifica-se à mesma.

POST/quotas/unmark-paidquotas:write

Reverte uma liquidação anterior — o caso típico é um reembolso ou anulação no POS. Identifica-se pela externalReference original, não por quotas.

POST /quotas/unmark-paid
{ "externalReference": "order-item-8891" }

{
  "reversed": true,
  "alreadyReversed": false,
  "quotaIds": ["clx9f4d...", "clxa1e7..."]
}

A reversão só toca nas quotas que aquela liquidação pagou: apaga as transações que criou e devolve as quotas a PENDING ou OVERDUE. Pagamentos registados por outra via ficam intactos.

É idempotente — repetir devolve alreadyReversed: true sem voltar a mexer nas quotas. Uma referência desconhecida devolve 404 not_found.

GET/payment-requestsquotas:read

Lista as campanhas de pagamento pontuais da associação, com totais de pagos e pendentes. Útil para sincronizar o catálogo do sistema externo.

GET /payment-requests

{
  "data": [
    {
      "id": "clxb3c9...",
      "name": "Jantar de aniversário",
      "amount": 25,
      "total": 80,
      "paid": 31,
      "pending": 49,
      "createdAt": "2026-02-10T09:12:00.000Z"
    }
  ]
}

amount é o valor a pagar por sócio, em euros. Já total, paid e pending são contagens de sócios, não valores: no exemplo acima, a campanha abrange 80 sócios, dos quais 31 já pagaram e 49 estão por pagar. Sócios cuja quota foi dispensada não entram no pending, pelo que paid + pending pode ser inferior a total.

Idempotência

Pagamentos não podem ser cobrados duas vezes por causa de uma rede instável. Por isso /quotas/mark-paid exige uma externalReference: um identificador estável e único do seu lado para aquele pagamento — tipicamente o id da linha de encomenda. Não use um valor aleatório gerado a cada tentativa, ou a proteção deixa de funcionar.

Com essa referência, pode repetir o pedido à vontade:

  • Se a liquidação já tinha terminado, a resposta original é devolvida tal e qual, com idempotent: true. Nada é pago de novo.
  • Se ainda estiver a decorrer (o seu pedido anterior ainda não respondeu), recebe 409 settlement_in_progress. Aguarde um momento e repita.
  • Se a tentativa anterior falhou a meio, a repetição liquida normalmente.

Perante um timeout ou um 500, a atitude correta é repetir o pedido com a mesma referência — nunca com uma nova.

Webhooks

Em vez de sondar a API, pode receber um POST quando algo acontece na associação. O endpoint é registado no fluxo de ligação, que devolve também o segredo de assinatura (começa por whsec_).

EventoQuando
payment_request.createdFoi criada uma campanha de pagamento pontual
payment_request.closedUma campanha de pagamento foi encerrada
quota.paidUma quota foi liquidada, seja qual for a origem

quota.paid inclui source (admin, api ou stripe) e externalReference, para que possa distinguir os pagamentos que o seu sistema registou dos restantes e ignorar o eco dos seus próprios lançamentos. Os restantes campos são quotaId, associateId, transactionId, amount, kind, paymentMethod, receiptNumber e paidAt.

O pedido

POST https://o-seu-servidor.pt/webhooks/associados
X-Webhook-Event: payment_request.created
X-Webhook-Delivery: clxd8e2...
X-Webhook-Signature: sha256=3f9a1c...

{
  "id": "clxd8e2...",
  "event": "payment_request.created",
  "createdAt": "2026-02-10T09:12:00.000Z",
  "data": { /* específico do evento */ }
}

Verificar a assinatura

X-Webhook-Signature é sha256= seguido do HMAC-SHA256 do corpo em bruto do pedido, com o seu segredo como chave. Calcule o HMAC antes de fazer parse do JSON — se reserializar o objeto, a assinatura deixa de bater certo. Rejeite qualquer pedido cuja assinatura não confira.

import crypto from "crypto";

const esperado =
  "sha256=" +
  crypto.createHmac("sha256", WEBHOOK_SECRET).update(corpoEmBruto).digest("hex");

const valido = crypto.timingSafeEqual(
  Buffer.from(esperado),
  Buffer.from(req.headers["x-webhook-signature"]),
);

Repetições

Responda 2xx para confirmar a receção. Qualquer outra resposta — ou não responder em 10 segundos — conta como falha e o envio é repetido, até 6 tentativas, com espera exponencial a começar em 1 minuto e limitada a 6 horas.

Como há repetições, o mesmo evento pode chegar mais do que uma vez. Use o id da entrega (também em X-Webhook-Delivery) para ignorar duplicados. Responda depressa e processe em segundo plano.

Ligar uma aplicação

As aplicações parceiras obtêm a chave através de um fluxo de consentimento, em vez de a chave ser copiada à mão. É um authorization code simplificado:

  1. A aplicação envia o administrador para /connect/authorize, com client_id, redirect_uri e, opcionalmente, state, scope e webhook_url.
  2. O administrador autentica-se, escolhe a associação a ligar e vê os acessos pedidos. Ao autorizar, é reencaminhado para a redirect_uri com um code de utilização única, válido por 5 minutos.
  3. A aplicação troca esse código, servidor-a-servidor, em POST /api/connect/token, autenticando-se com client_id e client_secret. A resposta traz a chave de API e, se tiver sido pedido um webhook_url, o segredo de assinatura do webhook.
POST /api/connect/token
{
  "code": "ac_...",
  "client_id": "a-sua-app",
  "client_secret": "...",
  "redirect_uri": "https://a-sua-app.pt/callback"
}

{
  "api_key": "assoc_live_...",
  "scopes": "associates:read,quotas:read,quotas:write",
  "tenant": { "id": "...", "name": "Associação Exemplo" },
  "webhook": { "id": "...", "secret": "whsec_...", "events": "..." },
  "base_url": "https://associados.app"
}

A chave nunca viaja num reencaminhamento do navegador — só o código de utilização única — para não ficar em históricos e registos de servidor.

Não há registo dinâmico de clientes: cada aplicação parceira é pré-registada por nós, com o seu segredo e a lista exata de redirect_uri permitidas. Para registar uma aplicação, fale connosco.

Precisa de ajuda a integrar?

Diga-nos o que está a construir e ajudamos a encontrar o caminho mais curto.