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
bad_request400O corpo não é JSON
unauthorized401Chave de API em falta, inválida ou revogada
forbidden403A chave não tem a permissão exigida
plan_required403A associação não está no plano Profissional
no_picture404O sócio não tem fotografiaSó em GET /associates/{id}/avatar
not_found404A referência já existiu mas o sócio foi entretanto removido
deceased_associate409O sócio está registado como falecidoSó em POST /associates/{id}/status
quota_already_paid409A quota já está pagaSó em POST /quotas/{id}/waive
quota_config_missing409A associação não tem configuração de quotasSó em POST /quotas/generate
settlement_in_progress409Outra chamada com esta referência está a decorrerSó em POST /quotas/mark-paid
validation_error422Campos inválidos, ou grupo inexistente
rate_limited429Orçamento de pedidos por minuto esgotado
internal_error500A escrita falhou

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.

A chave

GET/mesem permissão exigida300/min

Descrever a chave que fez o pedido

Qual é a associação, o que a chave pode fazer, e quais são os limites por minuto. Não exige permissão nenhuma — é o primeiro pedido que um integrador faz com uma credencial nova.

Resposta 200

tenantobject | null
keyobject
namestring | null
prefixstring | null
scopesstring[]
createdAtdata ISO-8601 | null
expiresAtdata ISO-8601 | null
tierstring
limitsobject
readinteger
avatarinteger
writeinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado

Sócios

GET/associatesassociates:read300/min

Procurar sócios

A lista nunca carrega fotografias: hasPicture diz se vale a pena pedir /associates/{id}/avatar, que as serve já redimensionadas.

Parâmetros

search (opcional)string
page (opcional)string
pageSize (opcional)string

Resposta 200

dataobject[]
idstring
associateNumberinteger | null
namestring
statusstring
hasPictureboolean
pageinteger
pageSizeinteger
totalinteger
totalPagesinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
POST/associatesassociates:write60/min

Inscrever um sócio

Idempotente se lhe for dada uma externalReference, e só nesse caso: repetir a chamada com a mesma referência devolve o sócio já criado, com 200 em vez de 201. Sem referência, uma repetição inscreve um duplicado — uma integração não distingue um pedido que falhou de um que demorou.

Corpo

namestring
email (opcional)string | string
admissionDate (opcional)data ISO-8601
photoUploadId (opcional)string
removePhoto (opcional)boolean
address (opcional)string
postalCode (opcional)string
city (opcional)string
birthDate (opcional)data ISO-8601
cityOfBirth (opcional)string
countryOfBirth (opcional)string
maritalStatus (opcional)"SINGLE" | "MARRIED" | "DIVORCED" | "WIDOWED" | "CIVIL_UNION" | "OTHER"
sex (opcional)"MALE" | "FEMALE" | "OTHER"
nationalIdNumber (opcional)string
nationalIdEmissionDate (opcional)data ISO-8601
nationalIdEntity (opcional)string
nationalIdExpirationDate (opcional)data ISO-8601
fiscalId (opcional)string
occupation (opcional)string
phoneNumber (opcional)string
landlinePhone (opcional)string
customFields (opcional)object
notifOptOutEmail (opcional)boolean
notifOptOutSms (opcional)boolean
groupIdstring | null
externalReference (opcional)string

Resposta 200

idstring
associateNumberinteger | null
namestring
emailstring | null
statusstring
admissionDatedata ISO-8601 | null
birthDatedata ISO-8601 | null
phoneNumberstring | null
landlinePhonestring | null
addressstring | null
postalCodestring | null
citystring | null
fiscalIdstring | null
occupationstring | null
groupIdstring | null
customFieldsqualquer | nullCampos personalizados da associação. A forma é definida por cada associação.
hasPictureboolean
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 Repetição idempotente: o sócio que a primeira chamada já tinha criado
  • 201 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — A referência já existiu mas o sócio foi entretanto removido
  • 422 validation_error — Campos inválidos, ou grupo inexistente
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
GET/associates/{id}associates:read300/min

Obter a ficha de um sócio

Não inclui o número do documento de identificação, o registo de óbito, as coordenadas geocodificadas nem as preferências de notificação — quem factura a um sócio não precisa do cartão de cidadão dele.

Parâmetros

idstringno caminho

Resposta 200

idstring
associateNumberinteger | null
namestring
emailstring | null
statusstring
admissionDatedata ISO-8601 | null
birthDatedata ISO-8601 | null
phoneNumberstring | null
landlinePhonestring | null
addressstring | null
postalCodestring | null
citystring | null
fiscalIdstring | null
occupationstring | null
groupIdstring | null
customFieldsqualquer | nullCampos personalizados da associação. A forma é definida por cada associação.
hasPictureboolean
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
PATCH/associates/{id}associates:write60/min

Alterar a ficha de um sócio

Só os campos enviados são escritos. O estado tem endpoint próprio, e a referência externa não é alterável: é a chave de idempotência da criação, e reapontá-la faria uma criação repetida devolver o sócio errado.

Parâmetros

idstringno caminho

Corpo

name (opcional)string
email (opcional)string | null | string
admissionDate (opcional)data ISO-8601 | null
photoUploadId (opcional)string
removePhoto (opcional)boolean
address (opcional)string
postalCode (opcional)string
city (opcional)string
birthDate (opcional)data ISO-8601 | null
cityOfBirth (opcional)string
countryOfBirth (opcional)string
maritalStatus (opcional)"SINGLE" | "MARRIED" | "DIVORCED" | "WIDOWED" | "CIVIL_UNION" | "OTHER"
sex (opcional)"MALE" | "FEMALE" | "OTHER"
nationalIdNumber (opcional)string
nationalIdEmissionDate (opcional)data ISO-8601 | null
nationalIdEntity (opcional)string
nationalIdExpirationDate (opcional)data ISO-8601 | null
fiscalId (opcional)string
occupation (opcional)string
phoneNumber (opcional)string
landlinePhone (opcional)string
customFields (opcional)object
notifOptOutEmail (opcional)boolean
notifOptOutSms (opcional)boolean
groupId (opcional)string | null

Resposta 200

idstring
associateNumberinteger | null
namestring
emailstring | null
statusstring
admissionDatedata ISO-8601 | null
birthDatedata ISO-8601 | null
phoneNumberstring | null
landlinePhonestring | null
addressstring | null
postalCodestring | null
citystring | null
fiscalIdstring | null
occupationstring | null
groupIdstring | null
customFieldsqualquer | nullCampos personalizados da associação. A forma é definida por cada associação.
hasPictureboolean
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação
  • 422 validation_error — Campos inválidos, ou grupo inexistente
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
GET/associates/{id}/avatarassociates:read120/min

Obter a fotografia de um sócio, redimensionada

Devolve WebP quadrado. size é limitado entre 16 e 256 — um valor fora do intervalo é aproximado, não recusado. Guardar em cache privada apenas: é a fotografia de uma pessoa.

Parâmetros

idstringno caminho
size (opcional)string

Respostas

  • 200 A fotografia, quadrada, em WebP
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação; no_picture — O sócio não tem fotografia
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — O redimensionamento falhou
GET/associates/{id}/payable-quotasassociates:read300/min

O que um sócio deve neste momento

Parâmetros

idstringno caminho

Resposta 200

associateobject
idstring
namestring
associateNumberinteger | null
statusstring
quotasobject[]
idstring
kindstring
labelstring | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
GET/associates/{id}/quotasquotas:read300/min

Listar as quotas de um sócio

Parâmetros

idstringno caminho
status (opcional)"PENDING" | "PAID" | "OVERDUE" | "WAIVED" | string
kind (opcional)"RECURRING" | "ONE_OFF" | string
yearinteger
page (opcional)string
pageSize (opcional)string

Resposta 200

dataobject[]
idstring
associateIdstring
associate (opcional)object
idstring
namestring
associateNumberinteger | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring
kindstring
labelstring | null
paymentRequestIdstring | null
paidAtdata ISO-8601 | null
createdAtdata ISO-8601
updatedAtdata ISO-8601
pageinteger
pageSizeinteger
totalinteger
totalPagesinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação
  • 422 validation_errorstatus, kind ou year fora dos valores aceites
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
POST/associates/{id}/statusassociates:write60/min

Activar ou desactivar um sócio

ACTIVE e INACTIVE. Registar um óbito não se faz por aqui: perdoa quotas, marca uma data e alimenta uma fila que alguém confirma, e nada disso deve acontecer porque um sistema sincronizou uma linha desactualizada.

Parâmetros

idstringno caminho

Corpo

status"ACTIVE" | "INACTIVE"

Resposta 200

idstring
associateNumberinteger | null
namestring
emailstring | null
statusstring
admissionDatedata ISO-8601 | null
birthDatedata ISO-8601 | null
phoneNumberstring | null
landlinePhonestring | null
addressstring | null
postalCodestring | null
citystring | null
fiscalIdstring | null
occupationstring | null
groupIdstring | null
customFieldsqualquer | nullCampos personalizados da associação. A forma é definida por cada associação.
hasPictureboolean
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O sócio não existe nesta associação
  • 409 deceased_associate — O sócio está registado como falecido
  • 422 validation_errorstatus fora de ACTIVE/INACTIVE
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou

Quotas

GET/quotasquotas:read300/min

Listar quotas da associação

Parâmetros

status (opcional)"PENDING" | "PAID" | "OVERDUE" | "WAIVED" | string
kind (opcional)"RECURRING" | "ONE_OFF" | string
yearinteger
associateId (opcional)string
paymentRequestId (opcional)string
page (opcional)string
pageSize (opcional)string

Resposta 200

dataobject[]
idstring
associateIdstring
associate (opcional)object
idstring
namestring
associateNumberinteger | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring
kindstring
labelstring | null
paymentRequestIdstring | null
paidAtdata ISO-8601 | null
createdAtdata ISO-8601
updatedAtdata ISO-8601
pageinteger
pageSizeinteger
totalinteger
totalPagesinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 422 validation_errorstatus, kind ou year fora dos valores aceites
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
GET/quotas/{id}quotas:read300/min

Obter uma quota

Parâmetros

idstringno caminho

Resposta 200

idstring
associateIdstring
associate (opcional)object
idstring
namestring
associateNumberinteger | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring
kindstring
labelstring | null
paymentRequestIdstring | null
paidAtdata ISO-8601 | null
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — A quota não existe nesta associação
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
POST/quotas/generatequotas:write60/min

Gerar as quotas de um período

Aguenta uma repetição **sequencial** — só cria os períodos em falta, pelo que uma segunda chamada devolve created: 0. Duas chamadas **em paralelo** podem duplicar. Só são emitidas quotas a sócios activos: um id indicado em associateIds que não esteja activo vem devolvido em skippedAssociateIds. Não envia avisos por email: uma chamada pode emitir doze linhas por sócio, e escrever a toda a gente sem ninguém a ver não é um interruptor de máquina.

Corpo

yearinteger
month (opcional)integer
target (opcional)"all" | "missing"
associateIds (opcional)string[]

Resposta 201

createdinteger
skippedAssociateIdsstring[]

Respostas

  • 201 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 409 quota_config_missing — A associação não tem configuração de quotas
  • 422 validation_error — Campos inválidos
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
POST/quotas/mark-paidquotas:write60/min

Liquidar quotas

Idempotente por externalReference. Uma repetição devolve o resultado original, com os mesmos números de recibo — é o que permite a quem cobrou no terreno e perdeu a ligação reimprimir o recibo em vez de emitir um segundo pelo mesmo dinheiro.

Corpo

quotaIdsstring[]
paymentMethod (opcional)string
externalReferencestring

Resposta 200

idempotentboolean
statusstring
paidinteger
quotaIdsstring[]
externalReferencestring
transactions (opcional)object[]
idstring
quotaIdstring
receiptNumberstring | null
amountnumber

Respostas

  • 200 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 409 settlement_in_progress — Outra chamada com esta referência está a decorrer
  • 422 validation_error — Campos inválidos
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
POST/quotas/unmark-paidquotas:write60/min

Reverter uma liquidação

Só reverte liquidações feitas por esta API. O dinheiro recebido à porta pela aplicação de terreno partilha o mesmo livro de movimentos e não é reversível daqui: dinheiro entregue em mão só pode ser desfeito por quem o possa devolver.

Corpo

externalReferencestring

Resposta 200

reversedboolean
alreadyReversedboolean
quotaIdsstring[]

Respostas

  • 200 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — Não há liquidação desta API com essa referência
  • 422 validation_error — Campos inválidos
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
POST/quotas/{id}/waivequotas:write60/min

Perdoar uma quota

Perdoar não é liquidar: não há movimento, recibo nem fatura. Uma quota já paga é recusada — reverter uma liquidação é /quotas/unmark-paid, que também reverte o movimento.

Parâmetros

idstringno caminho

Resposta 200

idstring
associateIdstring
associate (opcional)object
idstring
namestring
associateNumberinteger | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring
kindstring
labelstring | null
paymentRequestIdstring | null
paidAtdata ISO-8601 | null
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — A quota não existe nesta associação
  • 409 quota_already_paid — A quota já está paga
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou

Campanhas de pagamento

GET/payment-requestsquotas:read300/min

Listar campanhas de pagamento pontual

Resposta 200

dataobject[]
idstring
namestring
amountnumber
totalinteger
paidinteger
pendinginteger
createdAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
GET/payment-requests/{id}quotas:read300/min

Obter uma campanha de pagamento e as suas quotas

Parâmetros

idstringno caminho

Resposta 200

idstring
namestring
amountnumber
createdAtdata ISO-8601
updatedAtdata ISO-8601
quotasobject[]
idstring
associateIdstring
associate (opcional)object
idstring
namestring
associateNumberinteger | null
referenceDatedata ISO-8601
dueDatedata ISO-8601
amountnumber
statusstring
kindstring
labelstring | null
paymentRequestIdstring | null
paidAtdata ISO-8601 | null
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — A campanha não existe nesta associação
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado

Movimentos

GET/transactionstransactions:read300/min

Listar movimentos

As datas filtram por quando o dinheiro mudou de mãos, não por quando o registo foi criado — um pagamento recebido no terreno e sincronizado mais tarde conta no dia em que foi cobrado.

Parâmetros

associateId (opcional)string
from (opcional)string
to (opcional)string
page (opcional)string
pageSize (opcional)string

Resposta 200

dataobject[]
idstring
associateIdstring | null
associate (opcional)object
idstring
namestring
associateNumberinteger | null
quotaIdstring | null
typestring
amountnumber
descriptionstring | null
paymentMethodstring | null
receiptNumberstring | null
externalReferencestring | null
collectedAtdata ISO-8601 | null
createdAtdata ISO-8601
pageinteger
pageSizeinteger
totalinteger
totalPagesinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 422 validation_error — Datas inválidas, ou from posterior a to
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado

Eventos

GET/eventsevents:read300/min

Listar eventos

isPublic filtra pelo que a página pública da associação mostra, não por visibilidade perante quem chama: uma chave da associação já está dentro do perímetro de confiança. Sem o filtro vêm os dois.

Parâmetros

from (opcional)string
to (opcional)string
isPublic (opcional)"true" | "false"
page (opcional)string
pageSize (opcional)string

Resposta 200

dataobject[]
idstring
titlestring
descriptionstring | null
datedata ISO-8601
endDatedata ISO-8601 | null
locationstring | null
isPublicboolean
status"SCHEDULED" | "CANCELLED"
createdAtdata ISO-8601
updatedAtdata ISO-8601
pageinteger
pageSizeinteger
totalinteger
totalPagesinteger

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 422 validation_error — Datas inválidas, from posterior a to, ou isPublic fora de true/false
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
POST/eventsevents:write60/min

Criar um evento

A imagem de capa não é aceite: é um data URL em base64 numa coluna de texto, e nada impediria um chamador não vigiado de enviar megabytes por evento.

Corpo

titlestring
description (opcional)string
datedata ISO-8601 | stringData ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01).
endDate (opcional)data ISO-8601 | stringData ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01).
location (opcional)string
isPublic (opcional)boolean

Resposta 201

idstring
titlestring
descriptionstring | null
datedata ISO-8601
endDatedata ISO-8601 | null
locationstring | null
isPublicboolean
status"SCHEDULED" | "CANCELLED"
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 201 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 422 validation_error — Campos inválidos, ou endDate anterior a date
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou
GET/events/{id}events:read300/min

Obter um evento

Parâmetros

idstringno caminho

Resposta 200

idstring
titlestring
descriptionstring | null
datedata ISO-8601
endDatedata ISO-8601 | null
locationstring | null
isPublicboolean
status"SCHEDULED" | "CANCELLED"
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O evento não existe nesta associação
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
PATCH/events/{id}events:write60/min

Alterar um evento

Parâmetros

idstringno caminho

Corpo

title (opcional)string
description (opcional)string
date (opcional)data ISO-8601 | stringData ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01).
endDate (opcional)data ISO-8601 | stringData ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01).
location (opcional)string
isPublic (opcional)boolean

Resposta 200

idstring
titlestring
descriptionstring | null
datedata ISO-8601
endDatedata ISO-8601 | null
locationstring | null
isPublicboolean
status"SCHEDULED" | "CANCELLED"
createdAtdata ISO-8601
updatedAtdata ISO-8601

Respostas

  • 200 OK
  • 400 bad_request — O corpo não é JSON
  • 401 unauthorized — Chave de API em falta, inválida ou revogada
  • 403 plan_required — A associação não está no plano Profissional; forbidden — A chave não tem a permissão exigida
  • 404 not_found — O evento não existe nesta associação
  • 422 validation_error — Campos inválidos, ou endDate anterior a date
  • 429 rate_limited — Orçamento de pedidos por minuto esgotado
  • 500 internal_error — A escrita falhou

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 de duas maneiras: pela própria associação, em Definições → Webhooks, ou pelo fluxo de ligação. Em ambos os casos é devolvido o segredo de assinatura (começa por whsec_) uma única vez — guardamo-lo cifrado e não há forma de o voltar a ler.

Nem todos os endereços são aceites — veja endereços aceites.

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
associate.createdUm sócio foi inscrito
associate.updatedA ficha de um sócio foi alterada
associate.status_changedUm sócio foi activado ou desactivado
quota.generatedForam geradas quotas para um período (um evento por geração, não um por quota)
quota.waivedUma quota foi perdoada
event.createdFoi criado um evento
event.updatedUm evento foi alterado

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.

Endereços aceites

Quem entrega os eventos é um cliente HTTP que corre dentro da nossa rede, por isso um endereço de webhook é uma instrução para irmos buscar alguma coisa a algum lado. Não aceitamos qualquer um. Se vir URL não permitido numa entrega, ou uma recusa ao guardar, é uma destas regras:

  • https, e só no porto 443. Um porto arbitrário é como se chega a um painel interno.
  • Sem credenciais no URL — nada de https://utilizador:senha@….
  • Tem de resolver para um endereço público. Ficam de fora localhost, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 100.64.0.0/10, os intervalos de documentação e reservados, e os equivalentes em IPv6 — incluindo um IPv4 embrulhado em IPv6, como ::ffff:127.0.0.1.
  • Nomes reservados para a rede local.local, .internal, .lan, .home.arpa — não são aceites.

A verificação corre ao guardar e outra vez antes de cada envio, e a ligação é feita para um dos endereços que a verificação aprovou. Verificar só ao guardar não chegaria: um nome pode ser reapontado a seguir, e é exactamente isso que alguém tentaria.

Não seguimos redirecionamentos. Uma resposta 3xx conta como entrega falhada — o endereço para onde aponta não passou por verificação nenhuma. Aponte o webhook directamente para o destino final.

Alterar o endereço de um endpoint gera um segredo novo e invalida o anterior: um endpoint reapontado é uma relação de confiança nova, e o destino antigo deixa de conseguir verificar as nossas assinaturas.

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.

Especificação

Toda a referência acima é gerada a partir das próprias rotas, e a mesma origem serve um documento OpenAPI 3.1:

GET https://associados.app/api/public/v1/openapi.json

Não exige chave — uma especificação é o que se lê antes de ter uma credencial, e descreve formatos, não dados de nenhuma associação. Serve para gerar um cliente na sua linguagem, para importar num Postman ou num Insomnia, e para comparar contra o que o seu código espera.

Porque é gerado, não pode divergir do que a API faz: um endpoint que exista sem estar declarado faz a nossa suite de testes falhar. Cada operação traz também o âmbito que exige e o orçamento de pedidos por minuto que gasta.

Precisa de ajuda a integrar?

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