Go SDK
✅ Dostępne - wersja 0.1.0
Oficjalna biblioteka Go do integracji z API dpay.pl. Automatyzuje generowanie sum kontrolnych, weryfikację powiadomień IPN oraz wywołania API: rejestrację płatności, szczegóły transakcji, zwroty, listę banków, aliasy BLIK, płatności kartowe server-to-server i wypłaty 1:1. Kwoty reprezentuje typ Money (grosze jako int64), a odpowiedzi API mapowane są na typowane struktury. Każda metoda przyjmuje context.Context, więc anulowanie i limity czasu działają tak, jak w reszcie Twojego kodu.
Wymagania
- Go 1.22 lub nowszy (testowane do Go 1.26)
- Zero zależności runtime - SDK korzysta wyłącznie z biblioteki standardowej
Instalacja
go get github.com/dpayglobal/dpay-go-sdk
import dpay "github.com/dpayglobal/dpay-go-sdk"
Kod źródłowy dostępny na GitHub, dokumentacja API na pkg.go.dev.
Konfiguracja
Utwórz klienta, podając dane z Panelu dpay.pl:
client, err := dpay.New("nazwa_serwisu", "twoj_secret_hash")
if err != nil {
return err
}
Nazwa serwisu i secret hash są wymagane pozycyjnie, reszta to opcje:
| Opcja | Typ | Opis |
|---|---|---|
dpay.New(service, secretHash) | string, string | Nazwa serwisu (Punktu Płatności) i klucz Hash z panel.dpay.pl (wymagane) |
dpay.WithTimeout | time.Duration | Timeout domyślnego klienta HTTP (domyślnie 30s) |
dpay.WithHTTPClient | dpay.HTTPDoer | Własny transport - proxy, retry, instrumentacja, testy |
dpay.WithBaseURLs | dpay.BaseURLs | Nadpisanie hostów API (pola APIPayments, Panel, Gateway) |
Klient udostępnia serwisy jako pola:
| Serwis | Zakres |
|---|---|
client.Payments | Rejestracja płatności, szczegóły transakcji |
client.Refunds | Zwroty i sprawdzanie dostępności zwrotu |
client.Banks | Lista banków pay-by-link |
client.Blik | Aliasy BLIK OneClick i Recurring |
client.Cards | Płatności kartowe server-to-server |
client.Payouts | Szczegóły wypłat 1:1 |
Klient jest bezpieczny do współdzielenia między goroutine'ami - utwórz go raz przy starcie aplikacji.
Domyślny klient HTTP nie podąża za przekierowaniami - odpowiedź 302 wraca jako wynik, a nie jako strona, pod którą prowadzi. Jeśli podajesz własny *http.Client przez WithHTTPClient, ustaw CheckRedirect tak samo:
&http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
Pola opcjonalne - wskaźniki
Pola opcjonalne w żądaniach są wskaźnikami. nil pomija pole, a wskaźnik na wartość wysyła ją jawnie - również wtedy, gdy to false lub zero. To rozróżnienie ma znaczenie dla API, dlatego SDK nigdzie nie używa omitempty.
Description: dpay.String("Zamówienie #1234") // wysyła pole
AcceptTos: dpay.Bool(false) // wysyła false
Channel: nil // pomija pole
Do budowania wskaźników służą dpay.String, dpay.Bool, dpay.Int i dpay.Int64.
Kwoty - typ Money
Wszystkie kwoty w SDK to typ dpay.Money, wewnętrznie przechowujący grosze jako int64. SDK sam dba o właściwy format kwoty dla każdego endpointu (kwota dziesiętna, liczba zmiennoprzecinkowa albo grosze), więc nie musisz pamiętać, który endpoint oczekuje którego formatu.
dpay.PLN(1050) // 10.50 PLN
money, err := dpay.NewMoney(500, dpay.CurrencyEUR)
money, err := dpay.ParseMoney("10.50", dpay.CurrencyPLN)
money.Minor() // 1050 (grosze)
money.String() // "10.50"
money.Currency() // dpay.CurrencyPLN
Money jest typem wartościowym i porównywalnym - Equal zwraca true tylko dla tej samej kwoty i waluty. Kwoty nie są liczone na liczbach zmiennoprzecinkowych.
Rejestracja płatności
Żądanie to zwykła struktura inicjalizowana literałem. Suma kontrolna dokładana jest automatycznie:
payment, err := client.Payments.Register(ctx, &dpay.RegisterPaymentRequest{
Amount: dpay.PLN(1050),
TransactionType: dpay.TransactionTypeTransfers,
URLs: dpay.ReturnURLs{
Success: "https://twojsklep.pl/sukces",
Fail: "https://twojsklep.pl/blad",
IPN: "https://twojsklep.pl/ipn",
},
Description: dpay.String("Zamówienie #1234"),
Custom: dpay.String("order-1234"),
Payer: &dpay.Payer{
Email: dpay.String("klient@example.com"),
},
})
if err != nil {
return err
}
if url := payment.RedirectURL(); url != "" {
http.Redirect(w, r, url, http.StatusSeeOther)
return nil
}
if payment.IsPaid() {
// płatność rozliczona inline (np. BLIK Level 0)
}
Typy transakcji: TransactionTypeTransfers, TransactionTypeDCBGateway, TransactionTypeCardAuth, TransactionTypeMBWayDirect, TransactionTypeBizumDirect, TransactionTypeBlikRecurring, TransactionTypeCardRecurring.
Żądanie jest walidowane automatycznie przed wysłaniem. Możesz też sprawdzić je wcześniej - request.Validate() zwraca ten sam błąd, na przykład przy walidacji formularza.
Ważniejsze pola RegisterPaymentRequest
| Pole | Opis |
|---|---|
Description | Opis transakcji widoczny dla klienta |
Custom | Własne dane identyfikujące zamówienie (wracają w IPN) |
Payer | E-mail oraz imię i nazwisko klienta |
Channel | Płatność bezpośrednia wskazanym kanałem banku |
CreditCard, PayPal, Paysafecard, Installment, Blik | Włączenie lub wyłączenie metod na bramce |
NoBanks | Ukrycie listy banków |
BlikCode + UserAgent + UserIP | Płatność BLIK Level 0 (kod 6-cyfrowy) |
BlikAlias + UserAgent + UserIP | Płatność aliasem BLIK OneClick |
RegisterBlikAlias | Rejestracja aliasu OneClick przy płatności |
CardRecurring | Rejestracja mandatu card recurring |
CardRecurringAlias | Obciążenie zapisanej karty (MIT) |
Payout | Instrukcja wypłaty 1:1 |
Efaktura + Invoice | Płatność za eFakturę (KSeF) |
PhoneNumber + CurrencyCode | MB WAY |
BillingAddress, ShippingAddress, Products | Obiekty wolnej formy - użyj dpay.NewFields(), aby zachować kolejność pól |
Obsługa IPN
Powiadomienia weryfikujesz przez dpay.VerifyIPN - nieprawidłowy podpis zwraca błąd pasujący do dpay.ErrSignature:
func ipnHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
event, err := dpay.VerifyIPN(body, "twoj_secret_hash")
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
if event.IsTransfer() {
markOrderAsPaid(event.ID(), event.Amount())
}
if event.IsCapture() {
markOrderAsCaptured(event.ID(), event.Amount(), event.CapturePaymentID())
}
fmt.Fprint(w, dpay.IPNAck)
}
OKdpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała dpay.IPNAck). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body - fmt.Fprint nie dodaje znaku nowej linii, w przeciwieństwie do fmt.Fprintln.
Zawsze porównaj event.Amount() (string, np. "10.00") z kwotą zamówienia w Twojej bazie i przetwarzaj każdą transakcję tylko raz (IPN może przyjść wielokrotnie). Payload IPN nie niesie waluty, dlatego kwota jest stringiem, a nie obiektem Money.
Szczegóły transakcji
transaction, err := client.Payments.Details(ctx, "identyfikator-transakcji")
transaction.Status() // "paid", "created", "processing", "expired", "captured"
transaction.IsPaid() // true dla paid i captured
transaction.Value().String() // "29.99"
transaction.RefundedAmount() // Money
transaction.AvailableRefundAmount() // Money
transaction.IsFullyRefunded() // bool
transaction.Refunds() // []*dpay.TransactionRefund
Nieznane pola z odpowiedzi nie psują parsowania - cały payload dostępny jest przez transaction.Raw().
Zwroty
// zwrot pełny
refund, err := client.Refunds.Create(ctx, "identyfikator-transakcji")
// zwrot częściowy z powodem
refund, err := client.Refunds.Create(ctx, "identyfikator-transakcji",
dpay.WithRefundAmount(dpay.PLN(500)),
dpay.WithRefundReason("reklamacja"))
refund.IsAccepted()
Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (a nie błąd), także gdy zwrot jest niemożliwy:
availability, err := client.Refunds.CheckAvailability(ctx, "identyfikator-transakcji",
dpay.WithRefundAmount(dpay.PLN(500)))
if err == nil && !availability.IsAvailable() {
availability.Message() // np. "Transakcja nie została opłacona"
availability.HTTPStatus() // stabilny kod przyczyny, patrz tabela
}
| Kod HTTP | Przyczyna odmowy |
|---|---|
400 | Kwota przekracza dostępną kwotę zwrotu |
401 | Kanał płatności nie obsługuje zwrotów |
402 | Transakcja nie została opłacona |
406 | Niewystarczające saldo na pokrycie zwrotu |
409 | Wniosek o zwrot został już złożony |
410 | Transakcja została już zwrócona |
411 | Transakcje obciążeniowe nie podlegają zwrotom |
Banki
banks, err := client.Banks.All(ctx) // wszystkie banki dpay.pl
banks, err := client.Banks.ForService(ctx) // banki dostępne dla Twojego serwisu
banks[0].ID()
banks[0].Name()
BLIK - aliasy OneClick i Recurring
alias, err := client.Blik.Alias(ctx, "DPAY.UID.123456.abc12345", dpay.BlikAliasTypeUID)
alias.IsActive()
alias.Apps()
err = client.Blik.UnregisterAlias(ctx, "DPAY.UID.123456.abc12345", dpay.BlikAliasTypeUID)
status, err := client.Blik.RecurringStatus(ctx, "PAYID-...")
status.Registration()
Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (pola BlikCode + RegisterBlikAlias, płatność przez BlikAlias).
Karty server-to-server
Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:
publicKey, err := client.Cards.PublicKey(ctx)
encrypted, err := dpay.EncryptCard(
dpay.CardData{PAN: "4111111111111111", CVV: "123", Expiry: "12/30"},
transactionID,
publicKey,
)
result, err := client.Cards.PayOTP(ctx, transactionID, &dpay.CardPaymentRequest{
DeviceInfo: deviceInfo, // dane przeglądarki płatnika
EncryptedCardData: dpay.String(encrypted),
CardHolderFirstName: dpay.String("Jan"),
CardHolderLastName: dpay.String("Kowalski"),
ChannelID: dpay.Int(31),
})
switch {
case result.IsSuccess():
// płatność przechwycona
case result.RequiresThreeDSForm():
html := result.ThreeDSFormHTML() // wyrenderuj w przeglądarce płatnika
case result.RequiresRedirect():
http.Redirect(w, r, result.RedirectURL(), http.StatusSeeOther)
case result.HasDCCOffer():
offer := result.DCCOffer() // pokaż ofertę DCC płatnikowi
// decyzję odeślij ponownym PayOTP z polem DCCDecision: dpay.DCCDecisionAccept
}
Dostępne są także: PreAuth (pre-autoryzacja), Capture i Cancel (przechwycenie i anulowanie), GooglePay oraz ApplePay. Odrzucenie płatności przy HTTP 200 zwraca błąd pasujący do dpay.ErrCardPayment, z kodem błędu w ErrorCode.
Przechwycenie pełnej kwoty wykonuje się przekazując nil zamiast kwoty:
client.Cards.Capture(ctx, transactionID, nil) // pełna kwota
client.Cards.Capture(ctx, transactionID, &amount) // kwota częściowa
Klucz publiczny jest rotowany - pobieraj go przed każdą próbą płatności, nie buforuj. Szyfrowanie RSA korzysta z pakietu crypto/rsa z biblioteki standardowej, więc karty S2S nie wymagają żadnej dodatkowej zależności.
Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.
Wypłaty 1:1
details, err := client.Payouts.Details(ctx, 12345)
details.IsProcessed()
details.Net().String()
details.Receiver()
Obsługa błędów
SDK nie ma hierarchii wyjątków - rodzaj błędu rozpoznajesz przez errors.Is, a szczegóły odczytujesz przez errors.As:
payment, err := client.Payments.Register(ctx, request)
switch {
case errors.Is(err, dpay.ErrInvalidRequest):
var apiErr *dpay.APIError
errors.As(err, &apiErr)
log.Print(apiErr.FieldErrors) // map[string][]string
case errors.Is(err, dpay.ErrPaymentRejected):
var rejected *dpay.PaymentRejectedError
errors.As(err, &rejected)
log.Print(rejected.TransactionID, rejected.ErrorCode)
case errors.Is(err, dpay.ErrTransport):
// nie wiadomo, czy żądanie dotarło - zweryfikuj przez Payments.Details
}
| Sentinel | Kiedy |
|---|---|
ErrTransport | Błąd sieci - status płatności nieznany, użyj Payments.Details |
ErrAuthentication | 401 - błędny checksum lub secret hash |
ErrInvalidRequest | 400/422 - błędy walidacji (FieldErrors) |
ErrAccessDenied | 403 - brak uprawnień do operacji |
ErrNotFound | 404 - zasób nie istnieje |
ErrRateLimit | 429 - limit żądań (*RateLimitError niesie RetryAfter) |
ErrPaymentRejected | Rejestracja odrzucona (np. błędny kod BLIK; TransactionID) |
ErrCardPayment | Odrzucenie płatności kartowej (ErrorCode) |
ErrSignature | Nieprawidłowy podpis IPN |
ErrServer | 5xx lub niepoprawna odpowiedź API |
ErrInvalidArgument | Niepoprawny argument lub pole żądania |
ErrAPI | Pasuje do każdego błędu zwróconego przez API |
*APIError niesie HTTPStatus, ErrorCode, FieldErrors i RawBody - surową treść odpowiedzi, przydatną przy diagnostyce.
Testowanie integracji
SDK nie narzuca własnego mocka - wystarczy httptest z biblioteki standardowej:
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"transactionId":"tx-1","msg":"https://secure.dpay.pl/pay/1"}`))
}))
defer server.Close()
client, _ := dpay.New("test", "test", dpay.WithBaseURLs(dpay.BaseURLs{
APIPayments: server.URL,
Panel: server.URL,
}))
Jeśli wolisz sprawdzać wysłane żądania bez podnoszenia serwera, zaimplementuj dpay.HTTPDoer - to jednometodowy interfejs Do(*http.Request) (*http.Response, error), który spełnia także *http.Client.