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,polaizadanie_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.
<?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.
// 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).
# 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.
# 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.
- WooCommerce Klucze API i adres webhooka w ustawieniach płatności WordPressa; koszt bramek we WooCommerce.
- Shoper Płatności w Shoperze i próg, od którego własna bramka wychodzi taniej.
- PrestaShop Moduł płatności, klucze, webhook i tryb piaskownicy przed uruchomieniem sprzedaży.
- Shopify Zewnętrzna bramka w Shopify i koszt wobec Shopify Payments.
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.