Appearance
Quickstart: your first integration in the sandbox
This walkthrough takes you through the whole life of one application in about 30 minutes, with curl only: you create a test application, fetch it, download the signed PDF, send an offer, let the simulated client choose it and report the signed agreement. At the end you set up webhooks.
Everything here happens in the sandbox, which holds generated data only. The live API works the same way, with a different address and key.
What you need
- A sandbox API key (
fnt_test_...) from Finto, delivered to your technical contact through a one-time link. Store it in your secret manager. - Optional in the sandbox, required in live: the IP addresses your requests come from. Finto adds them to your allow-list.
curl,jqanduuidgen(or any HTTP client). For the webhook part: an HTTPS endpoint on port 443 reachable from the internet.
bash
export FINTO_API=https://partners.sandbox.finto.lt
export FINTO_KEY=fnt_test_... # your keyEvery request carries the key as a bearer token. Every POST that changes something also carries an Idempotency-Key: a unique value for that one change (a UUID), so a retried request is never applied twice.
1. Check the connection
bash
curl -s "$FINTO_API/v1/me" -H "Authorization: Bearer $FINTO_KEY" | jqjson
{
"lender": { "id": "cmi1lnd0r0001lb08demo0001", "name": "Demo bankas", "companyCode": "100000001" },
"environment": "sandbox",
"apiKey": { "id": "cmi1key0r0001lb08demo0001", "prefix": "fnt_test_7Hq2", "label": "Integration tests", "createdAt": "2026-11-18T12:00:00Z", "expiresAt": "2027-11-18T12:00:00Z" },
"requestIp": "203.0.113.10",
"allowedIps": ["203.0.113.0/28"],
"webhook": { "url": null, "enabled": false },
"webhookSourceIps": ["198.51.100.7", "198.51.100.8"],
"continueUrlHosts": ["demo-bankas.example"]
}requestIp is the address Finto sees. If you get 403 ip_not_allowed, send that address to Finto. 401 unauthorized means the key is wrong, expired, revoked or belongs to the other environment.
2. Create a test application
In the sandbox you play Finto's part too: this call creates an application that a (generated) client has just signed, addressed only to you.
bash
curl -s -X POST "$FINTO_API/v1/sandbox/applications" \
-H "Authorization: Bearer $FINTO_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"scenario": "car_lease"}' | jqjson
{
"id": "cmi7q2x4k0001lb08h3k2v9de",
"reference": "P-7KQ2MX",
"version": 1,
"state": "open",
"product": "car_lease",
"amount": 14500,
"termMonths": 72,
"...": "..."
}bash
export APP=cmi7q2x4k0001lb08h3k2v9de # the id you gotOther scenarios (joint application, PEP, high obligations, only required fields and more) are listed in sandbox.md.
3. See the event
Creating the application produced an application.submitted event. Without a webhook you read it from the feed:
bash
curl -s "$FINTO_API/v1/events" -H "Authorization: Bearer $FINTO_KEY" | jqjson
{
"data": [
{
"id": "cmi7evt0001lb08x0y1z2w3",
"sequence": 1,
"type": "application.submitted",
"environment": "sandbox",
"createdAt": "2026-11-20T08:14:06Z",
"data": { "applicationId": "cmi7q2x4k0001lb08h3k2v9de", "reference": "P-7KQ2MX", "version": 1, "changes": null, "closedReason": null, "decisionId": null }
}
],
"hasMore": false,
"lastSequence": 1
}The event holds identifiers only. sequence is your own counter, without gaps; next time you would call /v1/events?after=1. In production you receive the same event as a webhook within seconds (step 9).
4. Fetch the application
bash
curl -s "$FINTO_API/v1/applications/$APP" -H "Authorization: Bearer $FINTO_KEY" | jqThe response holds what the client signed: the request (request), the client (applicant), the spouse (spouse), the creditor table (creditors), the signature (signature) and the documents (documents). An abridged example:
json
{
"id": "cmi7q2x4k0001lb08h3k2v9de",
"reference": "P-7KQ2MX",
"version": 1,
"state": "open",
"channel": "autolizingas",
"expiresAt": "2026-12-20T08:14:03Z",
"request": {
"product": "car_lease",
"amount": 14500,
"termMonths": 72,
"downPayment": 2500
},
"applicant": {
"firstName": "Jonas",
"lastName": "Petraitis",
"personalCode": "50001018865",
"phone": "+37061234567",
"email": "jonas.petraitis@example.com",
"incomes": [{ "type": "employment", "monthlyNet": 1650, "tenure": { "minMonths": 4, "maxMonths": null } }],
"obligations": { "hasObligations": true, "monthlyPayments": 120 },
"pep": { "isPep": false, "...": null },
"beneficialOwner": { "forOwnBenefit": true, "...": null }
},
"creditors": [
{ "name": "AB „Demo bankas“", "companyCode": "100000001", "isYou": true },
{ "name": "UAB „Pavyzdys lizingas“", "companyCode": "300000002", "isYou": false }
],
"signature": { "documentId": "cmi7q2y1d0003lb08a1f4c7e2", "level": "qes", "signerName": "JONAS PETRAITIS", "...": "..." },
"documents": [
{ "id": "cmi7q2y1d0003lb08a1f4c7e2", "type": "signed_application", "sha256": "e6e18b74...", "...": "..." }
],
"decision": null
}Every field is explained in data.md. Every read is logged on Finto's side, so Finto staff see when you fetched the application.
5. Download the signed application
bash
export DOC=cmi7q2y1d0003lb08a1f4c7e2 # signature.documentId
curl -s "$FINTO_API/v1/applications/$APP/documents/$DOC" \
-H "Authorization: Bearer $FINTO_KEY" -o application.pdf
shasum -a 256 application.pdf # must equal the document's sha256The PDF carries the client's qualified electronic signature (Smart-ID or Mobile-ID). Check it with your usual signature validation; it is the legal record of the request, the creditor table and the consents (signed-application.md). In the sandbox the PDF is not signed (sandbox.md).
6. Tell Finto you are reviewing
bash
curl -s -X POST "$FINTO_API/v1/applications/$APP/decisions" \
-H "Authorization: Bearer $FINTO_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status": "in_review", "applicationVersion": 1, "lenderReference": "DB-2026-118734"}' | jqSend in_review as soon as you start, with your own application number in lenderReference: Finto staff see that you are on it. applicationVersion is the version of the application you fetched; every decision states it.
About the Idempotency-Key: in your system, generate it when you decide to send a change, store it with that change, and reuse it on every retry of the same change. If a call times out and you send it again with the same key and body, you get the stored decision back (200) instead of a second one. The rules are in conventions.md.
7. Send an offer
bash
VALID_UNTIL=$(date -u -v+14d +%F 2>/dev/null || date -u -d '+14 days' +%F)
curl -s -X POST "$FINTO_API/v1/applications/$APP/decisions" \
-H "Authorization: Bearer $FINTO_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"status": "approved",
"applicationVersion": 1,
"lenderReference": "DB-2026-118734",
"offer": {
"amount": 14500,
"termMonths": 72,
"monthlyPayment": 267.89,
"annualInterestRate": 9.9,
"aprc": 10.78,
"totalAmountPayable": 19438.08,
"interestRateType": "fixed",
"contractFee": 150,
"downPayment": 2500,
"validUntil": "'"$VALID_UNTIL"'",
"conditions": ["Privalomas KASKO draudimas"],
"continueUrl": "https://demo-bankas.example/continue/8f3c2a"
}
}' | jqThe response is the stored decision. A few rules:
- Money in euros with up to 2 decimals; percentages as numbers (
10.78is 10.78 %).totalAmountPayableincludes fees and any residual value, not the down payment. validUntilis the last valid day (Vilnius time): not in the past, at most 180 days ahead.conditionsare shown to the client: plain Lithuanian, no links.continueUrlmust be on one of yourcontinueUrlHosts.- To change the offer, send
approvedagain; the new one replaces the old. - Unknown fields are rejected (
422), so a typo cannot slip through.
Try a mistake to see the error format: remove aprc and send again with a new idempotency key. You get 422 validation_failed with errors[0].path = "offer.aprc". See errors.md.
Other answers, when you need them:
json
{ "status": "info_required", "applicationVersion": 1, "requirements": [{ "type": "income_statement", "description": "Pažyma apie pajamas už paskutinius 6 mėnesius" }] }
{ "status": "rejected", "applicationVersion": 1, "rejectionReason": "high_debt_burden" }8. Let the client choose your offer
In live, the client chooses an offer with Finto. In the sandbox you trigger it yourself:
bash
curl -s -X POST "$FINTO_API/v1/sandbox/applications/$APP/simulate" \
-H "Authorization: Bearer $FINTO_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "select_offer"}' | jqThis produces an offer.selected event, and the application's state becomes offer_selected. In live, this is the moment you contact the client (or the client comes through your continueUrl) to identify them and sign the agreement.
Then report the outcome:
bash
curl -s -X POST "$FINTO_API/v1/applications/$APP/decisions" \
-H "Authorization: Bearer $FINTO_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status": "contract_signed", "applicationVersion": 1, "contract": {"signedAt": "'"$(date -u +%FT%TZ)"'", "amount": 14500, "termMonths": 72, "contractNumber": "LS-2026-00981"}}' | jq
curl -s -X POST "$FINTO_API/v1/applications/$APP/decisions" \
-H "Authorization: Bearer $FINTO_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status": "disbursed", "applicationVersion": 1, "disbursement": {"disbursedAt": "'"$(date -u +%FT%TZ)"'", "amount": 14500}}' | jqAfter disbursed you get application.closed with completed: nothing more is expected. If the client does not sign after all, send cancelled with a cancellationReason instead.
That is the whole happy path. Other paths (rejection, documents requested, the client choosing another lender, consent withdrawn) are in lifecycle.md, and each can be played with simulate.
9. Receive webhooks
- Expose an HTTPS endpoint on port 443 with a public certificate, for example
https://api.your-bank.example/finto/webhooks. - Send the URL to Finto. You get a webhook secret (
whsec_...) through a one-time link.GET /v1/methen showswebhook.url. - Implement signature verification. Copy the function for your language from webhooks.md and check it against the test vector there.
- Ask for a test event:
bash
curl -s -X POST "$FINTO_API/v1/events/test" -H "Authorization: Bearer $FINTO_KEY" | jqYour endpoint receives a signed ping within seconds. Answer 200 within 10 seconds. Real events that fail are retried for about two days; a ping is not retried.
10. Run the reference lender
examples/node/lender.mjs is a complete lender in one file (Node.js 20+, no dependencies): it receives webhooks, polls the feed as a safety net, fetches applications and answers with a simple rule. It listens on plain HTTP, so put it behind your HTTPS reverse proxy (or run it without FINTO_WEBHOOK_SECRET to use the feed only):
bash
FINTO_API=$FINTO_API FINTO_KEY=$FINTO_KEY FINTO_WEBHOOK_SECRET=whsec_... \
PORT=8080 AUTO_COMPLETE=1 node lender.mjsCreate a few test applications and watch the whole flow.
Next
- lifecycle.md: every state, status and scenario.
- data.md: what each field means and how to map it.
- conventions.md: idempotency, versions, limits, code generation.
- security.md: keys, IP addresses, data handling.
- sandbox.md: scenarios and the go-live check.