Skip to content

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, jq and uuidgen (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 key

Every 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" | jq
json
{
  "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"}' | jq
json
{
  "id": "cmi7q2x4k0001lb08h3k2v9de",
  "reference": "P-7KQ2MX",
  "version": 1,
  "state": "open",
  "product": "car_lease",
  "amount": 14500,
  "termMonths": 72,
  "...": "..."
}
bash
export APP=cmi7q2x4k0001lb08h3k2v9de   # the id you got

Other 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" | jq
json
{
  "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" | jq

The 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 sha256

The 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"}' | jq

Send 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"
    }
  }' | jq

The response is the stored decision. A few rules:

  • Money in euros with up to 2 decimals; percentages as numbers (10.78 is 10.78 %). totalAmountPayable includes fees and any residual value, not the down payment.
  • validUntil is the last valid day (Vilnius time): not in the past, at most 180 days ahead.
  • conditions are shown to the client: plain Lithuanian, no links.
  • continueUrl must be on one of your continueUrlHosts.
  • To change the offer, send approved again; 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"}' | jq

This 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}}' | jq

After 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 ​

  1. Expose an HTTPS endpoint on port 443 with a public certificate, for example https://api.your-bank.example/finto/webhooks.
  2. Send the URL to Finto. You get a webhook secret (whsec_...) through a one-time link. GET /v1/me then shows webhook.url.
  3. Implement signature verification. Copy the function for your language from webhooks.md and check it against the test vector there.
  4. Ask for a test event:
bash
curl -s -X POST "$FINTO_API/v1/events/test" -H "Authorization: Bearer $FINTO_KEY" | jq

Your 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.mjs

Create a few test applications and watch the whole flow.

Next ​