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
curl 'https://edofinansowania.pl/api/v1/grants?voivodeship=mazowieckie&installationTypes=PV' \
-H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'{
"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 }
}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.
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ń.
sk_live_9f3Kq2mVXbT8sLpR7dWnA4uZcE1yH6jG0oB5tNfQiMx1a2b3c4d
└──┬───┘└────────────────────┬────────────────────┘└──┬───┘
prefiks 43 znaki losowe (base64url) suma kontrolnaKlucz sekretny kontra publikowalny
| Klucz sekretny | Klucz publikowalny | |
|---|---|---|
| Prefiks | sk_live_ | pk_live_ |
| Gdzie działa | Wywołania serwer do serwera. Trzymaj go po stronie backendu. | Wyłącznie przeglądarka, wyłącznie ze zgłoszonych domen. |
| CORS | Brak 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. |
| Domeny | Lista 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.
| Zakres | Trasy |
|---|---|
| grants | GET /v1/grants, GET /v1/grants/changes, GET /v1/grants/{id} |
| eligibility | POST /v1/eligibility/check, POST /v1/eligibility/run |
| meta | GET /v1/meta/vocabularies |
| bez zakresu | GET /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ź
304zuż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-Afternic nie kosztuje poza czasem. GET /v1/healthnie zużywa niczego, bo kończy się przed licznikiem. Możesz go odpytywać z monitoringu tak często, jak chcesz.- Odpowiedzi
4xxi5xxz 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łówek | Znaczenie |
|---|---|
| X-RateLimit-Limit | Ile żądań na minutę dopuszcza ten klucz. |
| X-RateLimit-Remaining | Ile zostało w bieżącej minucie. |
| X-RateLimit-Reset | Czas uniksowy końca bieżącego okna minutowego. |
| X-Quota-Limit | Miesięczna pula całego konta. |
| X-Quota-Remaining | Ile zostało w tym miesiącu. |
| X-Quota-Reset | Czas uniksowy początku nowego okresu rozliczeniowego. |
| RateLimit, RateLimit-Policy | Ten 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-After | Po ilu sekundach ponowić. Tylko przy odpowiedzi 429. |
| ETag | Tylko 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.
| Kod | Etykieta |
|---|---|
| pv | Fotowoltaika |
| heat_pump | Pompa ciepła |
| wind_turbine | Turbina wiatrowa |
| energy_storage | Magazyn energii |
| biomass_boiler | Kocioł na biomasę |
| solar_thermal | Kolektory słoneczne |
| thermal_modernisation | Termomodernizacja |
| machinery | Maszyny |
| ev_charger | Ładowarka EV |
| hydroelectric | Hydroelektrownia |
| biogas_plant | Biogazownia |
{
"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ą
200podGET /v1/grants/{id}, z polemisOpen: 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ć.
| Wartość | Znaczenie |
|---|---|
| matched: true | Kryterium zostało sprawdzone i przeszło. |
| matched: false | Kryterium zostało sprawdzone i nie przeszło. |
| matched: null | Kryterium nie było sprawdzane, bo nie podałeś go w profilu. Towarzyszy mu reason: not_filtered. To nie jest zgoda ani odmowa. |
| via: exact | Dotacja wymienia to konkretne województwo. Warto to pokazać, bo brzmi mocniej w rozmowie z klientem. |
| via: nationwide | Dotacja pasuje, bo obejmuje cały kraj, a nie dlatego, że wymienia województwo klienta. |
| values | Przy instalacjach: część wspólna tego, o co pytałeś, i tego, co dotacja finansuje. |
{
"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"] }
}
}
]
}"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:incompletez polemnextQuestionalbocompletez polemverdict. - Pole
pathto pytania faktycznie odwiedzone, w kolejności. Nadaje się do zapisania w historii klienta jako ślad, na jakiej podstawie zapadł werdykt. progress.totalQuestionsjest 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.
curl 'https://edofinansowania.pl/api/v1/grants?limit=200' \
-H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'{
"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.
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.
curl 'https://edofinansowania.pl/api/v1/grants/changes?since=2026-07-30T09:00:00.000Z' \
-H 'Authorization: Bearer sk_live_TWOJ_KLUCZ'{
"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
syncedAti odeślij je jakosincenastę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
upsertedto rekordy do wstawienia albo zaktualizowania. Klucz toid, a niegrantCode. - Lista
removedmówi, co wypadło z katalogu, a polereason, co z tym zrobić.expiredto zakończony nabór, warto zachować rekord w historii klienta.unpublishedideletedto rekordy do usunięcia. Pokrywa się to z zachowaniemGET /v1/grants/{id}: dotacja wygasła nadal odpowiada200, pozostałe dwie404. - Endpoint nie stronicuje i nie obsługuje
ETag, bosyncedAtzmienia się przy każdym wywołaniu. Listaupsertedjest ucinana na 200 pozycjach, a flagatruncateddotyczy wyłącznie listyremoved. 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.
# 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/apiużyj nagłówkaX-If-None-Match. Warstwa proxy nie przekazuje standardowegoIf-None-Matchdo serwera, więc żądanie z samym nagłówkiem standardowym zawsze dostanie pełną odpowiedź. Sprawdzone na produkcji, nie wywnioskowane z dokumentacji proxy. 304oszczędza transfer, nie limit. Licznik minutowy rusza na bramce, zanim zajrzymy do bazy.ETagjest liczony z całej odpowiedzi razem ze stroną wyników, więc zmiana parametrów zapytania to innyETag. 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.
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":{}}'{
"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.
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.
{
"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ą
422z listąunreachable. - Zapisz
engine.versionrazem 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. failedConditionnadaje się do pokazania handlowcowi, bo mówi, który warunek przesądził.messagejest 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.
{
"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.
{
"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ąć.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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ł.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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ł.
{
"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.
{
"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.