API v1.0.0

eDofinansowania API dla partnerów

API dla partnerów handlowych. Pozwala pobrać katalog dotacji do własnego systemu, dopasować dotacje do profilu klienta i przeprowadzić pełne sprawdzenie kwalifikowalności. Kontrakt mówi jednym językiem. Wartości słownikowe są kodami: województwa jako ISO 3166-2:PL, wielkość firmy i rodzaje instalacji w snake_case. Każdy kod przychodzi razem z polską etykietą do wyświetlenia, więc nie trzeba jej wymyślać u siebie; filtry przyjmują wyłącznie kod. Odpowiedzi na pytania silnika mają stabilne identyfikatory, a tekst przy nich jest etykietą, którą można przeredagować bez zmiany przebiegu. Komunikaty błędów są po angielsku, tak jak reszta kontraktu. Treść redakcyjna, czyli tytuły i opisy dotacji, teksty pytań kwalifikacyjnych i komunikaty werdyktu, jest po polsku i pozostaje po polsku. To dane, nie interfejs: polskie programy dotacyjne mają polskie nazwy. Każda odpowiedź niesie nagłówek Content-Language, więc nie trzeba tego zakładać.

Szybki start

zapytanie
curl 'https://edofinansowania.pl/api/v1/grants?voivodeship=mazowieckie&installationTypes=PV' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'
odpowiedź
{
  "data": [
    {
      "grantCode": "EDF-0004",
      "title": "Kredyt ekologiczny",
      "fundingAmount": "do 80% kosztów kwalifikowanych",
      "applicationDeadline": "2026-09-30",
      "eligibleInstallations": ["PV", "Heat Pump"],
      "hasEligibilityEngine": true
    }
  ],
  "pagination": { "hasMore": false, "nextCursor": null }
}
nabór do 2026-09-30zostało 57 dni

Odpowiedzi w skrócie

Adres bazowy
https://edofinansowania.pl/api
Uwierzytelnianie
Authorization: Bearer sk_live_... (albo nagłówek X-API-Key)
Limit
60 zapytań na minutę, pula miesięczna ustalana per partner
Format błędów
RFC 9457 Problem Details, application/problem+json
Wersjonowanie
Wersja w ścieżce (/v1). Zmiana łamiąca oznaczałaby nową wersję, a wycofanie starej ogłosimy z co najmniej 6-miesięcznym wyprzedzeniem.
Synchronizacja
GET /v1/grants/changes zwraca, co doszło i co wypadło z katalogu. Zapisz syncedAt i odeślij je jako since.

Klucze, limity, nagłówki

Każde żądanie poza GET /v1/health wymaga klucza. Klucz podajesz w nagłówku Authorization ze schematem Bearer albo, jeśli tak wygodniej Twojej bibliotece, w nagłówku X-API-Key. Obie drogi są równoważne i prowadzą do tego samego sprawdzenia.

uwierzytelnianie
curl 'https://edofinansowania.pl/api/v1/health' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

# to samo, jeśli Twoja biblioteka woli własny nagłówek
curl 'https://edofinansowania.pl/api/v1/health' \
  -H 'X-API-Key: sk_live_TWOJ_KLUCZ'

Budowa klucza

Klucz to prefiks, 43 znaki losowe i 8 znaków sumy kontrolnej. Suma kontrolna jest liczona z części losowej, więc klucz z literówką albo obcięty przy kopiowaniu odpada, zanim dotkniemy bazy. Praktyczny wniosek: jeśli dostajesz problem-invalid-key natychmiast, to prawie zawsze wina kopiowania, a nie uprawnień.

format klucza
sk_live_9f3Kq2mVXbT8sLpR7dWnA4uZcE1yH6jG0oB5tNfQiMx1a2b3c4d
└──┬───┘└────────────────────┬────────────────────┘└──┬───┘
prefiks         43 znaki losowe (base64url)    suma kontrolna

Klucz sekretny kontra publikowalny

Różnice między rodzajami kluczy
Klucz sekretnyKlucz publikowalny
Prefikssk_live_pk_live_
Gdzie działaWywołania serwer do serwera. Trzymaj go po stronie backendu.Wyłącznie przeglądarka, wyłącznie ze zgłoszonych domen.
CORSBrak nagłówka Access-Control-Allow-Origin. Wyciekniętego klucza nie da się użyć ze strony WWW.Odpowiedź niesie Access-Control-Allow-Origin dla domeny, z której przyszła.
DomenyLista domen musi być pusta.Lista domen musi mieć co najmniej jedną pozycję. Żądanie spoza niej albo bez nagłówka Origin dostaje 403.
Limit na minutęDomyślnie 60.Domyślnie 30, osobny licznik. Widget na publicznej stronie nie może przepalić limitu integracji z CRM.
WażnośćKlucz kontraktowy bezterminowo, klucz założony samodzielnie w panelu 90 dni.Tak samo jak dla klucza sekretnego.

Blokada domeny przy kluczu publikowalnym jest zabezpieczeniem przed nadużyciem, nie zabezpieczeniem kryptograficznym. Prawdomówny nagłówek Origin wysyła tylko przeglądarka, a narzędzie wiersza poleceń może go podrobić. Dlatego klucz publikowalny nie dostaje niczego, czego nie można pokazać publicznie.

Zakresy

Konto partnera ma włączone zakresy, a każda trasa należy do jednego z nich. Trasa spoza Twoich zakresów odpowiada problem-endpoint-not-enabled i jest to ustawienie po naszej stronie, nie przełącznik w panelu.

Zakresy i należące do nich trasy
ZakresTrasy
grantsGET /v1/grants, GET /v1/grants/changes, GET /v1/grants/{id}
eligibilityPOST /v1/eligibility/check, POST /v1/eligibility/run
metaGET /v1/meta/vocabularies
bez zakresuGET /v1/health, jedyna trasa bez klucza

Limity

Obowiązują dwa liczniki naraz: na minutę i na miesiąc. Domyślnie 60 żądań na minutę dla klucza sekretnego, 30 dla publikowalnego i 50 000 miesięcznie na całe konto, ale wartości ustalamy per partner, więc wiążące jest to, co przychodzi w nagłówkach. Okno minutowe jest stałe, nie przesuwne: zaczyna się o pełnej sekundzie zerowej każdej minuty.

  • Odpowiedź 304 zużywa limit. Licznik rusza na bramce, zanim ustalimy, czy cokolwiek się zmieniło. Warunkowe żądanie oszczędza transfer, nie pulę.
  • Żądanie odrzucone z powodu limitu minutowego nie zjada puli miesięcznej. Ponowienie po Retry-After nic nie kosztuje poza czasem.
  • GET /v1/health nie zużywa niczego, bo kończy się przed licznikiem. Możesz go odpytywać z monitoringu tak często, jak chcesz.
  • Odpowiedzi 4xx i 5xx z uwierzytelnionej ścieżki liczą się normalnie. Pętla ponawiająca błędne żądanie wyczerpie pulę tak samo jak pętla poprawna.

Nagłówki odpowiedzi

Nagłówki limitów
NagłówekZnaczenie
X-RateLimit-LimitIle żądań na minutę dopuszcza ten klucz.
X-RateLimit-RemainingIle zostało w bieżącej minucie.
X-RateLimit-ResetCzas uniksowy końca bieżącego okna minutowego.
X-Quota-LimitMiesięczna pula całego konta.
X-Quota-RemainingIle zostało w tym miesiącu.
X-Quota-ResetCzas uniksowy początku nowego okresu rozliczeniowego.
RateLimit, RateLimit-PolicyTen sam stan w postaci z draftu draft-ietf-httpapi-ratelimit-headers. Draft nie jest jeszcze RFC, więc wiążące są nagłówki X-, a te jadą dla klientów, które draft już wspierają.
Retry-AfterPo ilu sekundach ponowić. Tylko przy odpowiedzi 429.
ETagTylko przy GET /v1/grants. Odeślij w If-None-Match, a przy braku zmian dostaniesz 304 bez treści.

Jedna rzecz, której nie widać po samych nazwach: nagłówki limitów pojawiają się dopiero wtedy, gdy wiadomo, czyj jest klucz. Odpowiedź 401 i trzy z czterech odmian 403 zapadają wcześniej i nie niosą żadnego licznika. Nie buduj więc sterowania ponawianiem na założeniu, że X-RateLimit-Remaining jest zawsze obecny.

Przy kluczu publikowalnym przeglądarka pokaże te nagłówki tylko dlatego, że wymieniamy je w Access-Control-Expose-Headers. Bez tego widget nie miałby jak się wycofać przed wyczerpaniem limitu.

Uwierzytelnianie

Klucze API i limity zapytań.

GET/v1/health

Sprawdzenie dostępności API

Jedyna trasa dostępna bez klucza. Odpowiada wyłącznie na pytanie, czy API żyje: nie zwraca danych katalogu ani danych partnera i nie zużywa limitu. Dzięki temu możesz ją wpisać do własnego monitoringu bez trzymania tam żywego klucza, a przy awarii odróżnisz nasz problem od swojego.

curl -X GET 'https://edofinansowania.pl/api/v1/health' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Odpowiedzi

200
API odpowiada.

Dotacje

Katalog dotacji i szczegóły pojedynczej dotacji.

GET/v1/grants

Katalog dotacji

Zwraca dotacje opublikowane, których termin naboru nie minął, posortowane rosnąco po tym terminie. Filtry są opcjonalne i każdy pominięty niczego nie zawęża. Stronicowanie jest kursorowe: zapisz nextCursor i podaj je w kolejnym żądaniu, aż hasMore będzie fałszem. Do bieżącej synchronizacji katalogu użyj GET /v1/grants/changes, a nie updatedSince, bo tamten endpoint mówi również o dotacjach, które z katalogu wypadły. Odpowiedź niesie ETag: odesłanie go w If-None-Match zwróci 304 bez treści.

Parametry

voivodeshipstring · zapytanie
Kod województwa (ISO 3166-2:PL). Dotacje oznaczone NATIONWIDE pasują do każdego.
companySizestring · zapytanie
Wielkość firmy klienta.
installationTypesstring · zapytanie
Kody rodzajów instalacji po przecinku. Dopasowanie na zasadzie LUB.
limitinteger · zapytanie
Ile wyników na stronę. Domyślnie 50, maksymalnie 200.
cursorstring · zapytanie
Wartość `nextCursor` z poprzedniej odpowiedzi. Nie twórz go samodzielnie - jest nieprzezroczysty, żeby sortowanie mogło się zmienić bez psucia Twojej integracji.
updatedSincestring · zapytanie
Tylko dotacje zmienione po tej dacie (ISO 8601).
If-None-Matchstring · nagłówek
ETag z poprzedniej odpowiedzi. Jeśli katalog się nie zmienił, odpowiedź to 304 bez treści.
X-If-None-Matchstring · nagłówek
To samo co If-None-Match. Użyj tego nagłówka, jeśli łączysz się przez edofinansowania.pl/api - warstwa proxy nie przekazuje standardowego If-None-Match do serwera.
curl -X GET 'https://edofinansowania.pl/api/v1/grants?voivodeship=PL-MZ&companySize=small&installationTypes=pv%2Cheat_pump&updatedSince=2026-07-01T00%3A00%3A00Z' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Odpowiedzi

200
Strona wyników.
304
Katalog nie zmienił się od czasu podanego ETag. Bez treści. Uwaga: 304 nadal liczy się do limitu. Przez edofinansowania.pl/api użyj nagłówka X-If-None-Match.
400
Nieprawidłowe zapytanie - odpowiedź zawiera listę dozwolonych wartości.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

GET/v1/grants/changes

Co się zmieniło od ostatniej synchronizacji

Zwraca dwie listy: `upserted` (dotacje do dodania lub zaktualizowania) i `removed` (dotacje, które wypadły z katalogu). Bez tej drugiej listy katalog u partnera po kilku miesiącach zawiera martwe rekordy z nieaktualnymi terminami. Zapisz `syncedAt` z odpowiedzi i odeślij je jako `since` przy następnym wywołaniu. Nie używaj do tego własnego zegara: system spieszący się o kilka minut na stałe pomijałby okno zmian, bez żadnego objawu. `reason` mówi, co zrobić z rekordem: `expired` oznacza zakończony nabór (warto zachować w historii), `unpublished` i `deleted` oznaczają rekord do usunięcia. To pokrywa się z tym, co zwróci `GET /v1/grants/{id}`: dotacja `expired` nadal odpowiada 200, pozostałe 404. Endpoint nie stronicuje i nie obsługuje `ETag` - `syncedAt` zmienia się przy każdym wywołaniu, więc zapytanie warunkowe i tak nigdy by nie trafiło. Jeśli kiedykolwiek dostaniesz `truncated: true`, zrób pełną synchronizację przez `GET /v1/grants`. Lista upserted jest ucinana na 200 pozycjach i nie ma własnej flagi obcięcia (truncated dotyczy wyłącznie listy removed). Przy codziennej synchronizacji nie ma to znaczenia, ale po dłuższej przerwie odpytaj katalog w całości przez GET /v1/grants.

Parametry

sincestring · zapytanie
Znacznik `syncedAt` z poprzedniej odpowiedzi. Pominięty = pełna migawka katalogu.
curl -X GET 'https://edofinansowania.pl/api/v1/grants/changes?since=2026-07-30T09%3A00%3A00.000Z' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Odpowiedzi

200
Zmiany od podanego znacznika.
400
Nieprawidłowe zapytanie - odpowiedź zawiera listę dozwolonych wartości.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

GET/v1/grants/{id}

Szczegóły dotacji

Pełna treść: warunki, dokumenty, FAQ, harmonogram i sekcje opisowe. Identyfikatorem może być UUID albo kod dotacji w rodzaju EDF-0004. Zwraca też dotacje po terminie, z polem isOpen ustawionym na fałsz, więc CRM trzymający starą referencję dostaje treść, a nie 404. Status 404 znaczy tutaj, że dotacja została wycofana z katalogu albo usunięta.

Parametry

idstring · w ścieżce · wymagany
UUID albo kod dotacji.
curl -X GET 'https://edofinansowania.pl/api/v1/grants/EDF-0004' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Odpowiedzi

200
Dotacja.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
404
Nie znaleziono zasobu.
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

Kwalifikowalność

Dopasowanie po profilu firmy i pełny silnik pytań.

POST/v1/eligibility/check

Dopasowanie dotacji do profilu klienta

Wysyłasz to, co i tak masz w CRM, dostajesz listę pasujących dotacji razem z powodem dopasowania. Kryterium, którego nie podałeś, ma matched równe null, co znaczy "nie sprawdzaliśmy", a nie "przeszło". Reguła dopasowania jest ta sama co w katalogu: między kryteriami działa koniunkcja, wewnątrz listy instalacji alternatywa, a dotacja ogólnopolska pasuje do każdego województwa. Odpowiedź nie stronicuje i jest ucinana na 200 dotacjach, więc bardzo szerokie profile warto zawężać filtrami.

curl -X POST 'https://edofinansowania.pl/api/v1/eligibility/check' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ' \
  -H 'Content-Type: application/json' \
  -d '{"voivodeship":"PL-MZ","companySize":"small","installationTypes":["pv"]}'

Odpowiedzi

200
Dopasowane dotacje.
400
Nieprawidłowe zapytanie - odpowiedź zawiera listę dozwolonych wartości.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

POST/v1/eligibility/run

Pełne sprawdzenie kwalifikowalności

Bezstanowe. Wysyłasz wszystkie zebrane dotąd odpowiedzi, dostajesz albo następne pytanie, albo werdykt. Pętlę prowadzi Twój system, my nie trzymamy sesji. Odpowiedzi identyfikujesz przez pole id opcji, nie przez jej tekst. Tekst jest etykietą do pokazania użytkownikowi i może zostać przeredagowany; identyfikator jest tym, po czym silnik wybiera gałąź. W v1 były tym samym napisem, co oznaczało, że poprawka literówki w odpowiedzi zmieniała przebieg.

curl -X POST 'https://edofinansowania.pl/api/v1/eligibility/run' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ' \
  -H 'Content-Type: application/json' \
  -d '{"grantId":"EDF-0004","answers":{"q1":"yes"}}'

Odpowiedzi

200
Następne pytanie albo werdykt.
400
Nieprawidłowe zapytanie - odpowiedź zawiera listę dozwolonych wartości.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
404
Nie znaleziono zasobu.
422
Zapytanie poprawne składniowo, ale niespójne.
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

Słowniki

Dozwolone wartości filtrów.

GET/v1/meta/vocabularies

Dozwolone wartości filtrów

Zbuduj z tego listy wyboru w swoim CRM. Wersja 2 zwraca same kody, bez etykiet: etykieta jest prezentacją i należy do klienta, a v1 zamroziła polską etykietę w swoim kontrakcie i nie ma powodu tego powtarzać. Wartość NATIONWIDE nie jest kodem podziału terytorialnego, tylko oznaczeniem dotacji obejmującej cały kraj.

curl -X GET 'https://edofinansowania.pl/api/v1/meta/vocabularies' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Odpowiedzi

200
Słowniki.
401
Brak klucza albo klucz nieprawidłowy, odwołany lub wygasły.
403
Forbidden
429
Przekroczony limit. Nagłówek Retry-After mówi, kiedy ponowić.
500
ServerError

Kwalifikowalność

Dopasowanie dotacji do klienta opiera się na trzech kryteriach: gdzie klient prowadzi działalność, jak duża jest firma i co chce zainstalować. Te same trzy kryteria działają w katalogu jako filtry zapytania i w POST /v1/eligibility/check jako profil firmy. Rozdział poniżej opisuje dokładnie, jakie wartości są dopuszczalne i jak liczymy trafienie, żeby nie trzeba było tego odtwarzać z zachowania.

Dopuszczalne wartości

Porównanie jest dokładne co do znaku. Nie normalizujemy niczego celowo: wartość spoza słownika kończy się odpowiedzią 400 z listą allowed, a nie pustym wynikiem, bo puste wyniki są znacznie trudniejsze do wykrycia w cudzym systemie.

Województwa jako kody ISO 3166-2:PL. Wartość NATIONWIDE nie jest kodem podziału terytorialnego: tak oznaczamy dotacje obejmujące cały kraj i pasują one do klienta z dowolnego miejsca.

  • PL-DS
  • PL-KP
  • PL-LU
  • PL-LB
  • PL-LD
  • PL-MA
  • PL-MZ
  • PL-OP
  • PL-PK
  • PL-PD
  • PL-PM
  • PL-SL
  • PL-SK
  • PL-WN
  • PL-WP
  • PL-ZP
  • NATIONWIDE

Wielkość firmy.

  • micro
  • small
  • medium
  • large
  • small_mid_cap
  • mid_cap

Rodzaje instalacji. Do API wysyłasz kod z lewej kolumny, użytkownikowi pokazujesz etykietę z prawej. Oba pola zwraca też GET /v1/meta/vocabularies, więc nie trzeba ich przepisywać do siebie.

Rodzaje instalacji
KodEtykieta
pvFotowoltaika
heat_pumpPompa ciepła
wind_turbineTurbina wiatrowa
energy_storageMagazyn energii
biomass_boilerKocioł na biomasę
solar_thermalKolektory słoneczne
thermal_modernisationTermomodernizacja
machineryMaszyny
ev_chargerŁadowarka EV
hydroelectricHydroelektrownia
biogas_plantBiogazownia
odpowiedź na wartość spoza słownika
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-invalid-request",
  "title": "Invalid request",
  "status": 400,
  "detail": "Unknown voivodeship: PL-XX",
  "instance": "/v1/grants",
  "allowed": ["PL-DS", "PL-KP", "PL-MZ", "NATIONWIDE"]
}

Reguła dopasowania

Dotacja trafia do wyniku, gdy spełnia wszystkie poniższe warunki naraz. Kolejność nie ma znaczenia, ale warto przeczytać je jako całość, bo dwa pierwsze punkty odpowiadają za większość pytań w rodzaju "dlaczego nie widzę dotacji, którą widziałem wczoraj".

  • Jest opublikowana. Rekord wycofany z katalogu znika z wyników natychmiast, również wtedy, gdy jego nabór nadal trwa.
  • Termin naboru nie minął. Porównujemy z dzisiejszą datą, więc dotacja z terminem dzisiejszym jeszcze się liczy, a jutro zniknie. Szczegóły takiej dotacji nadal odpowiadają 200 pod GET /v1/grants/{id}, z polem isOpen: false.
  • Województwo pasuje, gdy dotacja wymienia dokładnie ten kod albo gdy jest oznaczona jako NATIONWIDE.
  • Wielkość firmy pasuje, gdy dotacja wymienia dokładnie tę wartość. Nie ma tu odpowiednika jokera ogólnopolskiego.
  • Rodzaje instalacji łączą się alternatywą: wystarczy, że dotacja obsługuje jeden z podanych. Kryterium ma sens sprzedażowy, bo klient pytający o fotowoltaikę i magazyn energii chce zobaczyć również program finansujący samą fotowoltaikę.
  • Kryterium pominięte nie zawęża niczego. Pusty profil zwraca cały otwarty katalog.

Dlaczego dotacja pasuje

POST /v1/eligibility/check dokłada do każdej dotacji obiekt matchedOn, żeby handlowiec widział powód, a nie samą listę. Trzy stany są tu istotne i nie należy ich mylić.

Znaczenie pola matched
WartośćZnaczenie
matched: trueKryterium zostało sprawdzone i przeszło.
matched: falseKryterium zostało sprawdzone i nie przeszło.
matched: nullKryterium nie było sprawdzane, bo nie podałeś go w profilu. Towarzyszy mu reason: not_filtered. To nie jest zgoda ani odmowa.
via: exactDotacja wymienia to konkretne województwo. Warto to pokazać, bo brzmi mocniej w rozmowie z klientem.
via: nationwideDotacja pasuje, bo obejmuje cały kraj, a nie dlatego, że wymienia województwo klienta.
valuesPrzy instalacjach: część wspólna tego, o co pytałeś, i tego, co dotacja finansuje.
dopasowanie po pełnym profilu
{
  "profile": {
    "voivodeship": "PL-MZ",
    "companySize": "small",
    "installationTypes": ["pv", "heat_pump"]
  },
  "total": 2,
  "data": [
    {
      "grantCode": "EDF-0004",
      "title": "Kredyt ekologiczny",
      "eligibleLocations": ["NATIONWIDE"],
      "eligibleInstallations": ["pv", "heat_pump", "energy_storage"],
      "eligibleCompanySizes": ["micro", "small", "medium"],
      "matchedOn": {
        "voivodeship": { "matched": true, "via": "nationwide", "value": "NATIONWIDE" },
        "companySize": { "matched": true, "value": "small" },
        "installationTypes": { "matched": true, "values": ["pv", "heat_pump"] }
      }
    }
  ]
}
profil z samym województwem
"matchedOn": {
  "voivodeship": { "matched": true, "via": "exact", "value": "PL-SL" },
  "companySize": { "matched": null, "reason": "not_filtered" },
  "installationTypes": { "matched": null, "reason": "not_filtered" }
}

Ten endpoint nie stronicuje i zwraca najwyżej 200 dotacji, a pole total liczy to, co faktycznie przyszło. Przy bardzo szerokim profilu, na przykład samym województwie, warto dołożyć rodzaj instalacji, zamiast zakładać, że widzisz komplet.

Pełne sprawdzenie: silnik pytań

Dopasowanie po profilu odpowiada na pytanie "co w ogóle wchodzi w grę". Właściwą kwalifikowalność sprawdza POST /v1/eligibility/run, który prowadzi przez pytania konkretnego programu. Uruchamiaj go tylko dla dotacji z hasEligibilityEngine: true, bo pozostałe nie mają skonfigurowanego silnika.

  • Endpoint jest bezstanowy. Za każdym razem wysyłasz komplet zebranych dotąd odpowiedzi, a my odtwarzamy przejście od pytania startowego. Nie ma identyfikatora sesji do przechowywania.
  • Odpowiedź ma jeden z dwóch kształtów, rozróżnianych polem status: incomplete z polem nextQuestion albo complete z polem verdict.
  • Pole path to pytania faktycznie odwiedzone, w kolejności. Nadaje się do zapisania w historii klienta jako ślad, na jakiej podstawie zapadł werdykt.
  • progress.totalQuestions jest górnym ograniczeniem, nie liczbą dokładną. Graf pytań się rozgałęzia, więc konkretna ścieżka bywa krótsza. Pasek postępu zbudowany na tej liczbie nie dojdzie do końca i lepiej pokazać sam licznik odpowiedzi.
  • Odsyłaj identyfikator wybranej opcji z nextQuestion.options, nie jej tekst. Tekst jest etykietą i może zostać przeredagowany; identyfikator jest tym, po czym silnik wybiera gałąź.

Cztery rzeczy mogą pójść nie tak i każda ma własny wpis w katalogu błędów: odpowiedź na pytanie, którego silnik nie zna, odpowiedź spoza dozwolonych opcji, odpowiedzi nieukładające się w jedną ścieżkę oraz wada w samym grafie pytań. Pierwsze trzy to 422 i wynikają z żądania, czwarta to 500 i wynika z naszych danych.

Scenariusze integracji

1. Katalog u siebie i jego odświeżanie

Zacznij od jednego pełnego przejścia po katalogu. Bierz największą dozwoloną stronę, bo limit liczy żądania, a nie rekordy, więc 200 na stronę kosztuje cztery razy mniej niż 50.

pierwsza strona
curl 'https://edofinansowania.pl/api/v1/grants?limit=200' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'
odpowiedź
{
  "data": [ /* 200 dotacji */ ],
  "pagination": {
    "limit": 200,
    "hasMore": true,
    "nextCursor": "eyJkIjoiMjAyNi0wOS0zMCIsImkiOiI3YzQuLi4ifQ"
  }
}

Dopóki hasMore jest prawdą, powtarzaj z cursor równym nextCursor z poprzedniej odpowiedzi. Przepisuj tę wartość w całości. Kursor jest nieprzezroczysty celowo: pod spodem siedzi pozycja w sortowaniu po terminie naboru, a opakowanie go pozwala nam zmienić sortowanie bez psucia gotowych integracji.

kolejna strona
curl 'https://edofinansowania.pl/api/v1/grants?limit=200&cursor=eyJkIjoiMjAyNi0wOS0zMCIsImkiOiI3YzQuLi4ifQ' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'

Od tego momentu nie powtarzaj pełnego przejścia. Raz dziennie wystarczy jedno wywołanie GET /v1/grants/changes ze znacznikiem z poprzedniego razu.

synchronizacja przyrostowa
curl 'https://edofinansowania.pl/api/v1/grants/changes?since=2026-07-30T09:00:00.000Z' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'
odpowiedź
{
  "since": "2026-07-30T09:00:00.000Z",
  "syncedAt": "2026-07-31T09:00:04.812Z",
  "upserted": [
    { "id": "7c4...", "grantCode": "EDF-0012", "title": "...", "applicationDeadline": "2026-11-30" }
  ],
  "removed": [
    { "id": "1b9...", "grantCode": "EDF-0031", "reason": "expired", "removedAt": "2026-07-31T00:00:00.000Z" }
  ],
  "truncated": false
}
  • Zapisz syncedAt i odeślij je jako since następnym razem. Nie używaj do tego własnego zegara. System spieszący się o kilka minut na stałe pomijałby okno zmian, i to bez żadnego objawu, bo odpowiedzi wyglądałyby poprawnie.
  • Lista upserted to rekordy do wstawienia albo zaktualizowania. Klucz to id, a nie grantCode.
  • Lista removed mówi, co wypadło z katalogu, a pole reason, co z tym zrobić. expired to zakończony nabór, warto zachować rekord w historii klienta. unpublished i deleted to rekordy do usunięcia. Pokrywa się to z zachowaniem GET /v1/grants/{id}: dotacja wygasła nadal odpowiada 200, pozostałe dwie 404.
  • Endpoint nie stronicuje i nie obsługuje ETag, bo syncedAt zmienia się przy każdym wywołaniu. Lista upserted jest ucinana na 200 pozycjach, a flaga truncated dotyczy wyłącznie listy removed. Po dłuższej przerwie w synchronizacji odpytaj katalog w całości zamiast pytać o zmiany.

2. Odświeżanie warunkowe

Jeśli odpytujesz katalog z konkretnym filtrem częściej niż raz dziennie, na przykład żeby odświeżyć widok w CRM, użyj ETag. Przy braku zmian odpowiedź to 304 bez treści.

żądanie warunkowe
# pierwsze pobranie: zapisz ETag z odpowiedzi
curl -i 'https://edofinansowania.pl/api/v1/grants?voivodeship=PL-MZ' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'
# ETag: W/"3f9a1c08b2d4e6f7a8b9c0d1e2f3a4b5"

# kolejne: odeślij go i licz się z odpowiedzią 304 bez treści
curl -i 'https://edofinansowania.pl/api/v1/grants?voivodeship=PL-MZ' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ' \
  -H 'X-If-None-Match: W/"3f9a1c08b2d4e6f7a8b9c0d1e2f3a4b5"'
  • Przez adres edofinansowania.pl/api użyj nagłówka X-If-None-Match. Warstwa proxy nie przekazuje standardowego If-None-Match do serwera, więc żądanie z samym nagłówkiem standardowym zawsze dostanie pełną odpowiedź. Sprawdzone na produkcji, nie wywnioskowane z dokumentacji proxy.
  • 304 oszczędza transfer, nie limit. Licznik minutowy rusza na bramce, zanim zajrzymy do bazy.
  • ETag jest liczony z całej odpowiedzi razem ze stroną wyników, więc zmiana parametrów zapytania to inny ETag. Trzymaj go obok filtra, którego dotyczy.

3. Prowadzenie sprawdzenia kwalifikowalności

Pętlę prowadzi Twój system. Zaczynasz od samego grantId z pustą mapą odpowiedzi.

start
curl -X POST 'https://edofinansowania.pl/api/v1/eligibility/run' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ' \
  -H 'Content-Type: application/json' \
  -d '{"grantId":"EDF-0004","answers":{}}'
pierwsze pytanie
{
  "status": "incomplete",
  "engine": { "id": "eng_kredyt_eko", "version": "3" },
  "nextQuestion": {
    "id": "q1",
    "text": "Czy firma prowadzi działalność dłużej niż 12 miesięcy?",
    "type": "yes-no",
    "options": [{ "id": "yes", "label": "Tak" }, { "id": "no", "label": "Nie" }],
    "description": null,
    "helpText": "Liczy się data rozpoczęcia działalności z CEIDG lub KRS."
  },
  "progress": { "answered": 0, "totalQuestions": 6 },
  "path": []
}

Zadajesz pytanie użytkownikowi, dokładasz do mapy id wybranej opcji, nie jej tekst, i wołasz ponownie z kompletem. Nie wysyłasz samej ostatniej odpowiedzi, bo endpoint nie pamięta poprzednich.

kolejny krok
curl -X POST 'https://edofinansowania.pl/api/v1/eligibility/run' \
  -H 'Authorization: Bearer sk_live_TWOJ_KLUCZ' \
  -H 'Content-Type: application/json' \
  -d '{"grantId":"EDF-0004","answers":{"q1":"yes"}}'

Powtarzasz, dopóki status to incomplete. Werdykt przychodzi w tej samej strukturze, tylko z innym kształtem.

werdykt
{
  "status": "complete",
  "engine": { "id": "eng_kredyt_eko", "version": "3" },
  "verdict": "not_eligible",
  "message": "Program nie obejmuje firm bez historii kredytowej.",
  "failedCondition": "Brak zdolności kredytowej potwierdzonej przez bank",
  "progress": { "answered": 3, "totalQuestions": 6 },
  "path": ["q1", "q2", "q4"]
}
  • Przy cofnięciu użytkownika usuń z mapy wszystkie odpowiedzi po zmienianym pytaniu. Zostawione odpowiedzi z porzuconej gałęzi dają 422 z listą unreachable.
  • Zapisz engine.version razem z werdyktem. Jeśli zmieni się w trakcie rozmowy, zebrane odpowiedzi mogą przestać układać się w ścieżkę i rozmowę trzeba zacząć od nowa. Zapisana wersja tłumaczy też później, dlaczego ten sam klient dostał inny wynik.
  • failedCondition nadaje się do pokazania handlowcowi, bo mówi, który warunek przesądził. message jest pisane pod klienta końcowego.

Katalog błędów

Błędy przychodzą jako application/problem+json w formacie RFC 9457. Rozpoznawaj je po polu type, nie po treści title: adres w type jest identyfikatorem i się nie zmienia, a tytuł jest zdaniem dla człowieka i może zostać przeredagowany.

Poza polami type, title, status, detail i instance dokument bywa uzupełniony o pola dodatkowe, wymienione przy odpowiednich wpisach. Zgodnie z RFC leżą one na najwyższym poziomie, a nie w zagnieżdżonym obiekcie.

401Missing API key

problem-missing-key

Żądanie dotarło bez nagłówka Authorization i bez X-API-Key.

Dodaj nagłówek Authorization: Bearer sk_live_... albo X-API-Key. Sprawdź też, czy Twoja biblioteka HTTP nie gubi nagłówków przy przekierowaniu, bo część z nich robi to domyślnie.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-missing-key",
  "title": "Missing API key",
  "status": 401,
  "detail": "Send the key in an Authorization: Bearer <key> or X-API-Key header.",
  "instance": "/v1/grants"
}

401Invalid API key

problem-invalid-key

Klucz nie przechodzi kontroli formatu albo sumy kontrolnej, albo nie ma go w naszej bazie. Oba przypadki dają tę samą odpowiedź, żeby po niej nie dało się rozpoznać, który klucz istnieje.

Porównaj klucz znak po znaku z tym, co panel pokazał przy tworzeniu. Najczęstsze przyczyny to obcięty koniec przy kopiowaniu i spacja doklejona do wartości nagłówka.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-invalid-key",
  "title": "Invalid API key",
  "status": 401,
  "detail": "The key has an invalid format or checksum.",
  "instance": "/v1/grants"
}

401API key revoked

problem-key-revoked

Klucz istnieje, ale ktoś w Twojej organizacji go unieważnił.

Wygeneruj nowy klucz w panelu partnera i podmień go w konfiguracji. Odwołania nie da się cofnąć.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-key-revoked",
  "title": "API key revoked",
  "status": 401,
  "instance": "/v1/grants"
}

401API key expired

problem-key-expired

Klucz miał datę ważności i ta data minęła. Klucze zakładane samodzielnie w panelu żyją 90 dni, klucze kontraktowe nie wygasają.

Wygeneruj nowy klucz. Jeśli integracja ma chodzić bez przerw, ustaw przypomnienie na tydzień przed datą z pola expires_at, które panel pokazuje przy każdym kluczu.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-key-expired",
  "title": "API key expired",
  "status": 401,
  "instance": "/v1/grants"
}

403Origin not allowed for this key

problem-origin-not-allowed

Klucz publikowalny pk_live_ przyszedł z domeny, której nie ma na jego liście, albo w ogóle bez nagłówka Origin.

Dopisz domenę do klucza w panelu. Jeśli wołasz z serwera, użyj klucza sekretnego: pk_live_ jest przeznaczony wyłącznie do przeglądarki.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-origin-not-allowed",
  "title": "Origin not allowed for this key",
  "status": 403,
  "instance": "/v1/grants"
}

403Partner access suspended

problem-partner-suspended

Konto partnera ma status inny niż aktywny. Blokada obejmuje wszystkie klucze tego konta naraz.

Skontaktuj się z nami. Wygenerowanie nowego klucza niczego nie zmieni, bo blokada siedzi na koncie, nie na kluczu.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-partner-suspended",
  "title": "Partner access suspended",
  "status": 403,
  "instance": "/v1/grants"
}

403Subscription lapsed

problem-subscription-lapsed

Konto partnera założone samodzielnie, a żaden jego członek nie ma już aktywnej subskrypcji. Sprawdzamy to przy każdym żądaniu, nie tylko przy zakładaniu konta.

Odnów subskrypcję na koncie któregokolwiek członka organizacji. Dostęp wraca od razu, bez wymiany klucza.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-subscription-lapsed",
  "title": "Subscription lapsed",
  "status": 403,
  "instance": "/v1/grants"
}

403Endpoint not enabled for your account

problem-endpoint-not-enabled

Trasa należy do zakresu, którego Twoje konto nie ma włączonego. Zakresy to grants, eligibility i meta.

Napisz do nas, który zakres ma zostać włączony. To zmiana po naszej stronie, nie ustawienie w panelu.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-endpoint-not-enabled",
  "title": "Endpoint not enabled for your account",
  "status": 403,
  "instance": "/v1/eligibility/run"
}

429Per-minute rate limit exceeded

problem-rate-limit-exceeded

Wyszedłeś ponad liczbę żądań na minutę. Domyślnie 60 dla klucza sekretnego i 30 dla publikowalnego, ale wartość ustalamy per partner i widać ją w nagłówku X-RateLimit-Limit.

Odczekaj tyle sekund, ile mówi Retry-After, i ponów. Zdławione żądanie nie zjada puli miesięcznej, więc ponowienie kosztuje tylko czas.

retryAfter
Liczba sekund do końca bieżącego okna. Ta sama wartość co w nagłówku Retry-After.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-rate-limit-exceeded",
  "title": "Per-minute rate limit exceeded",
  "status": 429,
  "instance": "/v1/grants",
  "retryAfter": 17
}

429Monthly quota exhausted

problem-quota-exceeded

Skończyła się pula na bieżący miesiąc. Zeruje się pierwszego dnia następnego, a dokładny moment podaje X-Quota-Reset jako czas uniksowy.

Poczekaj na nowy okres rozliczeniowy albo napisz do nas po wyższą pulę. Zanim to zrobisz, sprawdź, czy nie synchronizujesz katalogu częściej, niż potrzeba: GET /v1/grants/changes kosztuje jedno żądanie tam, gdzie pełne przejście po katalogu kosztuje kilkanaście.

retryAfter
Liczba sekund do początku nowego okresu rozliczeniowego.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-quota-exceeded",
  "title": "Monthly quota exhausted",
  "status": 429,
  "instance": "/v1/grants",
  "retryAfter": 216000
}

404Resource not found

problem-not-found

Ścieżka nie pasuje do żadnej trasy albo zasób o podanym identyfikatorze nie istnieje. Przy dotacji tytuł jest doprecyzowany na Nie znaleziono dotacji.

Sprawdź ścieżkę i identyfikator. GET /v1/grants/{id} przyjmuje UUID albo kod dotacji i odpowiada również dla dotacji po terminie, więc 404 znaczy tutaj, że rekord został wycofany albo usunięty, a nie że nabór się skończył.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-not-found",
  "title": "Resource not found",
  "status": 404,
  "detail": "No published grant with the identifier EDF-9999.",
  "instance": "/v1/grants/EDF-9999"
}

400Invalid request

problem-invalid-request

Wartość filtra spoza słownika, data w złym formacie, brak wymaganego pola albo treść, która nie jest poprawnym JSON-em.

Przeczytaj pole detail, bo mówi, który parametr jest zły. Przy wartości spoza słownika odpowiedź niesie dodatkowo pole allowed z pełną listą dopuszczalnych wartości, więc nie trzeba zgadywać ani wołać słowników osobno.

allowed
Pełna lista dopuszczalnych wartości parametru, który został odrzucony. Obecne tylko przy błędzie słownikowym.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-invalid-request",
  "title": "Invalid request",
  "status": 400,
  "detail": "Unknown voivodeship: PL-XX",
  "instance": "/v1/grants",
  "allowed": [
    "PL-DS",
    "PL-KP",
    "PL-MZ",
    "NATIONWIDE"
  ]
}

400Invalid cursor

problem-invalid-cursor

Wartość parametru cursor nie pochodzi od nas albo została uszkodzona po drodze.

Przepisuj nextCursor z poprzedniej odpowiedzi w całości i nie składaj go samodzielnie. Kursor jest nieprzezroczysty celowo, żeby zmiana sortowania nie psuła gotowych integracji. Po tym błędzie zacznij stronicowanie od początku.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-invalid-cursor",
  "title": "Invalid cursor",
  "status": 400,
  "detail": "A cursor comes from the nextCursor field of a previous response and must not be constructed by hand.",
  "instance": "/v1/grants"
}

422Unknown question in the answers

problem-unknown-question

W mapie answers jest identyfikator pytania, którego silnik tej dotacji nie zna.

Nie przenoś odpowiedzi między dotacjami, bo identyfikatory pytań są lokalne dla silnika. Pole questionIds w odpowiedzi zawiera komplet pytań tego silnika, więc widać po nim, co poszło nie tak.

unknownQuestions
Identyfikatory, których silnik nie rozpoznał.
questionIds
Wszystkie pytania, które ten silnik zna.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-unknown-question",
  "title": "Unknown question in the answers",
  "status": 422,
  "detail": "The engine does not know these questions: q9.",
  "instance": "/v1/eligibility/run",
  "unknownQuestions": [
    "q9"
  ],
  "questionIds": [
    "q1",
    "q2",
    "q3"
  ]
}

422Invalid answer

problem-invalid-answer

Odpowiedź na pytanie nie jest jedną z jego opcji. Porównanie jest dokładne co do znaku, po obcięciu białych znaków z brzegów.

Odsyłaj dokładnie te wartości, które przyszły w nextQuestion.options, zamiast tłumaczyć je na własne kody. Pole options w odpowiedzi powtarza dopuszczalne opcje pytania, które się wysypało.

questionId
Pytanie, na które odpowiedź została odrzucona.
options
Dopuszczalne odpowiedzi na to pytanie.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-invalid-answer",
  "title": "Invalid answer",
  "status": 422,
  "detail": "The answer to question q2 is not one of its options.",
  "instance": "/v1/eligibility/run",
  "questionId": "q2",
  "options": [
    {
      "id": "yes",
      "label": "Tak"
    },
    {
      "id": "no",
      "label": "Nie"
    }
  ]
}

422The answers do not form a single path

problem-contradictory-answers

Odpowiedzi dotyczą pytań, do których wybrana ścieżka nie prowadzi. Zwykle dlatego, że użytkownik cofnął się i zmienił wcześniejszą odpowiedź, a odpowiedzi z porzuconej gałęzi zostały w komplecie.

Przy cofnięciu usuwaj wszystkie odpowiedzi po zmienianym pytaniu. Pole path pokazuje, którędy przebieg faktycznie poszedł, a unreachable wymienia odpowiedzi do wyrzucenia.

unreachable
Odpowiedzi na pytania, do których ta ścieżka nie dociera.
path
Pytania faktycznie odwiedzone, w kolejności.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-contradictory-answers",
  "title": "The answers do not form a single path",
  "status": 422,
  "detail": "The answers given belong to questions this path does not reach.",
  "instance": "/v1/eligibility/run",
  "unreachable": [
    "q5"
  ],
  "path": [
    "q1",
    "q2"
  ]
}

404No eligibility engine for this grant

problem-no-engine-for-grant

Dotacja nie ma aktywnego silnika pytań.

Sprawdzaj pole hasEligibilityEngine z katalogu, zanim zaproponujesz handlowcowi pełne sprawdzenie. Dla dotacji bez silnika zostaje POST /v1/eligibility/check, czyli dopasowanie po profilu firmy.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-no-engine-for-grant",
  "title": "No eligibility engine for this grant",
  "status": 404,
  "detail": "Grant EDF-0004 has no active question engine.",
  "instance": "/v1/eligibility/run"
}

500The eligibility engine is inconsistent

problem-engine-misconfigured

Graf pytań ma wadę: pytanie bez wyjścia, krawędź do nieistniejącego pytania albo cykl. To błąd w naszych danych, nie w Twoim żądaniu.

Zgłoś nam dotację razem z polami cause i path z odpowiedzi. Ponawianie tego samego żądania nic nie da, bo wynik jest deterministyczny.

cause
Rodzaj wady: question-not-found, no-next-question, next-question-not-found, start-question-not-found albo cycle-detected.
path
Pytania odwiedzone, zanim przebieg się zatrzymał.
przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-engine-misconfigured",
  "title": "The eligibility engine is inconsistent",
  "status": 500,
  "detail": "Stopped at question q3 (no-next-question).",
  "instance": "/v1/eligibility/run",
  "cause": "no-next-question",
  "path": [
    "q1",
    "q2",
    "q3"
  ]
}

500Server error

problem-internal

Trasa wyrzuciła wyjątek.

Ponów żądanie z rosnącym odstępem. Jeśli powtarza się dłużej niż kilka minut, napisz do nas i podaj ścieżkę z pola instance oraz przybliżony czas zdarzenia.

przykład
{
  "type": "https://edofinansowania.pl/dokumentacja-api#problem-internal",
  "title": "Server error",
  "status": 500,
  "instance": "/v1/grants"
}

Kontrakt maszynowy

Ta strona jest renderowana z dokumentu OpenAPI 3.1, a test w naszym CI porównuje ten dokument z faktycznie zaimplementowanymi trasami. Jeśli coś tu przeczytasz, to tak właśnie działa API. Plik nadaje się do wrzucenia w Postmana albo generator klienta.

openapi.json