Przegląd
Flow Procurement REST API — JSON over HTTPS, JWT-auth, multi-tenant. Wszystkie endpointy pod /api/v1/ (z paroma wyjątkami: /auth/*, /admin/*, /superadmin/* bez prefiksu).
Base URL
https://flow-procurement.up.railway.app
http://localhost:8000
Format danych
- Request:
Content-Type: application/json - Response:
application/json(z wyjątkiem/audit/export-htmli/health/*) - Daty: ISO-8601 UTC (np.
2026-04-26T19:32:00Z) - Waluty: PLN domyślnie, multi-currency (EUR/USD/GBP/CHF/CZK/JPY) per pole
currency - NIP: 10 cyfr bez separatorów (np.
"5260250274") - UNSPSC: 8 cyfr (np.
"25101500"= Hamulce i układy)
Wersjonowanie
Tesla-style YYYY.WW.BUILD.PATCH (np. 2026.17.86.0) — bumpowane automatycznie przy każdym push do main. Wersja widoczna w GET /health/ready response field version. Breaking changes sygnalizujemy przed deploymentem przez Slack/email + 30-dniowy deprecation window.
Uwierzytelnianie
JWT Bearer token. Każdy request (poza /auth/login, /health, /docs) wymaga headera Authorization: Bearer <token>.
1. Login
curl -X POST https://flow-procurement.up.railway.app/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"buyer","password":"buyer123"}'
# Response:
# {
# "access_token": "eyJhbGc...",
# "token_type": "bearer",
# "expires_in": 86400,
# "user": {"username":"buyer","role":"buyer","tenant_id":"demo","must_change_password":false}
# }
import requests
BASE = "https://flow-procurement.up.railway.app"
resp = requests.post(f"{BASE}/auth/login", json={
"username": "buyer",
"password": "buyer123"
})
data = resp.json()
TOKEN = data["access_token"]
print("Logged in as", data["user"]["username"], "role:", data["user"]["role"])
# Use the token for all subsequent requests:
headers = {"Authorization": f"Bearer {TOKEN}"}
me = requests.get(f"{BASE}/auth/me", headers=headers).json()
print(me)
const BASE = "https://flow-procurement.up.railway.app";
const login = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "buyer", password: "buyer123" })
});
const { access_token, user } = await login.json();
console.log("Logged in as", user.username, "role:", user.role);
// Use the token for all subsequent requests:
const me = await fetch(`${BASE}/auth/me`, {
headers: { Authorization: `Bearer ${access_token}` }
}).then(r => r.json());
console.log(me);
2. Token lifetime
| Pole | Wartość | Opis |
|---|---|---|
expires_in | 86400 (24h) | Domyślny TTL. Konfigurable via FLOW_JWT_TTL_SECONDS |
refresh_token | — | POST /auth/refresh z aktualnym tokenem zwraca nowy z odświeżonym TTL |
| Algorytm | HS256 | Symmetric, secret z FLOW_JWT_SECRET (≥32 chars enforced) |
Tenant context
Flow Procurement jest multi-tenant — każda firma ma własny tenant. tenant_id jest:
- Wpisany w JWT przy login (
user.tenant_id) - Czytany przez
TenantContextMiddlewarezX-Tenant-IDheadera (tylko super_admin może go przesłonić) - Filtruje wszystkie zapytania DB (suppliers, orders, catalog) tenant-isolated
Cross-tenant reads zwracają 404 Not Found (nie 403 Forbidden) — celowo, żeby nie wyjawiać czy zasób istnieje w innym tenant.
Błędy + rate limity
| Status | Znaczenie | Przykład |
|---|---|---|
200 | OK | Sukces, response w body |
400 | Bad Request | {"detail": "NIP musi mieć 10 cyfr"} |
401 | Unauthorized | Brak / nieprawidłowy / wygasły token |
403 | Forbidden | Token OK, ale rola nie ma uprawnień do akcji |
404 | Not Found | Resource nie istnieje (lub należy do innego tenanta) |
409 | Conflict | Duplikat (np. SKU już istnieje w katalogu) |
422 | Validation | Pydantic — pola brakuje / zły typ |
429 | Rate Limit | Default: 100 req/min per IP per endpoint group |
500 | Server Error | Bug w backendzie — zalogowany w Sentry |
Wszystkie błędy mają jednolite body: {"detail": "<message>"} (FastAPI default).
Buyer · Koszyk + intake
POST /api/v1/copilot/conversational-intake
Polski natural language → lista pozycji + UNSPSC + dostawca. AI parsuje zdanie i auto-resolves brand/qty/budget/deadline.
curl -X POST $BASE/api/v1/copilot/conversational-intake \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"100 filtrów oleju Castrol, budżet 5 tys, do końca tygodnia"}'
# Response:
# {
# "items": [{"name":"Filtr oleju Castrol","qty":100,"price_estimate":50,"unspsc":"25101504"}],
# "urgency": "med",
# "deadline": "2026-05-02",
# "category": "Oleje i filtry",
# "reply": "Dodałem do koszyka 100 sztuk filtrów Castrol..."
# }
Buyer · Optymalizacja
POST /api/v1/buying/optimize
HiGHS LP solver — multi-criteria allocation z Pareto + Monte Carlo + shadow prices.
resp = requests.post(f"{BASE}/api/v1/buying/optimize", headers=headers, json={
"items": [{"id": "BRK-PAD-0041", "qty": 100, "name": "Klocki TRW"}],
"weights": {"w_cost": 0.4, "w_time": 0.3, "w_compliance": 0.15, "w_esg": 0.15},
"constraints": {
"preferred_share_min": 0.6, # C15: min 60% wolumenu od preferred
"single_source_max": 0.8, # C8: max 80% u jednego dostawcy
"esg_min_score": 65, # C13: ESG floor
}
})
sol = resp.json()
print("Total cost:", sol["total_cost"], "savings:", sol["savings_pln"])
for alloc in sol["allocations"]:
print(f" {alloc['supplier_name']}: {alloc['allocated_qty']}× @ {alloc['unit_cost']} PLN")
Constraints (C1–C15b)
| ID | Constraint | Default |
|---|---|---|
| C1 | Pełne pokrycie zapotrzebowania | hard |
| C2-C5 | SLA / on-time / lead time bound | soft |
| C8 | Single-source max share | 0.8 |
| C13 | ESG floor | 0 (off) |
| C14 | Contract lock-in (hard) | off |
| C15 | Preferred-share min | 0 (off) |
Buyer · Zamówienia + PO
/api/v1/buying/orders — lista wszystkich zamówień tenant'a/api/v1/buying/orders/{id} — szczegóły zamówienia + PO breakdown/api/v1/buying/orders/{id}/checkout — generuj PO z optimized allocation/api/v1/buying/orders/{id}/cancel — anuluj (z reason)Buyer/Manager · Zatwierdzanie
Approval thresholds skonfigurowane per tenant (tab Reguły w admin):
| Próg PLN | Wymagani approverzy |
|---|---|
| < 5 000 | auto-approved |
| 5 000 – 25 000 | manager |
| 25 000 – 100 000 | manager + director |
| > 100 000 | manager + director + cfo |
curl -X POST $BASE/api/v1/buying/orders/$ORDER_ID/approve \ -H "Authorization: Bearer $MANAGER_TOKEN"
Supplier · Moje zamówienia
/portal/orders — moje PO (filtrowane po supplier_id z JWT)/portal/orders/{id} — szczegóły PO (404 jeśli nie moje)/portal/dashboard — KPI: total_orders, pending_confirm, on_time_rateSupplier · Mój katalog (publishing)
Każdy dostawca może publikować swoje produkty bezpośrednio do wspólnego katalogu, widocznego dla wszystkich kupujących.
/portal/catalog — moje pozycje + metryki (views/cart/orders)/portal/catalog — opublikuj nowy produkt/portal/catalog/{id} — edytuj cenę / opis / status/portal/catalog/{id} — usuń pozycję/portal/catalog/import — bulk import z CSV# Publikacja nowego produktu
new_item = requests.post(f"{BASE}/portal/catalog", headers=headers, json={
"sku": "TRW-GDB1550",
"name": "Klocki hamulcowe TRW GDB1550 (przód, VW Golf VII)",
"price": 189.50,
"currency": "PLN",
"unit": "szt",
"category": "Hamulce",
"unspsc_code": "25101500",
"manufacturer": "TRW",
"ean": "5901234567890",
"delivery_days": 2,
"min_order_qty": 1,
"status": "published"
}).json()
print("Created item id:", new_item["item"]["id"])
# Bulk import z CSV
csv_text = """sku,name,price,unit,unspsc,category
TRW-001,Klocki przód,189.50,szt,25101500,Hamulce
TRW-002,Klocki tył,159.90,szt,25101500,Hamulce"""
result = requests.post(f"{BASE}/portal/catalog/import", headers=headers, json={
"csv": csv_text
}).json()
print(f"Created: {result['created']}, updated: {result['updated']}, errors: {len(result['errors'])}")
Supplier · Confirm/Reject PO
POST /portal/orders/{order_id}/po/{po_id}/confirm
Potwierdzenie z opcjonalnym scope per linia (partial confirm). confirmed_qty <= quantity.
curl -X POST $BASE/portal/orders/ORD-123/po/PO-001/confirm \
-H "Authorization: Bearer $TRW_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirmed_lines":[{"line_id":"L1","confirmed_qty":50},{"line_id":"L2","confirmed_qty":0}]}'
POST /portal/orders/{order_id}/po/{po_id}/reject
Odrzucenie z opcjonalną kontr-ofertą:
curl -X POST $BASE/portal/orders/ORD-123/po/PO-001/reject \
-H "Authorization: Bearer $TRW_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Brak stanu",
"counter_offer": {
"alt_unit_cost": 195.00,
"alt_qty": 80,
"alt_delivery_date": "2026-05-15"
}
}'
Compliance PL · KSeF (e-faktury 2026)
/api/v1/buying/ksef/send-invoice — wyślij fakturę do KSeF/api/v1/buying/ksef/status/{ref} — status (sent/upo_received/rejected)/api/v1/buying/ksef/messages — lista wszystkich wysłanychksef = requests.post(f"{BASE}/api/v1/buying/ksef/send-invoice",
headers=headers, json={"invoice_id": 42}).json()
print("KSeF ref:", ksef["ksef_ref"], "status:", ksef["status"])
# Po chwili sprawdź UPO:
import time; time.sleep(2)
status = requests.get(f"{BASE}/api/v1/buying/ksef/status/{ksef['ksef_ref']}",
headers=headers).json()
if status["status"] == "upo_received":
print("UPO ID:", status["upo_id"])
FLOW_KSEF_BACKEND
mock | (default) deterministyczne mock-refsy KSEF-MOCK-... + auto-UPO po 1s |
live | real MF API — wymaga certyfikatu kwalifikowanego, ENV: FLOW_KSEF_CERT_PATH, FLOW_KSEF_CERT_PASS |
Compliance PL · Biała Lista VAT
Codzienny check przeciwko wl-api.mf.gov.pl. Auto-block PO generation gdy supplier traci VAT czynny.
/api/v1/buying/whitelist/refresh-now — admin trigger (sprawdza wszystkich active suppliers)/api/v1/buying/whitelist/alerts — lista supplier'ów z VAT wykreślonyCompliance PL · Audit + JPK_FA export
/api/v1/audit/timeline?limit=50&include_violations=true — UNION audit + DECLARE + BPMN violations/api/v1/audit/export-html — KAS-ready HTML z HMAC signature/api/v1/audit/export-jpk-fa — JPK_FA XML (struktura MF)Compliance · DECLARE rules
LTL temporal rules nad event log (P2P process). 4 typy:
- existence — aktywność musi się zdarzyć (np. "PO musi mieć confirmation")
- absence — aktywność NIE może się zdarzyć (np. "Brak płatności przed GR")
- response — A → B (np. "PO sent → confirmation w 7 dni")
- precedence — B tylko po A (np. "GR tylko po PO_confirmed")
curl -X POST $BASE/api/v1/compliance/rules \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"rule_id": "PO-CONFIRM-7D",
"type": "response",
"activity_a": "PO_sent",
"activity_b": "PO_confirmed",
"max_delay_hours": 168
}'
Admin · 1-click supplier onboarding
POST /admin/onboard-supplier?nip=<10digits>
Pełen profil dostawcy w <30s: VIES + KRS (osint_engine) + Biała Lista VAT + CPI risk score.
curl -X POST "$BASE/admin/onboard-supplier?nip=5260250274" \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Response (in <30s):
# {
# "success": true,
# "supplier_id": "VND-5260-274",
# "name": "Inter Cars S.A.",
# "address": "...",
# "vat_status": "active",
# "krs_active": true,
# "cpi_risk_score": 23,
# "vat_check_source": "live"
# }
Admin · Catalog import (master)
Master catalog (admin-managed) — distinct od supplier-published catalog. Import z CSV: sku,name,price,category,unit,unspsc,description. Akceptuje ; i , jako delimitery.
Admin · User management
/admin/users — lista userów w tenancie/admin/users — invite (POST {email, role, supplier_id?})/admin/users/{id}/reset-password — generuje 12-char temp + must_change_password=trueWebhooks (planned 2026 Q3)
order.approved, po.confirmed, invoice.upo_received, supplier.vat_lost. Polling przez /api/v1/notifications dostępny już teraz.
ERP push (SAP / Oracle / Workday)
Adaptery z env switch (analogicznie do KSeF):
FLOW_ERP_BACKEND=sap_s4hana # | oracle_fusion | workday | mock FLOW_ERP_BASE_URL=https://erp.client.com FLOW_ERP_OAUTH_CLIENT_ID=... FLOW_ERP_OAUTH_SECRET=...
Po approval, PO jest pushowany do ERP klienta przez REST. Sync orders → ERP, sync invoices ← ERP.
SDK / klienty
| Język | Status | Repo |
|---|---|---|
| Python | v0.3 (wczesna wersja) | pip install flow-procurement-client (Q3 2026) |
| Node.js / TypeScript | v0.2 | npm install @flow-procurement/client (Q3 2026) |
| OpenAPI Generator | działa już teraz | openapi-generator-cli generate -i $BASE/openapi.json -g python -o ./client |
| Postman Collection | w przygotowaniu | Importuj $BASE/openapi.json bezpośrednio do Postmana |
Pomoc i kontakt
📧 [email protected] · 💬 GitHub Issues · 📚 Swagger UI z live tryout