Who owns what. The agency's system is the source of truth for the contract: buyer, property, amounts and due dates. Finpass is the source of truth for the payment: payment codes, bank confirmation, matching, receipts and the evidence trail. Finpass never becomes the agency's management system.
Authentication
Each agency has one API key. The owner generates it in the dashboard under Integration → Generate new key. The key is shown once, and generating a new one switches the old one off.
Authorization: Bearer fp_…
Content-Type: application/json
The sync rules
The same rules apply to the API and to spreadsheet imports.
The key is the contract reference from the agency's system (unique per agency), plus the instalment number (
seq) inside the contract. Sending the same contract again never duplicates anything.Create or update. An unknown reference creates the contract. A known reference updates the buyer, the property label and the instalments that changed.
Omitted instalments are left alone. Nothing is deleted because it was missing from a payload. To cancel an instalment, send it with
"status": "cancelled".Money already in motion is protected. These changes are not applied; they come back as a
conflictfor a person to resolve in the dashboard:- a new amount on an instalment that is paid, partly paid, or has a payment code issued;
- cancelling an instalment that has received money;
- reopening a cancelled instalment.
A due-date change is applied as long as the amount stays the same.
Termination is not synced. Sending
"status": "terminated"returns a conflict. The termination is done in the dashboard, because it calculates the refund.The contract currency never changes after the contract is created.
Buyer phone changes apply to future reminders. If the new number already belongs to another buyer of the same agency, the old number is kept and a conflict is returned.
Contract payload
{
"reference": "LOTE-12-QD-7",
"status": "active",
"propertyLabel": "Quadra 7, lote 12",
"currency": "BRL",
"buyer": {
"name": "Maria Souza",
"phone": "64211234567",
"document": "123.456.789-00",
"email": "maria@example.com"
},
"installments": [
{ "seq": 1, "amount": "2450.00", "dueDate": "2026-11-10" },
{ "seq": 2, "amount": "2450.00", "dueDate": "2026-12-10" },
{ "seq": 3, "amount": "2450.00", "dueDate": "2027-01-10", "status": "cancelled" }
]
}
| Field | Rule |
|---|---|
reference |
Required, up to 120 characters. |
currency |
ISO 4217, for example BRL, PYG, USD, EUR, NZD. |
buyer.phone |
Digits with country code. +, spaces and dashes are ignored. |
installments[].amount |
Decimal string with a dot ("2450.00"). PYG has no decimals ("920000"). |
installments[].dueDate |
YYYY-MM-DD. |
installments[].status |
open (default) or cancelled. |
A contract that fails validation is rejected whole, and nothing is written.
Field names, status values and error codes are the same in every language.
Endpoints
POST /api/sync/contracts: batch
Up to 200 contracts per call. Each contract is handled on its own, and the whole batch is recorded as one sync run, visible in the dashboard under Integration.
{ "contracts": [ { …contract… }, { …contract… } ] }
Response 200:
{
"runId": "5fbda03b-…",
"total": 3, "created": 1, "updated": 0, "unchanged": 1, "rejected": 1, "conflicts": 0,
"results": [
{
"reference": "LOTE-12-QD-7",
"status": "created",
"contractId": "…",
"changes": { "buyer": true, "property": true,
"installments": { "created": 2, "updated": 0, "cancelled": 0, "unchanged": 0 } },
"conflicts": []
},
{ "reference": "X-9", "status": "rejected", "error": "telefone_invalido", "changes": { … }, "conflicts": [] }
]
}
PUT /api/contracts/{reference}: one contract
The body is the contract payload, and reference may be omitted. The response is a single
result:
201when created;200when updated or unchanged;422when rejected.
A rejected contract, or one with conflicts, also appears in the dashboard history.
GET /api/contracts/{reference}: current state
Returns the buyer, every instalment with its status (open, settled or cancelled),
paidMinor, whether a payment code is issued (chargeIssued), and the contract balance. Use it
to check what Finpass holds before or after a sync.
GET /api/sync/runs/{runId}: a past sync run
Returns the run totals and the result of each contract in it.
Error and conflict codes
| Code | Meaning |
|---|---|
referencia_obrigatoria |
The contract has no reference. |
moeda_nao_suportada |
Currency not supported. |
telefone_invalido |
Buyer phone missing or not a phone number. |
email_invalido |
Buyer email is malformed. |
parcelas_obrigatorias |
The contract has no instalments. |
parcelas_demais |
More than 600 instalments. |
numero_de_parcela_invalido |
seq must be a positive integer. |
parcela_repetida:N |
seq N appears twice. |
vencimento_invalido:N |
Bad due date on instalment N. |
valor_invalido:N |
Bad amount on instalment N. |
contrato_nao_esta_ativo |
The contract was terminated in Finpass. |
moeda_do_contrato_nao_muda |
The currency differs from the existing contract. |
contrato_distratado_nao_e_importado |
A new contract arrived already terminated. |
distrato_deve_ser_feito_no_painel (conflict) |
Terminate it in the dashboard. |
parcela_paga_nao_muda (conflict) |
The instalment is paid. |
parcela_com_pagamento_parcial (conflict) |
The instalment received a partial payment. |
parcela_com_cobranca_em_aberto (conflict) |
A payment code is issued. Retry once it is paid or expired. |
parcela_com_pagamento_nao_pode_ser_cancelada (conflict) |
The instalment has money in it. |
parcela_cancelada_nao_volta (conflict) |
A cancelled instalment is not reopened. |
telefone_ja_pertence_a_outro_comprador (conflict) |
The new phone belongs to another buyer. |
Without a technical integration
The dashboard's Import portfolio screen takes a spreadsheet with one row per contract. It follows the same rules: re-importing updates what changed and never duplicates. Accepted columns:
- contract:
contratoorreferencia; - buyer:
compradorornome,telefoneorwhatsapp,documento,cpforci,email; - property:
imovelorlote; - money and dates:
moeda,valor,parcelas,primeiro_vencimento.
Webhooks: Finpass tells your system
Configure them in the dashboard under Integration → Notices for your system:
- Set the HTTPS address.
- Generate the signing secret. It is shown once.
- Send a test.
Events
| Event | When |
|---|---|
payment.confirmed |
An instalment is fully paid. The bank confirmed it, or a person matched it in the exceptions queue. |
payment.partial |
Money was credited, but the instalment is still open. |
exception.created |
Money arrived that Finpass would not match on its own: unknown payer or reference, wrong amount, or another currency. |
installment.overdue |
An instalment passed its due date and is still open. Sent once per instalment. |
buyer.reminders_opted_out / buyer.reminders_opted_in |
The buyer replied STOP or REMINDERS. |
contract.terminated |
The contract was terminated in the dashboard, with the paid, retained and refund amounts. |
ping |
The test button. |
Envelope
{
"id": "6f1c…",
"type": "payment.confirmed",
"createdAt": "2026-10-10T14:32:05.120Z",
"data": {
"contract": { "id": "…", "reference": "LOTE-12-QD-7" },
"installment": { "seq": 37, "amountMinor": 245000, "currency": "BRL", "dueDate": "2026-10-10",
"status": "settled", "paidMinor": 245000 },
"payment": { "amountMinor": 245000, "currency": "BRL", "confirmedBy": "bank",
"payerName": "MARIA SOUZA", "bankTransactionId": "E1234…",
"receiptUrl": "https://…/v/…" },
"buyer": { "name": "Maria Souza", "phone": "64211234567", "document": "123.456.789-00" }
}
}
When the payment was made in another currency, payment also carries
"original": { "amountMinor": 70000, "currency": "BRL", "rate": "1314" }. Here R$ 700.00 was
paid at a declared rate of 1 BRL = 1,314 PYG, against an instalment in guaraníes.
Amounts are always integers in the currency's smallest unit (amountMinor): 245000 in BRL is
R$ 2,450.00; in PYG the smallest unit is the guaraní itself.
Headers
Finpass-Event: the event type.Finpass-Delivery: the event id. It is the same on every retry, so use it to ignore duplicates.Finpass-Signature:t=<unix seconds>,v1=<hex>.
Verifying the signature
Compute HMAC-SHA256(secret, "<t>." + raw_body) in hex and compare it with v1 using a
constant-time comparison. Reject the request if t is more than 5 minutes old.
const [t, v1] = header.split(',').map((p) => p.split('=')[1])
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300
Delivery
- Any
2xxresponse counts as delivered. Answer quickly, and do the work afterwards. - Payments confirmed by the bank are sent within seconds. Everything else goes out within 5 minutes.
- Failures are retried after 2, 4, 8, 16, 32, 64 and 128 minutes. After the 8th attempt the event is marked "gave up".
- Every event, its status and the last response code are listed in the dashboard. Any event that was not delivered can be resent with one click.
- Events are recorded only while an address is configured.
Receipts export (pull)
GET /api/payments?from=YYYY-MM-DD&to=YYYY-MM-DD
Lists every payment credited in the period, both dates inclusive. Each item has:
- the contract reference and the instalment;
- the amount, and the original currency when it differs;
- the payer name and the bank transaction id;
- the receipt link;
- the buyer.
Use it if you would rather fetch than receive webhooks, or to reconcile once a day.
The dashboard has the same data as a spreadsheet: Integration → Export receipts. The file
uses ; as separator, a decimal comma for Portuguese and Spanish agencies, and a BOM so Excel
reads accents correctly.