Flow Procurement · Dokumentacja API

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

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

PoleWartośćOpis
expires_in86400 (24h)Domyślny TTL. Konfigurable via FLOW_JWT_TTL_SECONDS
refresh_tokenPOST /auth/refresh z aktualnym tokenem zwraca nowy z odświeżonym TTL
AlgorytmHS256Symmetric, secret z FLOW_JWT_SECRET (≥32 chars enforced)
⚠️ Bezpieczeństwo: token ma 24h TTL i nie ma odwoływania (revocation list) — w razie kradzieży token jest ważny do wygaśnięcia. Dla produkcji enterprise rozważ HttpOnly cookie + krótszy TTL (15 min) + refresh token w osobnym secure cookie. Roadmap.

Tenant context

Flow Procurement jest multi-tenant — każda firma ma własny tenant. tenant_id jest:

  1. Wpisany w JWT przy login (user.tenant_id)
  2. Czytany przez TenantContextMiddleware z X-Tenant-ID headera (tylko super_admin może go przesłonić)
  3. 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

StatusZnaczeniePrzykład
200OKSukces, response w body
400Bad Request{"detail": "NIP musi mieć 10 cyfr"}
401UnauthorizedBrak / nieprawidłowy / wygasły token
403ForbiddenToken OK, ale rola nie ma uprawnień do akcji
404Not FoundResource nie istnieje (lub należy do innego tenanta)
409ConflictDuplikat (np. SKU już istnieje w katalogu)
422ValidationPydantic — pola brakuje / zły typ
429Rate LimitDefault: 100 req/min per IP per endpoint group
500Server ErrorBug 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)

IDConstraintDefault
C1Pełne pokrycie zapotrzebowaniahard
C2-C5SLA / on-time / lead time boundsoft
C8Single-source max share0.8
C13ESG floor0 (off)
C14Contract lock-in (hard)off
C15Preferred-share min0 (off)

Buyer · Zamówienia + PO

GET/api/v1/buying/orders — lista wszystkich zamówień tenant'a
GET/api/v1/buying/orders/{id} — szczegóły zamówienia + PO breakdown
POST/api/v1/buying/orders/{id}/checkout — generuj PO z optimized allocation
POST/api/v1/buying/orders/{id}/cancel — anuluj (z reason)

Buyer/Manager · Zatwierdzanie

Approval thresholds skonfigurowane per tenant (tab Reguły w admin):

Próg PLNWymagani approverzy
< 5 000auto-approved
5 000 – 25 000manager
25 000 – 100 000manager + director
> 100 000manager + director + cfo
curl -X POST $BASE/api/v1/buying/orders/$ORDER_ID/approve \
  -H "Authorization: Bearer $MANAGER_TOKEN"

Supplier · Moje zamówienia

GET/portal/orders — moje PO (filtrowane po supplier_id z JWT)
GET/portal/orders/{id} — szczegóły PO (404 jeśli nie moje)
GET/portal/dashboard — KPI: total_orders, pending_confirm, on_time_rate

Supplier · Mój katalog (publishing)

Każdy dostawca może publikować swoje produkty bezpośrednio do wspólnego katalogu, widocznego dla wszystkich kupujących.

GET/portal/catalog — moje pozycje + metryki (views/cart/orders)
POST/portal/catalog — opublikuj nowy produkt
PUT/portal/catalog/{id} — edytuj cenę / opis / status
DELETE/portal/catalog/{id} — usuń pozycję
POST/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)

📨 KSeF (Krajowy System e-Faktur): obowiązkowy w Polsce od 2026 dla firm z obrotem >200 mln zł. Faktura FA-2 XML musi być wysłana do MF w 24h. Flow Procurement obsługuje wysyłkę natywnie.
POST/api/v1/buying/ksef/send-invoice — wyślij fakturę do KSeF
GET/api/v1/buying/ksef/status/{ref} — status (sent/upo_received/rejected)
GET/api/v1/buying/ksef/messages — lista wszystkich wysłanych
ksef = 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
livereal 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.

POST/api/v1/buying/whitelist/refresh-now — admin trigger (sprawdza wszystkich active suppliers)
GET/api/v1/buying/whitelist/alerts — lista supplier'ów z VAT wykreślony

Compliance PL · Audit + JPK_FA export

GET/api/v1/audit/timeline?limit=50&include_violations=true — UNION audit + DECLARE + BPMN violations
GET/api/v1/audit/export-html — KAS-ready HTML z HMAC signature
GET/api/v1/audit/export-jpk-fa — JPK_FA XML (struktura MF)

Compliance · DECLARE rules

LTL temporal rules nad event log (P2P process). 4 typy:

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

GET/admin/users — lista userów w tenancie
POST/admin/users — invite (POST {email, role, supplier_id?})
POST/admin/users/{id}/reset-password — generuje 12-char temp + must_change_password=true

Webhooks (planned 2026 Q3)

🚧 In progress: webhooks dla event'ów 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ęzykStatusRepo
Pythonv0.3 (wczesna wersja)pip install flow-procurement-client (Q3 2026)
Node.js / TypeScriptv0.2npm install @flow-procurement/client (Q3 2026)
OpenAPI Generatordziała już terazopenapi-generator-cli generate -i $BASE/openapi.json -g python -o ./client
Postman Collectionw przygotowaniuImportuj $BASE/openapi.json bezpośrednio do Postmana

Pomoc i kontakt

📧 [email protected] · 💬 GitHub Issues · 📚 Swagger UI z live tryout

Flow Procurement Platform · v2026.17.86+ · OpenAPI spec · Made in 🇵🇱 Warsaw