Który przepływ wybrać#
| Sytuacja | Przepływ | Co dostajesz |
|---|---|---|
| Piszę skrypt lub integrację na własne konto sprzedawcy (ERP, synchronizacja cen, wysyłki). | client_credentials | Access token na konto właściciela aplikacji. Bez refresh tokenu — po wygaśnięciu pobierasz nowy tą samą parą. |
| Buduję aplikację, z której będą korzystać inni sprzedawcy (SaaS, wtyczka do sklepu, hurtownia). | authorization_code + PKCE | Access + refresh token per sprzedawca, po jego zgodzie na ekranie premiumsupply.pl. Zakresy widoczne przy zgodzie. |
Nie obsługujemy przepływów implicit, password ani device_code. Punkt końcowy tokenów jest wspólny: POST https://api.premiumsupply.pl/oauth/token, ciało application/x-www-form-urlencoded (akceptujemy też JSON).
Identyfikatory i prefiksy#
Każdy sekret ma czytelny prefiks — po samej wartości widać w logach i skanerach wycieków, co to jest. Tokeny są nieprzezroczyste (32 losowe bajty, w bazie tylko SHA-256) — nie są JWT i nie da się z nich nic odczytać.
| Prefiks | Co to | Gdzie występuje |
|---|---|---|
psa_ | client_id aplikacji | Panel aplikacji, Basic auth, parametr client_id |
pss_ | client_secret | Pokazany raz przy rejestracji i przy rotacji. Nigdy w przeglądarce ani w repozytorium. |
ps_at_ | access token (12 h) | Authorization: Bearer ps_at_… |
ps_rt_ | refresh token (90 dni, jednorazowy) | Tylko w odpowiedzi /oauth/token dla authorization_code i refresh_token |
ps_ac_ | kod autoryzacyjny (10 min, jednorazowy) | Parametr code na redirect_uri |
client_credentials — token na własne konto#
Przepływ client_credentials
- Twoja aplikacja → api.premiumsupply.pl: grant_type=client_credentials, opcjonalnie scope (podzbiór zakresów aplikacji). Uwierzytelnienie klienta nagłówkiem Basic albo polami client_id/client_secret w ciele.
- api.premiumsupply.pl → Twoja aplikacja: Access token ważny 12 h na konto właściciela aplikacji. Brak refresh tokenu.
- Twoja aplikacja → api.premiumsupply.pl: Zwykłe żądania z tokenem. Każda odpowiedź niesie X-Trace-Id i X-RateLimit-*.
- api.premiumsupply.pl → Twoja aplikacja: Po wygaśnięciu 401 unauthorized — pobierz nowy token krokiem 1. Lepiej odnawiać prewencyjnie, przed upływem expires_in.
curl -s -X POST https://api.premiumsupply.pl/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "scope=offers:read offers:write"{
"access_token": "ps_at_Qm9ndXNfdG9rZW5fZG9rdW1lbnRhY2ppXzEyMzQ1Njc4OTA",
"token_type": "Bearer",
"expires_in": 43200,
"scope": "offers:read offers:write"
}- Basic auth:
base64(client_id:client_secret). Alternatywnieclient_idiclient_secretjako pola formularza — ale nie w URL. scopejest opcjonalny; pominięty = wszystkie zakresy aplikacji. Zakres spoza aplikacji → 400 invalid_scope.- Aplikacja o statusie
suspendednie dostaje tokenów (401 invalid_client), a tokeny już wydane przestają działać.
authorization_code + PKCE — dostęp do kont innych sprzedawców#
Sprzedawca loguje się na premiumsupply.pl, widzi nazwę Twojej aplikacji, listę zakresów słowami (nie kodami) oraz host, na który wróci, i klika „Zezwól”. Wracasz z jednorazowym kodem, który wymieniasz na parę tokenów. PKCE (S256) jest obowiązkowe — także dla aplikacji serwerowych z sekretem.
Przepływ authorization_code z PKCE S256
Krok 1: PKCE i state#
import { createHash, randomBytes } from "node:crypto";
// code_verifier: 43–128 znaków z zestawu [A-Za-z0-9-._~]; 32 losowe bajty → 43 znaki base64url
const codeVerifier = randomBytes(32).toString("base64url");
// code_challenge = base64url(SHA-256(code_verifier)) — zawsze 43 znaki
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
// state: min. 8 znaków, losowy, zapisany w sesji użytkownika do porównania po powrocie
const state = randomBytes(16).toString("base64url");Krok 2: przekierowanie na /oauth/authorize#
GET https://api.premiumsupply.pl/oauth/authorize
?response_type=code
&client_id=psa_3f9c1b2a4d5e6f708192a3b4
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&scope=offers%3Aread%20orders%3Aread%20shipments%3Awrite
&state=k3Jm9pQ2vX8sLw4T
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256| Parametr | Wymagany | Zasady |
|---|---|---|
response_type | tak | Zawsze code. |
client_id | tak | Z panelu aplikacji. Nieznany → 400 invalid_client w JSON, bez przekierowania. |
redirect_uri | tak | Jeden z adresów zarejestrowanych w aplikacji, identyczny znak w znak. Niezarejestrowany → 400 w JSON, bez przekierowania. |
scope | tak | Zakresy rozdzielone spacją, wszystkie muszą być przyznane aplikacji. Puste → invalid_scope. |
state | tak | Min. 8 znaków, losowy, wraca bez zmian. Porównaj po powrocie. |
code_challenge | tak | 43 znaki base64url = SHA-256 z code_verifier. |
code_challenge_method | tak | Tylko S256. Metoda plain nie jest przyjmowana. |
Krok 3: powrót z kodem#
https://app.example.com/oauth/callback
?code=ps_ac_dGVzdG93eV9rb2RfYXV0b3J5emFjeWpueQ
&state=k3Jm9pQ2vX8sLw4Thttps://app.example.com/oauth/callback
?error=access_denied
&state=k3Jm9pQ2vX8sLw4TKrok 4: wymiana kodu na tokeny#
curl -s -X POST https://api.premiumsupply.pl/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=$CODE" \
-d "redirect_uri=https://app.example.com/oauth/callback" \
-d "code_verifier=$CODE_VERIFIER"{
"access_token": "ps_at_c2VsbGVyX3Rva2VuX2Rva3VtZW50YWNqYV8wOTg3NjU0MzIx",
"token_type": "Bearer",
"expires_in": 43200,
"refresh_token": "ps_rt_cmVmcmVzaF90b2tlbl9kb2t1bWVudGFjamlfMTEyMjMzNDQ1NQ",
"scope": "offers:read orders:read shipments:write"
}- Kod jest jednorazowy i żyje 10 minut. Drugie użycie, inny
redirect_urialbo złycode_verifier→ 400 invalid_grant. - Zakresy tokenu = zakresy, na które sprzedawca wyraził zgodę. Sprawdzisz je przez
GET /v1/me. - Jeden sprzedawca może wyrazić zgodę wielokrotnie — każda daje niezależną parę tokenów.
Odświeżanie i rotacja refresh tokenu#
curl -s -X POST https://api.premiumsupply.pl/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH_TOKEN"{
"access_token": "ps_at_bm93eV9hY2Nlc3NfdG9rZW5fcG9fcm90YWNqaV8wMDExMjIzMw",
"token_type": "Bearer",
"expires_in": 43200,
"refresh_token": "ps_rt_bm93eV9yZWZyZXNoX3Rva2VuX3N0YXJ5X2p1el9uaWVfZHppYWxh",
"scope": "offers:read orders:read shipments:write"
}funkcja odśwież(sesja):
zablokuj(sesja) # jedno odświeżenie naraz dla tej sesji
odpowiedź = POST /oauth/token (grant_type=refresh_token, refresh_token=sesja.refresh)
jeśli odpowiedź.status == 200:
zapisz_atomowo(sesja, access = odpowiedź.access_token,
refresh = odpowiedź.refresh_token) # NAJPIERW zapis, potem użycie
w przeciwnym razie, gdy error == "invalid_grant":
oznacz(sesja, "wymaga ponownej zgody") # stary refresh już nie wróci
odblokuj(sesja)Access token wydany wcześniej pozostaje ważny do swojego wygaśnięcia — rotacja dotyczy tylko refresh tokenu. Zakresy nowej pary są identyczne z poprzednią; żeby dodać zakres, sprzedawca musi przejść zgodę ponownie.
Czasy życia#
| Co | Ważność | Uwagi |
|---|---|---|
Access token ps_at_ | 12 h (expires_in: 43200) | Ten sam czas w obu przepływach. Odnawiaj prewencyjnie, np. 5 minut przed końcem. |
Refresh token ps_rt_ | 90 dni | Jednorazowy — każde użycie wydaje nowy z nowym 90-dniowym terminem. Sprzedawca nieaktywny przez 90 dni musi wyrazić zgodę ponownie. |
Kod autoryzacyjny ps_ac_ | 10 min | Jednorazowy. Wymieniaj natychmiast po powrocie na redirect_uri. |
client_secret pss_ | bez wygasania | Rotacja ręczna w panelu. Stary sekret gaśnie natychmiast; tokeny już wydane pozostają ważne. |
| Klucz idempotencji | 24 h | Nie jest tokenem, ale ma termin — po 24 h ta sama para (aplikacja, klucz) wykona żądanie ponownie. |
Zakresy uprawnień#
Zakresy są krótkie i bez prefiksu. Aplikacja deklaruje je w panelu; token może mieć podzbiór. Sprzedawca na ekranie zgody widzi etykiety z kolumny „Co oznacza”. Kolumna „Zasoby w v1” pokazuje, co dziś wymaga danego zakresu — pozostałe zakresy są zarezerwowane dla zasobów zapowiedzianych w changelogu; proś o nie dopiero, gdy będą potrzebne.
| Zakres | Co oznacza (etykieta na ekranie zgody) | Zasoby w v1 |
|---|---|---|
offers:read | Odczyt Twoich ofert |
|
offers:write | Tworzenie i edycja ofert, zmiana cen i stanów |
|
orders:read | Odczyt zamówień i danych kupujących |
|
orders:write | Zmiana statusów zamówień | zarezerwowany |
shipments:read | Odczyt przesyłek | zarezerwowany |
shipments:write | Zgłaszanie wysyłek (kupujący dostaje powiadomienie) |
|
returns:read | Odczyt zwrotów i reklamacji | zarezerwowany |
returns:write | Obsługa zwrotów i reklamacji | zarezerwowany |
pricelists:read | Odczyt cenników dostaw | zarezerwowany |
pricelists:write | Zarządzanie cennikami dostaw | zarezerwowany |
policies:read | Odczyt warunków zwrotów, reklamacji i gwarancji | zarezerwowany |
policies:write | Zarządzanie warunkami zwrotów, reklamacji i gwarancji | zarezerwowany |
settings:read | Odczyt ustawień sprzedaży | zarezerwowany |
settings:write | Zmiana ustawień sprzedaży | zarezerwowany |
billing:read | Odczyt salda i opłat | zarezerwowany |
messaging | Wiadomości z kupującymi | zarezerwowany |
profile:read | Odczyt danych Twojego konta | zarezerwowany |
GET /v1/me nie wymaga zakresu. Brak zakresu na zasobie → 403 insufficient_scope z nazwą brakującego zakresu w message.
Kiedy token traci ważność#
- Upływ czasu — 12 h (access), 90 dni (refresh), 10 min (kod).
- Użycie refresh tokenu — odwołuje ten konkretny refresh token (rotacja). Access tokeny wydane wcześniej działają do wygaśnięcia.
- Usunięcie aplikacji w panelu — natychmiast odwołuje wszystkie jej tokeny u wszystkich sprzedawców.
- Zawieszenie aplikacji (status
suspended) — tokeny przestają być akceptowane, nowe nie są wydawane. - Rotacja client_secret — nie odwołuje tokenów. To wymiana klucza, nie odcięcie integracji; na odcięcie jest usunięcie aplikacji.
Panel aplikacji (apps.developer.premiumsupply.pl ↗) pokazuje aktywne tokeny per rodzaj z ostatnim użyciem, adresem IP i User-Agentem — to widok „kto ma dostęp do mojego sklepu”. Ekran zgody dla sprzedawców znajduje się pod https://premiumsupply.pl/oauth/consent i jest wywoływany wyłącznie przez /oauth/authorize.
Błędy OAuth#
Punkty /oauth/* zwracają błędy w kształcie RFC 6749: { error, error_description, traceId }. Kody invalid_client, invalid_grant, invalid_scope, invalid_request, unsupported_grant_type, access_denied i unsupported_response_type są opisane w katalogu błędów. Pozostałe zasoby (/v1/*) używają wspólnego kształtu { traceId, errors[] }.