Przejdź do treści

Biblioteki i przykłady

Kompletne klienty REST API do skopiowania w PHP, Node.js i Pythonie, z idempotencją, stronicowaniem kursorem i jednym wyjątkiem na błąd API. Do tego generowanie klienta ze specyfikacji OpenAPI 3.1 w dowolnym języku.

Co jest, a czego nie ma #

Nie publikujemy oficjalnych pakietów composer, npm ani pip. API ma dziesięć operacji i jeden format błędu, więc klient mieści się w jednym pliku: poniżej są trzy takie pliki, gotowe do wklejenia do projektu i do utrzymania po Twojej stronie. Każdy z nich:

  • dodaje Idempotency-Key (UUID) przy tworzeniu płatności i zwrotu,
  • zamienia odpowiedź błędu na wyjątek z polami typ, kod, pole, pola i zadanie_id,
  • przechodzi całą listę płatności stronami po kursorze po,
  • obsługuje zdarzenia, ponowienie webhooka i symulację w piaskownicy,
  • przyjmuje nagłówek Placenie-Version, gdy chcesz przypiąć wersję API.

Weryfikację podpisu webhooka w tych samych trzech językach znajdziesz na stronie Webhooki. Jeśli wolisz wygenerowany klient z typami, użyj specyfikacji OpenAPI opisanej niżej.

PHP #

PHP 8.1 lub nowszy, rozszerzenia curl i json. Bez Composera. W Laravelu możesz zamienić curl na Http::withToken(), reszta zostaje.

PlacenieKlient.php
<?php
// Klient REST API Placenie v1. Bez zaleznosci: rozszerzenie curl i json.
// Uzycie: $placenie = new PlacenieKlient(getenv('PLACENIE_KLUCZ'));

final class PlacenieBlad extends RuntimeException
{
    public function __construct(
        public readonly int $http,
        public readonly string $typ,
        public readonly string $kod,
        string $opis,
        public readonly ?string $pole = null,
        public readonly array $pola = [],
        public readonly ?string $zadanieId = null,
    ) {
        parent::__construct($opis);
    }
}

final class PlacenieKlient
{
    public function __construct(
        private readonly string $klucz,
        private readonly string $baza = 'https://placenie.pl/api/v1',
        private readonly ?string $wersja = null,
    ) {
    }

    /** POST /v1/platnosci: zwraca obiekt platnosc z adres_platnosci. */
    public function utworzPlatnosc(array $dane, ?string $kluczIdempotencji = null): array
    {
        return $this->zapytanie('POST', '/platnosci', $dane, $kluczIdempotencji ?? self::uuid());
    }

    public function pobierzPlatnosc(string $id): array
    {
        return $this->zapytanie('GET', '/platnosci/'.rawurlencode($id));
    }

    /** Zmiana opisu i metadanych (metadane sa scalane, null usuwa klucz). */
    public function zmienPlatnosc(string $id, array $dane): array
    {
        return $this->zapytanie('POST', '/platnosci/'.rawurlencode($id), $dane);
    }

    /** Zwrot pelny (bez kwoty) albo czesciowy, kwota w groszach. */
    public function zwroc(string $id, ?int $kwota = null, ?string $kluczIdempotencji = null): array
    {
        return $this->zapytanie('POST', '/platnosci/'.rawurlencode($id).'/zwroty', $kwota === null ? [] : ['kwota' => $kwota], $kluczIdempotencji ?? self::uuid());
    }

    /** Lista platnosci: filtry status, metoda, numer_zamowienia, od, do, limit, po, przed. */
    public function listaPlatnosci(array $filtry = []): array
    {
        return $this->zapytanie('GET', '/platnosci'.($filtry ? '?'.http_build_query($filtry) : ''));
    }

    /** Wszystkie platnosci pasujace do filtrow, strona po stronie (kursor `po`). */
    public function kazdaPlatnosc(array $filtry = []): Generator
    {
        $filtry['limit'] ??= 100;

        do {
            $strona = $this->listaPlatnosci($filtry);

            foreach ($strona['dane'] as $platnosc) {
                yield $platnosc;
            }

            $filtry['po'] = $strona['dane'] ? end($strona['dane'])['id'] : null;
        } while ($strona['ma_wiecej'] && $filtry['po'] !== null);
    }

    public function listaZdarzen(array $filtry = []): array
    {
        return $this->zapytanie('GET', '/zdarzenia'.($filtry ? '?'.http_build_query($filtry) : ''));
    }

    public function pobierzZdarzenie(string $id): array
    {
        return $this->zapytanie('GET', '/zdarzenia/'.rawurlencode($id));
    }

    public function ponowWebhook(string $idZdarzenia): array
    {
        return $this->zapytanie('POST', '/zdarzenia/'.rawurlencode($idZdarzenia).'/ponow', []);
    }

    /** Tylko klucz testowy: ustawia wynik platnosci bez ekranu banku testowego. */
    public function symuluj(string $id, string $wynik, ?string $metoda = null): array
    {
        return $this->zapytanie('POST', '/platnosci/'.rawurlencode($id).'/symulacja', array_filter(['wynik' => $wynik, 'metoda' => $metoda]));
    }

    private function zapytanie(string $metoda, string $sciezka, ?array $cialo = null, ?string $kluczIdempotencji = null): array
    {
        $naglowki = ['Authorization: Bearer '.$this->klucz, 'Accept: application/json'];

        if ($cialo !== null) {
            $naglowki[] = 'Content-Type: application/json';
        }

        if ($kluczIdempotencji !== null) {
            $naglowki[] = 'Idempotency-Key: '.$kluczIdempotencji;
        }

        if ($this->wersja !== null) {
            $naglowki[] = 'Placenie-Version: '.$this->wersja;
        }

        $ch = curl_init($this->baza.$sciezka);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $metoda,
            CURLOPT_HTTPHEADER => $naglowki,
            CURLOPT_POSTFIELDS => $cialo === null ? null : json_encode($cialo, JSON_UNESCAPED_UNICODE),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 20,
        ]);

        $tresc = curl_exec($ch);
        $http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

        if ($tresc === false) {
            throw new PlacenieBlad(0, 'siec', 'brak_polaczenia', curl_error($ch));
        }

        $dane = json_decode($tresc, true) ?? [];

        if ($http >= 400) {
            $b = $dane['blad'] ?? [];

            throw new PlacenieBlad($http, $b['typ'] ?? 'blad_serwera', $b['kod'] ?? 'blad_serwera', $b['opis'] ?? 'Błąd API', $b['pole'] ?? null, $b['pola'] ?? [], $b['zadanie_id'] ?? null);
        }

        return $dane;
    }

    private static function uuid(): string
    {
        $b = random_bytes(16);
        $b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
        $b[8] = chr((ord($b[8]) & 0x3f) | 0x80);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
    }
}

// Przyklad:
$placenie = new PlacenieKlient(getenv('PLACENIE_KLUCZ'));

try {
    $platnosc = $placenie->utworzPlatnosc([
        'kwota' => 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',
    ]);

    header('Location: '.$platnosc['adres_platnosci']);
} catch (PlacenieBlad $e) {
    error_log($e->kod.' ('.$e->zadanieId.'): '.$e->getMessage());
}

Node.js #

Node 18 lub nowszy (wbudowane fetch i crypto), moduł ESM. Działa też w Bun i Deno.

placenie.js
// Klient REST API Placenie v1 dla Node 18+ (wbudowane fetch i crypto). Bez zaleznosci.
// Uzycie: const placenie = new PlacenieKlient(process.env.PLACENIE_KLUCZ);
import { randomUUID } from 'node:crypto';

export class PlacenieBlad extends Error {
  constructor(http, blad = {}) {
    super(blad.opis || 'Błąd API');
    this.name = 'PlacenieBlad';
    this.http = http;
    this.typ = blad.typ;
    this.kod = blad.kod;
    this.pole = blad.pole;
    this.pola = blad.pola || {};
    this.zadanieId = blad.zadanie_id;
  }
}

export class PlacenieKlient {
  constructor(klucz, { baza = 'https://placenie.pl/api/v1', wersja } = {}) {
    this.klucz = klucz;
    this.baza = baza;
    this.wersja = wersja;
  }

  /** POST /v1/platnosci: zwraca obiekt platnosc z adres_platnosci. */
  utworzPlatnosc(dane, kluczIdempotencji = randomUUID()) {
    return this.zapytanie('POST', '/platnosci', dane, kluczIdempotencji);
  }

  pobierzPlatnosc(id) {
    return this.zapytanie('GET', `/platnosci/${encodeURIComponent(id)}`);
  }

  /** Zmiana opisu i metadanych (metadane sa scalane, null usuwa klucz). */
  zmienPlatnosc(id, dane) {
    return this.zapytanie('POST', `/platnosci/${encodeURIComponent(id)}`, dane);
  }

  /** Zwrot pelny (bez kwoty) albo czesciowy, kwota w groszach. */
  zwroc(id, kwota, kluczIdempotencji = randomUUID()) {
    return this.zapytanie('POST', `/platnosci/${encodeURIComponent(id)}/zwroty`, kwota == null ? {} : { kwota }, kluczIdempotencji);
  }

  listaPlatnosci(filtry = {}) {
    return this.zapytanie('GET', `/platnosci${this.query(filtry)}`);
  }

  /** Wszystkie platnosci pasujace do filtrow, strona po stronie (kursor `po`). */
  async *kazdaPlatnosc(filtry = {}) {
    let po;
    do {
      const strona = await this.listaPlatnosci({ limit: 100, ...filtry, po });
      yield* strona.dane;
      po = strona.dane.at(-1)?.id;
      if (!strona.ma_wiecej) return;
    } while (po);
  }

  listaZdarzen(filtry = {}) {
    return this.zapytanie('GET', `/zdarzenia${this.query(filtry)}`);
  }

  pobierzZdarzenie(id) {
    return this.zapytanie('GET', `/zdarzenia/${encodeURIComponent(id)}`);
  }

  ponowWebhook(idZdarzenia) {
    return this.zapytanie('POST', `/zdarzenia/${encodeURIComponent(idZdarzenia)}/ponow`, {});
  }

  /** Tylko klucz testowy: ustawia wynik platnosci bez ekranu banku testowego. */
  symuluj(id, wynik, metoda) {
    return this.zapytanie('POST', `/platnosci/${encodeURIComponent(id)}/symulacja`, metoda ? { wynik, metoda } : { wynik });
  }

  query(filtry) {
    const q = new URLSearchParams(Object.entries(filtry).filter(([, v]) => v !== undefined && v !== null));
    return q.size ? `?${q}` : '';
  }

  async zapytanie(metoda, sciezka, cialo, kluczIdempotencji) {
    const headers = { Authorization: `Bearer ${this.klucz}`, Accept: 'application/json' };
    if (cialo !== undefined) headers['Content-Type'] = 'application/json';
    if (kluczIdempotencji) headers['Idempotency-Key'] = kluczIdempotencji;
    if (this.wersja) headers['Placenie-Version'] = this.wersja;

    const odpowiedz = await fetch(this.baza + sciezka, {
      method: metoda,
      headers,
      body: cialo === undefined ? undefined : JSON.stringify(cialo),
    });

    const dane = await odpowiedz.json().catch(() => ({}));
    if (!odpowiedz.ok) throw new PlacenieBlad(odpowiedz.status, dane.blad);
    return dane;
  }
}

// Przyklad:
const placenie = new PlacenieKlient(process.env.PLACENIE_KLUCZ);

try {
  const platnosc = await placenie.utworzPlatnosc({
    kwota: 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',
  });
  console.log(platnosc.adres_platnosci);
} catch (e) {
  if (e instanceof PlacenieBlad) console.error(e.kod, e.zadanieId, e.message);
  else throw e;
}

Python #

Python 3.9 lub nowszy, jedyna zależność to requests (pip install requests).

placenie.py
# Klient REST API Placenie v1 dla Pythona 3.9+. Jedyna zaleznosc: requests.
# Uzycie: placenie = PlacenieKlient(os.environ["PLACENIE_KLUCZ"])
import os
import uuid
from typing import Any, Iterator, Optional

import requests


class PlacenieBlad(Exception):
    def __init__(self, http: int, blad: dict):
        super().__init__(blad.get("opis", "Błąd API"))
        self.http = http
        self.typ = blad.get("typ")
        self.kod = blad.get("kod")
        self.pole = blad.get("pole")
        self.pola = blad.get("pola") or {}
        self.zadanie_id = blad.get("zadanie_id")


class PlacenieKlient:
    def __init__(self, klucz: str, baza: str = "https://placenie.pl/api/v1", wersja: Optional[str] = None):
        self.baza = baza
        self.sesja = requests.Session()
        self.sesja.headers.update({"Authorization": f"Bearer {klucz}", "Accept": "application/json"})
        if wersja:
            self.sesja.headers["Placenie-Version"] = wersja

    def utworz_platnosc(self, dane: dict, klucz_idempotencji: Optional[str] = None) -> dict:
        """POST /v1/platnosci: zwraca obiekt platnosc z adres_platnosci."""
        return self._zapytanie("POST", "/platnosci", dane, klucz_idempotencji or str(uuid.uuid4()))

    def pobierz_platnosc(self, id: str) -> dict:
        return self._zapytanie("GET", f"/platnosci/{id}")

    def zmien_platnosc(self, id: str, dane: dict) -> dict:
        """Zmiana opisu i metadanych (metadane sa scalane, None usuwa klucz)."""
        return self._zapytanie("POST", f"/platnosci/{id}", dane)

    def zwroc(self, id: str, kwota: Optional[int] = None, klucz_idempotencji: Optional[str] = None) -> dict:
        """Zwrot pelny (bez kwoty) albo czesciowy, kwota w groszach."""
        return self._zapytanie("POST", f"/platnosci/{id}/zwroty", {} if kwota is None else {"kwota": kwota}, klucz_idempotencji or str(uuid.uuid4()))

    def lista_platnosci(self, **filtry: Any) -> dict:
        return self._zapytanie("GET", "/platnosci", params=filtry)

    def kazda_platnosc(self, **filtry: Any) -> Iterator[dict]:
        """Wszystkie platnosci pasujace do filtrow, strona po stronie (kursor `po`)."""
        filtry.setdefault("limit", 100)
        while True:
            strona = self.lista_platnosci(**filtry)
            yield from strona["dane"]
            if not strona["ma_wiecej"] or not strona["dane"]:
                return
            filtry["po"] = strona["dane"][-1]["id"]

    def lista_zdarzen(self, **filtry: Any) -> dict:
        return self._zapytanie("GET", "/zdarzenia", params=filtry)

    def pobierz_zdarzenie(self, id: str) -> dict:
        return self._zapytanie("GET", f"/zdarzenia/{id}")

    def ponow_webhook(self, id_zdarzenia: str) -> dict:
        return self._zapytanie("POST", f"/zdarzenia/{id_zdarzenia}/ponow", {})

    def symuluj(self, id: str, wynik: str, metoda: Optional[str] = None) -> dict:
        """Tylko klucz testowy: ustawia wynik platnosci bez ekranu banku testowego."""
        cialo = {"wynik": wynik}
        if metoda:
            cialo["metoda"] = metoda
        return self._zapytanie("POST", f"/platnosci/{id}/symulacja", cialo)

    def _zapytanie(self, metoda: str, sciezka: str, cialo: Optional[dict] = None, klucz_idempotencji: Optional[str] = None, params: Optional[dict] = None) -> dict:
        naglowki = {}
        if klucz_idempotencji:
            naglowki["Idempotency-Key"] = klucz_idempotencji
        odpowiedz = self.sesja.request(metoda, self.baza + sciezka, json=cialo, params=params, headers=naglowki, timeout=20)
        try:
            dane = odpowiedz.json()
        except ValueError:
            dane = {}
        if odpowiedz.status_code >= 400:
            raise PlacenieBlad(odpowiedz.status_code, dane.get("blad", {}))
        return dane


# Przyklad:
placenie = PlacenieKlient(os.environ["PLACENIE_KLUCZ"])

try:
    platnosc = placenie.utworz_platnosc({
        "kwota": 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",
    })
    print(platnosc["adres_platnosci"])
except PlacenieBlad as e:
    print(e.kod, e.zadanie_id, e)

Klient z OpenAPI #

Całe API jest opisane w specyfikacji OpenAPI 3.1 pod adresem /api/v1/openapi.json (bez klucza; 60 pobrań na minutę na adres IP). To z niej generowana jest referencja API, więc opisuje dokładnie to, co robi serwer: ścieżki, schematy obiektów, kody błędów i typy zdarzeń webhooków.

Z tej specyfikacji wygenerujesz klient w dowolnym z kilkudziesięciu języków narzędziem openapi-generator, a także zaimportujesz kolekcję do Postmana, Insomni albo Bruno.

openapi-generator
# Pobierz specyfikacje (ta sama, ktora opisuje te strone)
curl -o placenie-openapi.json https://placenie.pl/api/v1/openapi.json

# Klient PHP (inne generatory: typescript-fetch, python, java, go, csharp, ruby)
npx @openapitools/openapi-generator-cli generate \
  -i placenie-openapi.json \
  -g php \
  -o ./placenie-php \
  --additional-properties=invokerPackage=Placenie,packageName=placenie

# Klient TypeScript
npx @openapitools/openapi-generator-cli generate \
  -i placenie-openapi.json \
  -g typescript-fetch \
  -o ./placenie-ts

Platformy sklepowe i linki do zapłaty #

Jeśli sprzedajesz na gotowej platformie, nie piszesz kodu: klucze API i adres webhooka z panelu wpisujesz w ustawieniach płatności platformy. Każda platforma ma osobną stronę z krokami i kosztem.

Wtyczki Placenie do pobrania: placenie-woocommerce.zip (WordPress 6.0 i WooCommerce 7.0 lub nowsze, także koszyk blokowy) oraz placenie-prestashop.zip (PrestaShop 1.7, 8 i 9). Wtyczka tworzy płatność przez POST /v1/platnosci, sprawdza podpis webhooka i oznacza zamówienie jako opłacone dopiero po odczycie płatności z API, z kontrolą kwoty i waluty. Te same pliki są w panelu, w zakładce Integracja.

Bez sklepu i bez kodu: linki do zapłaty tworzysz w panelu i wysyłasz klientowi. Płatność z linku jest zwykłą płatnością: ma identyfikator tr_…, pojawia się na liście GET /v1/platnosci i wysyła te same webhooki.