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
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.
| Âmbito | Permite |
|---|---|
| associates:read | Consultar sócios, fotografias e quotas em dívida |
| quotas:read | Consultar campanhas de pagamento |
| quotas:write | Liquidar 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ódigo | HTTP | Significado |
|---|---|---|
| unauthorized | 401 | Chave em falta, inválida, revogada ou expirada |
| forbidden | 403 | A chave não tem o âmbito necessário |
| plan_required | 403 | A associação não está no plano Profissional (ou deixou de estar) |
| rate_limited | 429 | Excedeu o limite de pedidos; ver Limites |
| bad_request | 400 | Corpo JSON malformado |
| validation_error | 422 | Corpo válido mas com campos incorretos (ver issues) |
| not_found | 404 | O recurso não existe nesta associação |
| no_picture | 404 | O sócio não tem fotografia — só em /avatar |
| unsupported_media | 415 | A fotografia guardada não está num formato que se consiga processar — só em /avatar |
| settlement_in_progress | 409 | Já há uma liquidação a decorrer para esta referência; repita daqui a instantes |
| internal_error | 500 | Erro 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:
| Endpoints | Por 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.
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
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.
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/webpDevolve 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.
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
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.
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.
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_).
| Evento | Quando |
|---|---|
| payment_request.created | Foi criada uma campanha de pagamento pontual |
| payment_request.closed | Uma campanha de pagamento foi encerrada |
| quota.paid | Uma 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:
- A aplicação envia o administrador para
/connect/authorize, comclient_id,redirect_urie, opcionalmente,state,scopeewebhook_url. - O administrador autentica-se, escolhe a associação a ligar e vê os acessos pedidos. Ao autorizar, é reencaminhado para a
redirect_uricom umcodede utilização única, válido por 5 minutos. - A aplicação troca esse código, servidor-a-servidor, em
POST /api/connect/token, autenticando-se comclient_ideclient_secret. A resposta traz a chave de API e, se tiver sido pedido umwebhook_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.