Przejdź do treści

Szybki start

Sześć kroków od klucza testowego do opłaconej płatności testowej i odebranego webhooka. Potrzebujesz konta Placenie (bez karty) i terminala albo dowolnego języka z klientem HTTP. Bez umowy z operatorem, bez pieniędzy.

1. Konto i klucz testowy #

  1. Załóż konto przyciskiem Załóż konto w nagłówku. Wystarczy adres e-mail, potwierdzasz go kodem z wiadomości.
  2. W panelu otwórz Integracja i klucze i przy pozycji Klucz testowy kliknij Wydaj klucz.
  3. Skopiuj klucz plc_test_… od razu. Pokazujemy go tylko raz, w bazie trzymamy wyłącznie jego skrót. Jeśli go zgubisz, kliknij Wymień klucz: poprzedni przestaje działać natychmiast.

Klucz testowy działa od założenia konta. Konto bez opłaconego planu ma 30 płatności testowych, opłacony plan zdejmuje ten limit. Sprawdź klucz jednym zapytaniem o listę płatności.

Terminal
# Klucz pokazuje sie w panelu tylko raz; trzymaj go w zmiennej srodowiskowej, nie w repozytorium
export PLACENIE_KLUCZ="plc_test_..."

curl "https://placenie.pl/api/v1/platnosci?limit=1" \
  -H "Authorization: Bearer $PLACENIE_KLUCZ"
Odpowiedź 200
{
  "obiekt": "lista",
  "adres": "/v1/platnosci",
  "dane": [],
  "ma_wiecej": false,
  "strona": 1,
  "stron": 1,
  "razem": 0
}

2. Utwórz płatność #

Jedyne wymagane pole to kwota w groszach (12900 to 129,00 zł). numer_zamowienia chroni przed podwójną płatnością: ten sam numer z tą samą kwotą zwraca istniejącą płatność (200 zamiast 201), ten sam numer z inną kwotą to 409 numer_zamowienia_zajety. Nagłówek Idempotency-Key dodatkowo zabezpiecza ponowienia po przerwaniu połączenia.

W odpowiedzi dostajesz obiekt platnosc. Zapisz id przy zamówieniu, a klienta przekieruj na adres_platnosci.

curl -X POST "https://placenie.pl/api/v1/platnosci" \
  -H "Authorization: Bearer plc_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c" \
  -d '{
      "kwota": 12900,
      "waluta": "PLN",
      "numer_zamowienia": "ZAM-2026-001",
      "opis": "Zamówienie ZAM-2026-001",
      "email_klienta": "[email protected]",
      "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
      "metadane": {
          "koszyk": "c_8812",
          "klient_crm": "4471"
      }
  }'
$ch = curl_init('https://placenie.pl/api/v1/platnosci');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer plc_test_...',
        'Content-Type: application/json',
        'Idempotency-Key: 0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'kwota' => 12900,
        'waluta' => 'PLN',
        'numer_zamowienia' => 'ZAM-2026-001',
        'opis' => 'Zamówienie ZAM-2026-001',
        'email_klienta' => '[email protected]',
        'adres_powrotu' => 'https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy',
        'metadane' => [
            'koszyk' => 'c_8812',
            'klient_crm' => '4471',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);

$platnosc = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($platnosc['blad']['kod'].': '.$platnosc['blad']['opis']);
}
const odpowiedz = await fetch('https://placenie.pl/api/v1/platnosci', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer plc_test_...',
    'Content-Type': 'application/json',
    'Idempotency-Key': '0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c',
  },
  body: JSON.stringify({
    kwota: 12900,
    waluta: 'PLN',
    numer_zamowienia: 'ZAM-2026-001',
    opis: 'Zamówienie ZAM-2026-001',
    email_klienta: '[email protected]',
    adres_powrotu: 'https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy',
    metadane: {
      koszyk: 'c_8812',
      klient_crm: '4471',
    },
  }),
});

const platnosc = await odpowiedz.json();

if (!odpowiedz.ok) {
  throw new Error(`${platnosc.blad.kod}: ${platnosc.blad.opis}`);
}
import requests

odpowiedz = requests.post(
    "https://placenie.pl/api/v1/platnosci",
    headers={"Authorization": "Bearer plc_test_...", "Idempotency-Key": "0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c"},
    json={
        "kwota": 12900,
        "waluta": "PLN",
        "numer_zamowienia": "ZAM-2026-001",
        "opis": "Zamówienie ZAM-2026-001",
        "email_klienta": "[email protected]",
        "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
        "metadane": {
            "koszyk": "c_8812",
            "klient_crm": "4471",
        },
    },
)
platnosc = odpowiedz.json()

if not odpowiedz.ok:
    raise RuntimeError(f"{platnosc['blad']['kod']}: {platnosc['blad']['opis']}")
Odpowiedź 201
{
    "obiekt": "platnosc",
    "id": "tr_8h2k1j0f9e8d7c6b5a4z",
    "tryb": "test",
    "status": "nowa",
    "kwota": 12900,
    "waluta": "PLN",
    "zwrocono": 0,
    "do_zwrotu": 0,
    "numer_zamowienia": "ZAM-2026-001",
    "opis": "Zamówienie ZAM-2026-001",
    "email_klienta": "[email protected]",
    "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
    "metoda": null,
    "metadane": {
        "koszyk": "c_8812",
        "klient_crm": "4471"
    },
    "adres_platnosci": "https://placenie.pl/zaplac/tr_8h2k1j0f9e8d7c6b5a4z",
    "utworzona": "2026-10-03T10:00:00+00:00",
    "oplacona": null,
    "zwroty": []
}

3. Opłać ją w banku testowym #

Otwórz adres_platnosci w przeglądarce. Ekran płatności wygląda tak samo jak w produkcji, ale zamiast banku prowadzi do banku testowego.

  • BLIK: kod 777777 zatwierdza płatność, kod 111111 kończy się odmową banku (status odrzucona, klient może spróbować ponownie z tego samego adresu).
  • Karta i szybki przelew: bank testowy pokazuje dwa przyciski, Zatwierdź płatność i Odrzuć w banku. Numer karty nie jest potrzebny.

Do testów automatycznych (CI) nie potrzebujesz przeglądarki: POST /v1/platnosci/{id}/symulacja ustawia wynik tą samą drogą co bank testowy, więc powstaje zdarzenie i idzie webhook. Działa tylko kluczem testowym i tylko dla płatności w stanie nowa albo odrzucona.

curl -X POST "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/symulacja" \
  -H "Authorization: Bearer plc_test_..." \
  -H "Content-Type: application/json" \
  -d '{
      "wynik": "oplacona",
      "metoda": "blik"
  }'
$ch = curl_init('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/symulacja');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer plc_test_...',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'wynik' => 'oplacona',
        'metoda' => 'blik',
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);

$platnosc = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($platnosc['blad']['kod'].': '.$platnosc['blad']['opis']);
}
const odpowiedz = await fetch('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/symulacja', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer plc_test_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    wynik: 'oplacona',
    metoda: 'blik',
  }),
});

const platnosc = await odpowiedz.json();

if (!odpowiedz.ok) {
  throw new Error(`${platnosc.blad.kod}: ${platnosc.blad.opis}`);
}
import requests

odpowiedz = requests.post(
    "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/symulacja",
    headers={"Authorization": "Bearer plc_test_..."},
    json={
        "wynik": "oplacona",
        "metoda": "blik",
    },
)
platnosc = odpowiedz.json()

if not odpowiedz.ok:
    raise RuntimeError(f"{platnosc['blad']['kod']}: {platnosc['blad']['opis']}")

4. Odbierz webhook #

W panelu, w Integracja i klucze, wpisz adres odbiornika w polu Webhook o statusie i skopiuj Sekret podpisu. Adres musi być https na publicznym hoście; do testów na własnym komputerze użyj tunelu (na przykład cloudflared albo ngrok), bo adresy lokalne i prywatne są odrzucane.

Po zaksięgowaniu płatności wysyłamy POST ze zdarzeniem platnosc.oplacona. Treść to obiekt zdarzenie z pełną płatnością w polu transakcja. Odbiornik sprawdza podpis, przetwarza zdarzenie idempotentnie po id i odpowiada kodem 2xx w ciągu 8 sekund.

Nagłówki żądania
POST /placenie/webhook HTTP/1.1
Host: twojsklep.pl
Content-Type: application/json
User-Agent: Placenie-Webhook/1
Placenie-Event-Id: evt_0a1b2c3d4e5f6g7h8i9j
Placenie-Event-Type: platnosc.oplacona
Placenie-Signature: t=1759490472,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8c2
Treść żądania
{
    "obiekt": "zdarzenie",
    "id": "evt_0a1b2c3d4e5f6g7h8i9j",
    "typ": "platnosc.oplacona",
    "tryb": "test",
    "utworzone": "2026-10-03T10:01:12+00:00",
    "transakcja": {
        "obiekt": "platnosc",
        "id": "tr_8h2k1j0f9e8d7c6b5a4z",
        "tryb": "test",
        "status": "oplacona",
        "kwota": 12900,
        "waluta": "PLN",
        "zwrocono": 0,
        "do_zwrotu": 12900,
        "numer_zamowienia": "ZAM-2026-001",
        "opis": "Zamówienie ZAM-2026-001",
        "email_klienta": "[email protected]",
        "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
        "metoda": "blik",
        "metadane": {
            "koszyk": "c_8812",
            "klient_crm": "4471"
        },
        "adres_platnosci": "https://placenie.pl/zaplac/tr_8h2k1j0f9e8d7c6b5a4z",
        "utworzona": "2026-10-03T10:00:00+00:00",
        "oplacona": "2026-10-03T10:01:12+00:00",
        "zwroty": []
    }
}

Minimalny odbiornik w trzech językach. Funkcje zdarzenieJuzPrzetworzone, oznaczZamowienieJakoOplacone i zapiszPrzetworzone to Twoja baza danych. Szczegóły podpisu, ponowień i testowania są na stronie Webhooki.

<?php
// Odbiornik webhooka Placenie: sprawdzenie podpisu i odpowiedz 200.
// $sekret to „Sekret podpisu” z panelu (Integracja i klucze), trzymany w konfiguracji.

function placenieSprawdzPodpis(string $naglowek, string $tresc, string $sekret, int $tolerancja = 300): bool
{
    $czas = null;
    $podpisy = [];

    foreach (explode(',', $naglowek) as $czesc) {
        [$k, $v] = array_pad(explode('=', trim($czesc), 2), 2, null);

        if ($k === 't' && ctype_digit((string) $v)) {
            $czas = (int) $v;
        } elseif ($k === 'v1' && $v !== null) {
            $podpisy[] = $v;
        }
    }

    if ($czas === null || $podpisy === [] || abs(time() - $czas) > $tolerancja) {
        return false;
    }

    $oczekiwany = hash_hmac('sha256', $czas.'.'.$tresc, $sekret);

    foreach ($podpisy as $p) {
        if (hash_equals($oczekiwany, $p)) {
            return true;
        }
    }

    return false;
}

$tresc = file_get_contents('php://input');
$sekret = getenv('PLACENIE_WEBHOOK_SEKRET');

if (! placenieSprawdzPodpis($_SERVER['HTTP_PLACENIE_SIGNATURE'] ?? '', $tresc, $sekret)) {
    http_response_code(400);
    exit('Nieprawidłowy podpis');
}

$zdarzenie = json_decode($tresc, true);

// Idempotencja: ten sam identyfikator zdarzenia moze przyjsc wiecej niz raz.
if (zdarzenieJuzPrzetworzone($zdarzenie['id'])) {
    http_response_code(200);
    exit;
}

$transakcja = $zdarzenie['transakcja'];

if ($zdarzenie['typ'] === 'platnosc.oplacona') {
    oznaczZamowienieJakoOplacone($transakcja['numer_zamowienia'], $transakcja['id'], $transakcja['kwota']);
}

zapiszPrzetworzone($zdarzenie['id']);
http_response_code(200);
// Odbiornik webhooka Placenie (Node 18+, Express). Potrzebna SUROWA tresc zadania,
// wiec express.raw zamiast express.json: podpis liczy sie z bajtow, nie z obiektu.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCJA_S = 300;

export function sprawdzPodpis(naglowek, tresc, sekret) {
  const czesci = Object.fromEntries(
    String(naglowek || '')
      .split(',')
      .map((c) => c.trim().split('=', 2)),
  );
  const czas = Number(czesci.t);
  if (!Number.isInteger(czas) || !czesci.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - czas) > TOLERANCJA_S) return false;

  const oczekiwany = createHmac('sha256', sekret).update(`${czas}.${tresc}`).digest('hex');
  const a = Buffer.from(oczekiwany, 'hex');
  const b = Buffer.from(czesci.v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

const app = express();

app.post('/placenie/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const tresc = req.body.toString('utf8');

  if (!sprawdzPodpis(req.get('Placenie-Signature'), tresc, process.env.PLACENIE_WEBHOOK_SEKRET)) {
    return res.status(400).send('Nieprawidłowy podpis');
  }

  const zdarzenie = JSON.parse(tresc);

  // Idempotencja po identyfikatorze zdarzenia (ten sam w naglowku Placenie-Event-Id).
  if (await zdarzenieJuzPrzetworzone(zdarzenie.id)) return res.sendStatus(200);

  if (zdarzenie.typ === 'platnosc.oplacona') {
    const t = zdarzenie.transakcja;
    await oznaczZamowienieJakoOplacone(t.numer_zamowienia, t.id, t.kwota);
  }

  await zapiszPrzetworzone(zdarzenie.id);
  res.sendStatus(200);
});
# Odbiornik webhooka Placenie (Flask). Podpis liczy sie z surowych bajtow zadania.
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

TOLERANCJA_S = 300
app = Flask(__name__)


def sprawdz_podpis(naglowek: str, tresc: bytes, sekret: str) -> bool:
    czesci = dict(c.strip().split("=", 1) for c in (naglowek or "").split(",") if "=" in c)
    try:
        czas = int(czesci["t"])
        podpis = czesci["v1"]
    except (KeyError, ValueError):
        return False
    if abs(int(time.time()) - czas) > TOLERANCJA_S:
        return False
    oczekiwany = hmac.new(sekret.encode(), f"{czas}.".encode() + tresc, hashlib.sha256).hexdigest()
    return hmac.compare_digest(oczekiwany, podpis)


@app.post("/placenie/webhook")
def webhook():
    tresc = request.get_data()
    if not sprawdz_podpis(request.headers.get("Placenie-Signature", ""), tresc, os.environ["PLACENIE_WEBHOOK_SEKRET"]):
        abort(400)

    zdarzenie = json.loads(tresc)

    # Idempotencja po identyfikatorze zdarzenia (ten sam w naglowku Placenie-Event-Id).
    if zdarzenie_juz_przetworzone(zdarzenie["id"]):
        return "", 200

    if zdarzenie["typ"] == "platnosc.oplacona":
        t = zdarzenie["transakcja"]
        oznacz_zamowienie_jako_oplacone(t["numer_zamowienia"], t["id"], t["kwota"])

    zapisz_przetworzone(zdarzenie["id"])
    return "", 200

5. Sprawdź stan zapytaniem #

Webhook jest drogą główną, ale stan płatności możesz pobrać w każdej chwili. Po powrocie klienta na adres_powrotu (dopisujemy ?placenie_id=tr_…) odczytaj status zapytaniem, zamiast ufać samemu powrotowi: klient może zamknąć kartę przed powrotem albo wrócić, zanim bank zaksięguje.

Statusy: nowa, oczekuje (tylko produkcja), oplacona, odrzucona, zwrocona. Każdą zmianę stanu znajdziesz też na liście GET /v1/zdarzenia, także gdy nie masz webhooka.

curl "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z" \
  -H "Authorization: Bearer plc_test_..."
$ch = curl_init('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer plc_test_...',
    ],
    CURLOPT_RETURNTRANSFER => true,
]);

$platnosc = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($platnosc['blad']['kod'].': '.$platnosc['blad']['opis']);
}
const odpowiedz = await fetch('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer plc_test_...',
  },
});

const platnosc = await odpowiedz.json();

if (!odpowiedz.ok) {
  throw new Error(`${platnosc.blad.kod}: ${platnosc.blad.opis}`);
}
import requests

odpowiedz = requests.get(
    "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z",
    headers={"Authorization": "Bearer plc_test_..."},
)
platnosc = odpowiedz.json()

if not odpowiedz.ok:
    raise RuntimeError(f"{platnosc['blad']['kod']}: {platnosc['blad']['opis']}")
Odpowiedź 200
{
    "obiekt": "platnosc",
    "id": "tr_8h2k1j0f9e8d7c6b5a4z",
    "tryb": "test",
    "status": "oplacona",
    "kwota": 12900,
    "waluta": "PLN",
    "zwrocono": 0,
    "do_zwrotu": 12900,
    "numer_zamowienia": "ZAM-2026-001",
    "opis": "Zamówienie ZAM-2026-001",
    "email_klienta": "[email protected]",
    "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
    "metoda": "blik",
    "metadane": {
        "koszyk": "c_8812",
        "klient_crm": "4471"
    },
    "adres_platnosci": "https://placenie.pl/zaplac/tr_8h2k1j0f9e8d7c6b5a4z",
    "utworzona": "2026-10-03T10:00:00+00:00",
    "oplacona": "2026-10-03T10:01:12+00:00",
    "zwroty": []
}

6. Zwrot i co dalej #

Zwrot pełny (puste ciało) albo częściowy (kwota w groszach) zlecasz jednym wywołaniem. Odpowiedź to płatność z listą zwroty, a do odbiornika idzie zdarzenie platnosc.zwrot. Po pełnym zwrocie status płatności to zwrocona.

curl -X POST "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/zwroty" \
  -H "Authorization: Bearer plc_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c" \
  -d '{
      "kwota": 2900
  }'
$ch = curl_init('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/zwroty');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer plc_test_...',
        'Content-Type: application/json',
        'Idempotency-Key: 0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'kwota' => 2900,
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);

$platnosc = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($status >= 400) {
    throw new RuntimeException($platnosc['blad']['kod'].': '.$platnosc['blad']['opis']);
}
const odpowiedz = await fetch('https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/zwroty', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer plc_test_...',
    'Content-Type': 'application/json',
    'Idempotency-Key': '0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c',
  },
  body: JSON.stringify({
    kwota: 2900,
  }),
});

const platnosc = await odpowiedz.json();

if (!odpowiedz.ok) {
  throw new Error(`${platnosc.blad.kod}: ${platnosc.blad.opis}`);
}
import requests

odpowiedz = requests.post(
    "https://placenie.pl/api/v1/platnosci/tr_8h2k1j0f9e8d7c6b5a4z/zwroty",
    headers={"Authorization": "Bearer plc_test_...", "Idempotency-Key": "0f9d4c2a-6b3e-4e1a-9c7d-2b8f1e3a5d6c"},
    json={
        "kwota": 2900,
    },
)
platnosc = odpowiedz.json()

if not odpowiedz.ok:
    raise RuntimeError(f"{platnosc['blad']['kod']}: {platnosc['blad']['opis']}")
Odpowiedź 201
{
    "obiekt": "platnosc",
    "id": "tr_8h2k1j0f9e8d7c6b5a4z",
    "tryb": "test",
    "status": "oplacona",
    "kwota": 12900,
    "waluta": "PLN",
    "zwrocono": 2900,
    "do_zwrotu": 10000,
    "numer_zamowienia": "ZAM-2026-001",
    "opis": "Zamówienie ZAM-2026-001",
    "email_klienta": "[email protected]",
    "adres_powrotu": "https://twojsklep.pl/zamowienie/ZAM-2026-001/dziekujemy",
    "metoda": "blik",
    "metadane": {
        "koszyk": "c_8812",
        "klient_crm": "4471"
    },
    "adres_platnosci": "https://placenie.pl/zaplac/tr_8h2k1j0f9e8d7c6b5a4z",
    "utworzona": "2026-10-03T10:00:00+00:00",
    "oplacona": "2026-10-03T10:01:12+00:00",
    "zwroty": [
        {
            "obiekt": "zwrot",
            "id": "zw_9f8e7d6c5b4a3z2y1x0w",
            "platnosc": "tr_8h2k1j0f9e8d7c6b5a4z",
            "kwota": 2900,
            "waluta": "PLN",
            "status": "wykonany",
            "zrodlo": "api",
            "utworzony": "2026-10-03T12:30:00+00:00"
        }
    ]
}