API da Agenda Financeira (v1)
API REST pra conectar a Agenda Financeira a outras plataformas: Zapier, n8n, Make, planilhas, ERPs ou qualquer sistema seu. Tudo em JSON, sobre HTTPS.
- URL base:
https://agendafinanceira.online/api/v1 - OpenAPI 3.1:
https://agendafinanceira.online/api/v1/openapi.json— importe no Postman, Insomnia ou gere um cliente. - Status/índice:
GET https://agendafinanceira.online/api/v1(sem autenticação) lista os endpoints.
Autenticação
- No app, em Perfil → Configurações → API (integrações), clique em Gerar chave. Dê um nome (ex.: "n8n produção") e marque Permitir gravação se a integração for criar/alterar dados.
- A chave (
af_live_…) aparece uma única vez. Guarde como uma senha — no nosso banco fica só um hash dela. - Envie a chave em toda chamada, de um dos dois jeitos:
Authorization: Bearer af_live_XXXXXXXX…
X-API-Key: af_live_XXXXXXXX…
Escopos: read (toda chave tem) e write (só se marcado ao gerar). Uma chave revogada
passa a responder 401 imediatamente.
Teste rápido:
curl -H "Authorization: Bearer $AF_KEY" https://agendafinanceira.online/api/v1/me
Convenções
- Datas no formato
YYYY-MM-DD. Horários em ISO 8601 (UTC). - Valores em reais como número decimal (
125.9), sempre positivos — o sentido vem deentry_type:income(receita) ouexpense(despesa). - Ids são UUIDs.
- Body de
POST/PATCHcomContent-Type: application/json. - Listas paginadas aceitam
?page=1&per_page=50(máximo 200) e respondem:
{ "data": [ … ], "pagination": { "page": 1, "per_page": 50, "total": 312, "total_pages": 7 } }
Erros
Todo erro vem como { "error": { "code", "message", "details?" } }:
| HTTP | code | quando |
|---|---|---|
| 401 | unauthorized |
chave ausente, inválida ou revogada |
| 403 | forbidden |
chave só de leitura tentando gravar |
| 404 | not_found |
id não existe ou não pertence ao titular da chave |
| 409 | conflict |
operação que só o app pode fazer (veja cada endpoint) |
| 422 | validation_error |
campo faltando/errado — details lista cada campo |
| 429 | rate_limited |
limite de requisições excedido (veja abaixo) |
| 500 | internal_error |
falha inesperada do nosso lado |
Limites de requisições
Por chave: 120 requisições por minuto e 10.000 por dia. Toda resposta traz os headers:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1790000000 (epoch em segundos do fim da janela)
Ao exceder, a resposta é 429 com Retry-After (segundos). Recomendação: em integrações que
sincronizam muitos registros, use os filtros de período em vez de varrer tudo, e trate 429 com
espera + nova tentativa.
Endpoints
Titular
GET /me — confirma a chave: titular, nome/escopos da chave e quantas Agendas ele tem.
GET /profiles — Agendas do titular (id, name). O profile_id daqui é o que se manda ao
criar lançamento quando o titular tem mais de uma Agenda.
Contas
GET /accounts?profile_id= — contas do titular:
id, profile_id, name, type (checking | savings | investment | cash | credit_card), currency, initial_balance, is_default, is_profile_default_account, closing_day, due_day, credit_limit, created_at.
is_profile_default_account: a Agenda é um centro de custo — pode receber/pagar em mais de uma conta ao longo do tempo. Marque uma conta como padrão de cada Agenda (Cadastros → Contas → editar) praPOST /transactionsusar quandoaccount_idnão for informado.
POST /accounts (escopo write) — cria uma conta simples (sem cartão de crédito, que exige
fechamento/vencimento configurados pelo app):
{
"profile_id": "uuid (opcional se o titular só tem uma Agenda)",
"name": "Conta do imóvel",
"type": "checking",
"currency": "BRL",
"initial_balance": 0
}
type aceita checking | savings | investment | cash (default checking).
Responde 201 com a conta criada. Sem idempotência por external_id — confira com GET /accounts?profile_id= antes de criar, se sua integração puder reenviar.
Categorias
GET /categories?entry_type=income|expense — id, profile_id, parent_id, name, entry_type, expense_nature, income_nature, color, icon, created_at.
Contatos
GET /contacts?profile_id=&q=&page=&per_page= — contatos ativos (clientes/fornecedores).
q filtra por trecho do nome.
Lançamentos
GET /transactions — filtros (todos opcionais): profile_id, account_id, category_id, contact_id, entry_type, is_paid=true|false, from, to (datas), q (trecho da descrição), external_id, page, per_page. Ordena por data decrescente.
Só devolve lançamentos que existem de fato. Ocorrências futuras de uma recorrência que o app ainda não materializou (isso acontece perto do vencimento, ou quando são pagas/editadas) não aparecem — para projeções use o app.
Objeto de lançamento (o mesmo em todas as respostas e nos webhooks):
{
"id": "…", "profile_id": "…",
"account_id": "…", "account_name": "Itaú",
"category_id": "…", "category_name": "Mercado",
"contact_id": null,
"description": "Supermercado", "amount": 235.4, "entry_type": "expense", "date": "2026-09-19",
"is_paid": true, "is_scheduled": false, "auto_settle": false,
"is_transfer": false, "transfer_account_id": null, "is_initial_balance": false,
"is_invoice_payment": false, "credit_card_invoice_id": null,
"recurrence_group_id": null, "installment_group_id": null,
"installment_number": null, "installment_total": null, "split_from_id": null,
"external_id": "pedido-8812", "notes": null, "created_at": "2026-09-19T12:00:00Z"
}
GET /transactions/{id}
POST /transactions (escopo write) — cria um lançamento avulso:
{
"profile_id": "uuid (opcional se o titular só tem uma Agenda)",
"account_id": "uuid (opcional — sem isso, usa a conta padrão da Agenda)",
"category_id": "uuid — do mesmo tipo do entry_type",
"description": "Mensalidade João",
"amount": 350,
"entry_type": "income",
"date": "2026-10-05",
"is_paid": false,
"is_scheduled": false,
"auto_settle": false,
"notes": "opcional",
"contact_id": "uuid opcional",
"external_id": "id do registro no seu sistema — opcional, recomendado"
}
Responde 201 com o lançamento. Idempotência: se já existir um lançamento com o mesmo
external_id na mesma Agenda, responde 200 com o existente em vez de duplicar. Use sempre que
sua integração puder reenviar (retry do Zapier/n8n, reprocessar planilha).
Sem account_id, usa a conta marcada como is_profile_default_account na Agenda; sem conta
padrão configurada, responde 422. Se a conciliação mostrar que o valor caiu em outra conta,
mova o lançamento direto pelo app (edite a conta do lançamento) — a API não precisa acertar de
primeira.
Regras que valem como no app:
- Conta de cartão de crédito com fechamento/vencimento configurados: a compra nasce vinculada
à fatura do ciclo da
date, comis_paid=false(quem é paga é a fatura). - Não cria recorrência nem compra parcelada — mande um lançamento por ocorrência ou use o app.
PATCH /transactions/{id} (write) — campos aceitos: description, amount, date, is_paid, notes, category_id, contact_id, external_id. Responde 409 para saldo inicial, transferências,
pagamento/ajuste de fatura e, em compra no cartão, is_paid/amount/date.
DELETE /transactions/{id} (write) — responde 409 para saldo inicial, transferências,
pagamento de fatura, ocorrência de recorrência e parcela de compra parcelada.
Conciliação (movimentações sugeridas)
POST /bank-transactions (escopo write) — manda uma movimentação para a fila de Conciliação
sem criar lançamento. Ela aparece em Pendentes com a legenda e a miniatura do comprovante, e só
vira lançamento quando alguém confirma (casar com existente, registrar novo, transferência ou
ignorar) — mesmo fluxo do extrato bancário. É o que o coletor de comprovantes de WhatsApp usa.
{
"account_id": "uuid da conta de onde saiu/entrou o dinheiro",
"external_id": "id na origem (ex.: id da mensagem do WhatsApp)",
"posted_at": "2026-09-30",
"description": "Pix Supera Terapia Zé",
"amount": 99,
"direction": "debit",
"source": "whatsapp",
"caption": "Supera Terapia Zé",
"merchant_name": "IDEA Instituto",
"raw": { "qualquer": "coisa extraída na origem" },
"receipt": { "media_type": "image/jpeg", "data": "<base64 sem prefixo data:>" }
}
direction: debit (saiu dinheiro) ou credit (entrou). receipt é opcional (jpeg, png, webp ou
pdf, até 5 MB) e é recusado antes de gravar se vier inválido. Idempotente por account_id +
external_id: reenvio responde 200 com a existente em vez de duplicar; a primeira vez responde
201 com { id, status, source, created, receipt_uploaded }.
Tarefas
Item de agenda sem valor financeiro — sem conta, sem categoria. Pra marcar algo na Agenda (ex.: período de uma hospedagem, um compromisso) sem criar lançamento.
GET /tasks — filtros (todos opcionais): profile_id, contact_id, is_done=true|false, from, to (datas), q (trecho da descrição), external_id, page, per_page. Ordena por data decrescente.
Objeto de tarefa:
{
"id": "…", "profile_id": "…",
"description": "Hospedagem — Sítio Benitez", "date": "2026-12-20", "time": null,
"color": "#6b7280", "is_done": false, "contact_id": null,
"recurrence_group_id": null, "external_id": "reserva-8812-checkin",
"created_at": "2026-09-19T12:00:00Z"
}
GET /tasks/{id}
POST /tasks (escopo write) — cria uma tarefa avulsa:
{
"profile_id": "uuid (opcional se o titular só tem uma Agenda)",
"description": "Check-in — Sítio Benitez",
"date": "2026-12-20",
"time": "15:00",
"color": "#6b7280",
"is_done": false,
"contact_id": "uuid opcional",
"external_id": "id do registro no seu sistema — opcional, recomendado"
}
Responde 201 com a tarefa. Idempotência: mesma regra de /transactions — mesmo
external_id na mesma Agenda responde 200 com a existente em vez de duplicar.
PATCH /tasks/{id} (write) — campos aceitos: description, date, time, color, is_done, contact_id, external_id. Responde 409 pra ocorrência de tarefa recorrente (só pelo app).
DELETE /tasks/{id} (write) — mesmo 409 de ocorrência recorrente.
Webhooks (gestão pela API)
Além do Perfil, dá pra cadastrar webhooks pela própria API — é o que ferramentas como Zapier ("REST Hooks") fazem ao ligar/desligar um gatilho.
GET /webhooks— lista os endpoints.POST /webhooks(write) —{ "url": "https://…", "events": ["transaction.created", …] | ["*"], "description": "opcional" }. Responde201com o endpoint e osecret(só nessa resposta).GET /webhooks/{id}— endpoint + últimas 20 entregas.PATCH /webhooks/{id}(write) —url,events,description,is_active.DELETE /webhooks/{id}(write).POST /webhooks/{id}/test(write) — enfileira um eventoping(responde202).
Webhooks
Cadastre uma URL (Perfil → Configurações → Webhooks, ou POST /webhooks) e a Agenda faz um
POST nela sempre que um lançamento muda — por qualquer caminho: pelo app, pela API, pela
baixa automática ou pela materialização de uma recorrência.
Eventos
| evento | quando |
|---|---|
transaction.created |
lançamento criado |
transaction.updated |
descrição, valor, data, conta, categoria, contato, notas etc. alterados |
transaction.paid |
is_paid passou de falso para verdadeiro (pago/recebido) |
transaction.deleted |
lançamento excluído |
ping |
teste disparado por você (botão "Enviar teste" ou POST /webhooks/{id}/test) |
Lançamentos de saldo inicial e alterações só de bookkeeping interno não geram evento.
Formato do envio
POST https://sua-url
Content-Type: application/json
User-Agent: AgendaFinanceira-Webhooks/1.0
X-AgendaFinanceira-Event: transaction.paid
X-AgendaFinanceira-Delivery: 7f3c… (id único desta entrega — use pra deduplicar)
X-AgendaFinanceira-Signature: sha256=3f1a…
{
"id": "7f3c…",
"event": "transaction.paid",
"created_at": "2026-09-19T12:00:05Z",
"data": { …objeto de lançamento… }
}
Responda com qualquer status 2xx em até 10 segundos. Faça o processamento pesado depois de
responder.
Verificando a assinatura
A assinatura é o HMAC-SHA256 do corpo bruto (bytes exatos recebidos) com o segredo do webhook,
em hexadecimal, prefixado por sha256=. Compare em tempo constante.
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, secret, signatureHeader) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Python:
import hmac, hashlib
def verify(raw_body: bytes, secret: str, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
PHP:
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_AGENDAFINANCEIRA_SIGNATURE'] ?? '');
Entrega e retentativas
- Normalmente o evento chega em poucos segundos após a mudança.
- Se sua URL não responder
2xx(ou demorar mais de 10 s), tentamos de novo com espera crescente: 1 min, 5 min, 30 min, 2 h e 12 h — até 6 tentativas no total. - Um endpoint que falha muitas vezes seguidas é desativado automaticamente; reative no Perfil
(ou
PATCH { "is_active": true }) depois de corrigir. - Entregas podem, raramente, chegar mais de uma vez ou fora de ordem: use
X-AgendaFinanceira-Deliverypra deduplicar e odata.id+ campos do objeto como fonte de verdade. - O histórico das últimas entregas (status, HTTP, erro) fica no Perfil → Webhooks e em
GET /webhooks/{id}.
Exemplos
Criar despesa paga:
curl -X POST https://agendafinanceira.online/api/v1/transactions \
-H "Authorization: Bearer $AF_KEY" -H "Content-Type: application/json" \
-d '{"account_id":"…","category_id":"…","description":"Uber","amount":23.9,
"entry_type":"expense","date":"2026-09-19","is_paid":true,"external_id":"uber-9f1"}'
Listar receitas em aberto de outubro:
curl -H "Authorization: Bearer $AF_KEY" \
"https://agendafinanceira.online/api/v1/transactions?entry_type=income&is_paid=false&from=2026-10-01&to=2026-10-31"
Marcar como pago:
curl -X PATCH https://agendafinanceira.online/api/v1/transactions/<id> \
-H "Authorization: Bearer $AF_KEY" -H "Content-Type: application/json" \
-d '{"is_paid":true}'
Cadastrar um webhook:
curl -X POST https://agendafinanceira.online/api/v1/webhooks \
-H "Authorization: Bearer $AF_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://meu-sistema.com/hooks/agenda","events":["transaction.paid"]}'
Limitações conhecidas
- Lançamentos e tarefas criados/alterados pela API não disparam a sincronização com o Google Tasks — ela roda só a partir do app.
- A API não cria recorrências nem compras parceladas.
Suporte
Dúvidas e problemas: fale com a equipe pelo canal de mensagens dentro do app.
