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 |
|---|---|---|
| bad_request | 400 | O corpo não é JSON |
| unauthorized | 401 | Chave de API em falta, inválida ou revogada |
| forbidden | 403 | A chave não tem a permissão exigida |
| plan_required | 403 | A associação não está no plano Profissional |
| no_picture | 404 | O sócio não tem fotografiaSó em GET /associates/{id}/avatar |
| not_found | 404 | A referência já existiu mas o sócio foi entretanto removido |
| deceased_associate | 409 | O sócio está registado como falecidoSó em POST /associates/{id}/status |
| quota_already_paid | 409 | A quota já está pagaSó em POST /quotas/{id}/waive |
| quota_config_missing | 409 | A associação não tem configuração de quotasSó em POST /quotas/generate |
| settlement_in_progress | 409 | Outra chamada com esta referência está a decorrerSó em POST /quotas/mark-paid |
| validation_error | 422 | Campos inválidos, ou grupo inexistente |
| rate_limited | 429 | Orçamento de pedidos por minuto esgotado |
| internal_error | 500 | A escrita falhou |
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.A chave
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
| tenant | object | null | |
| key | object | |
| └ name | string | null | |
| └ prefix | string | null | |
| └ scopes | string[] | |
| └ createdAt | data ISO-8601 | null | |
| └ expiresAt | data ISO-8601 | null | |
| tier | string | |
| limits | object | |
| └ read | integer | |
| └ avatar | integer | |
| └ write | integer |
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
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
| data | object[] | |
| └ id | string | |
| └ associateNumber | integer | null | |
| └ name | string | |
| └ status | string | |
| └ hasPicture | boolean | |
| page | integer | |
| pageSize | integer | |
| total | integer | |
| totalPages | integer |
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
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
| name | string | |
| 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 | |
| groupId | string | null | |
| externalReference (opcional) | string |
Resposta 200
| id | string | |
| associateNumber | integer | null | |
| name | string | |
| string | null | ||
| status | string | |
| admissionDate | data ISO-8601 | null | |
| birthDate | data ISO-8601 | null | |
| phoneNumber | string | null | |
| landlinePhone | string | null | |
| address | string | null | |
| postalCode | string | null | |
| city | string | null | |
| fiscalId | string | null | |
| occupation | string | null | |
| groupId | string | null | |
| customFields | qualquer | null | Campos personalizados da associação. A forma é definida por cada associação. |
| hasPicture | boolean | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
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
| id | string | no caminho |
Resposta 200
| id | string | |
| associateNumber | integer | null | |
| name | string | |
| string | null | ||
| status | string | |
| admissionDate | data ISO-8601 | null | |
| birthDate | data ISO-8601 | null | |
| phoneNumber | string | null | |
| landlinePhone | string | null | |
| address | string | null | |
| postalCode | string | null | |
| city | string | null | |
| fiscalId | string | null | |
| occupation | string | null | |
| groupId | string | null | |
| customFields | qualquer | null | Campos personalizados da associação. A forma é definida por cada associação. |
| hasPicture | boolean | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
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
| id | string | no 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
| id | string | |
| associateNumber | integer | null | |
| name | string | |
| string | null | ||
| status | string | |
| admissionDate | data ISO-8601 | null | |
| birthDate | data ISO-8601 | null | |
| phoneNumber | string | null | |
| landlinePhone | string | null | |
| address | string | null | |
| postalCode | string | null | |
| city | string | null | |
| fiscalId | string | null | |
| occupation | string | null | |
| groupId | string | null | |
| customFields | qualquer | null | Campos personalizados da associação. A forma é definida por cada associação. |
| hasPicture | boolean | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
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
| id | string | no 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
O que um sócio deve neste momento
Parâmetros
| id | string | no caminho |
Resposta 200
| associate | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| └ status | string | |
| quotas | object[] | |
| └ id | string | |
| └ kind | string | |
| └ label | string | null | |
| └ referenceDate | data ISO-8601 | |
| └ dueDate | data ISO-8601 | |
| └ amount | number | |
| └ status | string |
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
Listar as quotas de um sócio
Parâmetros
| id | string | no caminho |
| status (opcional) | "PENDING" | "PAID" | "OVERDUE" | "WAIVED" | string | |
| kind (opcional) | "RECURRING" | "ONE_OFF" | string | |
| year | integer | |
| page (opcional) | string | |
| pageSize (opcional) | string |
Resposta 200
| data | object[] | |
| └ id | string | |
| └ associateId | string | |
| └ associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| └ referenceDate | data ISO-8601 | |
| └ dueDate | data ISO-8601 | |
| └ amount | number | |
| └ status | string | |
| └ kind | string | |
| └ label | string | null | |
| └ paymentRequestId | string | null | |
| └ paidAt | data ISO-8601 | null | |
| └ createdAt | data ISO-8601 | |
| └ updatedAt | data ISO-8601 | |
| page | integer | |
| pageSize | integer | |
| total | integer | |
| totalPages | integer |
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_error—status,kindouyearfora dos valores aceites - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado
Activar ou desactivar um sócio
Só 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
| id | string | no caminho |
Corpo
| status | "ACTIVE" | "INACTIVE" |
Resposta 200
| id | string | |
| associateNumber | integer | null | |
| name | string | |
| string | null | ||
| status | string | |
| admissionDate | data ISO-8601 | null | |
| birthDate | data ISO-8601 | null | |
| phoneNumber | string | null | |
| landlinePhone | string | null | |
| address | string | null | |
| postalCode | string | null | |
| city | string | null | |
| fiscalId | string | null | |
| occupation | string | null | |
| groupId | string | null | |
| customFields | qualquer | null | Campos personalizados da associação. A forma é definida por cada associação. |
| hasPicture | boolean | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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_error—statusfora de ACTIVE/INACTIVE - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado - 500 —
internal_error— A escrita falhou
Quotas
Listar quotas da associação
Parâmetros
| status (opcional) | "PENDING" | "PAID" | "OVERDUE" | "WAIVED" | string | |
| kind (opcional) | "RECURRING" | "ONE_OFF" | string | |
| year | integer | |
| associateId (opcional) | string | |
| paymentRequestId (opcional) | string | |
| page (opcional) | string | |
| pageSize (opcional) | string |
Resposta 200
| data | object[] | |
| └ id | string | |
| └ associateId | string | |
| └ associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| └ referenceDate | data ISO-8601 | |
| └ dueDate | data ISO-8601 | |
| └ amount | number | |
| └ status | string | |
| └ kind | string | |
| └ label | string | null | |
| └ paymentRequestId | string | null | |
| └ paidAt | data ISO-8601 | null | |
| └ createdAt | data ISO-8601 | |
| └ updatedAt | data ISO-8601 | |
| page | integer | |
| pageSize | integer | |
| total | integer | |
| totalPages | integer |
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—status,kindouyearfora dos valores aceites - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado
Obter uma quota
Parâmetros
| id | string | no caminho |
Resposta 200
| id | string | |
| associateId | string | |
| associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| referenceDate | data ISO-8601 | |
| dueDate | data ISO-8601 | |
| amount | number | |
| status | string | |
| kind | string | |
| label | string | null | |
| paymentRequestId | string | null | |
| paidAt | data ISO-8601 | null | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
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
| year | integer | |
| month (opcional) | integer | |
| target (opcional) | "all" | "missing" | |
| associateIds (opcional) | string[] |
Resposta 201
| created | integer | |
| skippedAssociateIds | string[] |
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
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
| quotaIds | string[] | |
| paymentMethod (opcional) | string | |
| externalReference | string |
Resposta 200
| idempotent | boolean | |
| status | string | |
| paid | integer | |
| quotaIds | string[] | |
| externalReference | string | |
| transactions (opcional) | object[] | |
| └ id | string | |
| └ quotaId | string | |
| └ receiptNumber | string | null | |
| └ amount | number |
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
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
| externalReference | string |
Resposta 200
| reversed | boolean | |
| alreadyReversed | boolean | |
| quotaIds | string[] |
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
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
| id | string | no caminho |
Resposta 200
| id | string | |
| associateId | string | |
| associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| referenceDate | data ISO-8601 | |
| dueDate | data ISO-8601 | |
| amount | number | |
| status | string | |
| kind | string | |
| label | string | null | |
| paymentRequestId | string | null | |
| paidAt | data ISO-8601 | null | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
Listar campanhas de pagamento pontual
Resposta 200
| data | object[] | |
| └ id | string | |
| └ name | string | |
| └ amount | number | |
| └ total | integer | |
| └ paid | integer | |
| └ pending | integer | |
| └ createdAt | data 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
Obter uma campanha de pagamento e as suas quotas
Parâmetros
| id | string | no caminho |
Resposta 200
| id | string | |
| name | string | |
| amount | number | |
| createdAt | data ISO-8601 | |
| updatedAt | data ISO-8601 | |
| quotas | object[] | |
| └ id | string | |
| └ associateId | string | |
| └ associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| └ referenceDate | data ISO-8601 | |
| └ dueDate | data ISO-8601 | |
| └ amount | number | |
| └ status | string | |
| └ kind | string | |
| └ label | string | null | |
| └ paymentRequestId | string | null | |
| └ paidAt | data ISO-8601 | null | |
| └ createdAt | data ISO-8601 | |
| └ updatedAt | data 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
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
| data | object[] | |
| └ id | string | |
| └ associateId | string | null | |
| └ associate (opcional) | object | |
| └ id | string | |
| └ name | string | |
| └ associateNumber | integer | null | |
| └ quotaId | string | null | |
| └ type | string | |
| └ amount | number | |
| └ description | string | null | |
| └ paymentMethod | string | null | |
| └ receiptNumber | string | null | |
| └ externalReference | string | null | |
| └ collectedAt | data ISO-8601 | null | |
| └ createdAt | data ISO-8601 | |
| page | integer | |
| pageSize | integer | |
| total | integer | |
| totalPages | integer |
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, oufromposterior ato - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado
Eventos
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
| data | object[] | |
| └ id | string | |
| └ title | string | |
| └ description | string | null | |
| └ date | data ISO-8601 | |
| └ endDate | data ISO-8601 | null | |
| └ location | string | null | |
| └ isPublic | boolean | |
| └ status | "SCHEDULED" | "CANCELLED" | |
| └ createdAt | data ISO-8601 | |
| └ updatedAt | data ISO-8601 | |
| page | integer | |
| pageSize | integer | |
| total | integer | |
| totalPages | integer |
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,fromposterior ato, ouisPublicfora de true/false - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado
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
| title | string | |
| description (opcional) | string | |
| date | data ISO-8601 | string | Data ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01). |
| endDate (opcional) | data ISO-8601 | string | Data ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01). |
| location (opcional) | string | |
| isPublic (opcional) | boolean |
Resposta 201
| id | string | |
| title | string | |
| description | string | null | |
| date | data ISO-8601 | |
| endDate | data ISO-8601 | null | |
| location | string | null | |
| isPublic | boolean | |
| status | "SCHEDULED" | "CANCELLED" | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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, ouendDateanterior adate - 429 —
rate_limited— Orçamento de pedidos por minuto esgotado - 500 —
internal_error— A escrita falhou
Obter um evento
Parâmetros
| id | string | no caminho |
Resposta 200
| id | string | |
| title | string | |
| description | string | null | |
| date | data ISO-8601 | |
| endDate | data ISO-8601 | null | |
| location | string | null | |
| isPublic | boolean | |
| status | "SCHEDULED" | "CANCELLED" | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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
Alterar um evento
Parâmetros
| id | string | no caminho |
Corpo
| title (opcional) | string | |
| description (opcional) | string | |
| date (opcional) | data ISO-8601 | string | Data ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01). |
| endDate (opcional) | data ISO-8601 | string | Data ISO-8601, com ou sem hora (2026-05-01T09:00:00Z ou 2026-05-01). |
| location (opcional) | string | |
| isPublic (opcional) | boolean |
Resposta 200
| id | string | |
| title | string | |
| description | string | null | |
| date | data ISO-8601 | |
| endDate | data ISO-8601 | null | |
| location | string | null | |
| isPublic | boolean | |
| status | "SCHEDULED" | "CANCELLED" | |
| createdAt | data ISO-8601 | |
| updatedAt | data 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, ouendDateanterior adate - 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.
| 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 |
| associate.created | Um sócio foi inscrito |
| associate.updated | A ficha de um sócio foi alterada |
| associate.status_changed | Um sócio foi activado ou desactivado |
| quota.generated | Foram geradas quotas para um período (um evento por geração, não um por quota) |
| quota.waived | Uma quota foi perdoada |
| event.created | Foi criado um evento |
| event.updated | Um 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:
- Só
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:
- 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.
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.jsonNã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.