Go SDK
✅ Available - version 0.1.0
The official Go library for integrating with the dpay.pl API. It automates checksum generation, IPN signature verification and API calls: payment registration, transaction details, refunds, bank list, BLIK aliases, server-to-server card payments and 1:1 payouts. Amounts are represented by the Money type (minor units as int64) and API responses are mapped to typed structs. Every method takes a context.Context, so cancellation and deadlines work the same way as in the rest of your code.
Requirements
- Go 1.22 or newer (tested up to Go 1.26)
- Zero runtime dependencies - the SDK uses only the standard library
Installation
go get github.com/dpayglobal/dpay-go-sdk
import dpay "github.com/dpayglobal/dpay-go-sdk"
The source code is available on GitHub and the API documentation on pkg.go.dev.
Configuration
Create a client using the credentials from the dpay.pl Panel:
client, err := dpay.New("service_name", "your_secret_hash")
if err != nil {
return err
}
The service name and secret hash are required positionally, everything else is an option:
| Option | Type | Description |
|---|---|---|
dpay.New(service, secretHash) | string, string | Service name (Payment Point) and Hash key from panel.dpay.pl (required) |
dpay.WithTimeout | time.Duration | Timeout of the default HTTP client (defaults to 30s) |
dpay.WithHTTPClient | dpay.HTTPDoer | Custom transport - proxy, retries, instrumentation, tests |
dpay.WithBaseURLs | dpay.BaseURLs | Override API hosts (fields APIPayments, Panel, Gateway) |
The client exposes services as fields:
| Service | Scope |
|---|---|
client.Payments | Payment registration, transaction details |
client.Refunds | Refunds and refund availability checks |
client.Banks | Pay-by-link bank list |
client.Blik | BLIK OneClick and Recurring aliases |
client.Cards | Server-to-server card payments |
client.Payouts | 1:1 payout details |
The client is safe to share across goroutines - create it once at application startup.
The default HTTP client does not follow redirects - a 302 response is returned as the result rather than the page it points to. If you supply your own *http.Client through WithHTTPClient, set CheckRedirect the same way:
&http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
Optional fields are pointers
Optional request fields are pointers. nil omits the field, while a pointer to a value sends it explicitly - including when that value is false or zero. That distinction matters to the API, which is why the SDK never uses omitempty.
Description: dpay.String("Order #1234") // sends the field
AcceptTos: dpay.Bool(false) // sends false
Channel: nil // omits the field
Use dpay.String, dpay.Bool, dpay.Int and dpay.Int64 to build pointers.
Amounts - the Money type
Every amount in the SDK is a dpay.Money, internally holding minor units as int64. The SDK takes care of the correct amount format for each endpoint (decimal string, floating point number or minor units), so you never have to remember which endpoint expects which.
dpay.PLN(1050) // 10.50 PLN
money, err := dpay.NewMoney(500, dpay.CurrencyEUR)
money, err := dpay.ParseMoney("10.50", dpay.CurrencyPLN)
money.Minor() // 1050 (minor units)
money.String() // "10.50"
money.Currency() // dpay.CurrencyPLN
Money is a comparable value type - Equal returns true only for the same amount and currency. Amounts are never computed using floating point arithmetic.
Registering a payment
A request is a plain struct initialised with a literal. The checksum is added automatically:
payment, err := client.Payments.Register(ctx, &dpay.RegisterPaymentRequest{
Amount: dpay.PLN(1050),
TransactionType: dpay.TransactionTypeTransfers,
URLs: dpay.ReturnURLs{
Success: "https://yourshop.com/success",
Fail: "https://yourshop.com/failure",
IPN: "https://yourshop.com/ipn",
},
Description: dpay.String("Order #1234"),
Custom: dpay.String("order-1234"),
Payer: &dpay.Payer{
Email: dpay.String("customer@example.com"),
},
})
if err != nil {
return err
}
if url := payment.RedirectURL(); url != "" {
http.Redirect(w, r, url, http.StatusSeeOther)
return nil
}
if payment.IsPaid() {
// payment settled inline (e.g. BLIK Level 0)
}
Transaction types: TransactionTypeTransfers, TransactionTypeDCBGateway, TransactionTypeCardAuth, TransactionTypeMBWayDirect, TransactionTypeBizumDirect, TransactionTypeBlikRecurring, TransactionTypeCardRecurring.
The request is validated automatically before it is sent. You can also check it earlier - request.Validate() returns the same error, which is useful for form validation.
Notable RegisterPaymentRequest fields
| Field | Description |
|---|---|
Description | Transaction description shown to the customer |
Custom | Your own order identifier (returned in the IPN) |
Payer | Customer e-mail, first and last name |
Channel | Direct payment through a specific bank channel |
CreditCard, PayPal, Paysafecard, Installment, Blik | Enable or disable methods on the gateway |
NoBanks | Hide the bank list |
BlikCode + UserAgent + UserIP | BLIK Level 0 payment (6-digit code) |
BlikAlias + UserAgent + UserIP | Payment with a BLIK OneClick alias |
RegisterBlikAlias | Register a OneClick alias during payment |
CardRecurring | Register a card recurring mandate |
CardRecurringAlias | Charge a stored card (MIT) |
Payout | 1:1 payout instruction |
Efaktura + Invoice | Payment for an e-invoice (KSeF) |
PhoneNumber + CurrencyCode | MB WAY |
BillingAddress, ShippingAddress, Products | Free-form objects - use dpay.NewFields() to control field order |
Handling IPN
Notifications are verified with dpay.VerifyIPN - an invalid signature returns an error matching 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, "your_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 treats an IPN as delivered only when the response body is exactly OK (the dpay.IPNAck constant). The HTTP status is not checked. Make sure your framework does not append anything to the body - fmt.Fprint adds no newline, unlike fmt.Fprintln.
Always compare event.Amount() (a string, e.g. "10.00") with the order amount in your database, and process each transaction only once (an IPN may arrive multiple times). The IPN payload carries no currency, which is why the amount is a string rather than a Money object.
Transaction details
transaction, err := client.Payments.Details(ctx, "transaction-identifier")
transaction.Status() // "paid", "created", "processing", "expired", "captured"
transaction.IsPaid() // true for paid and captured
transaction.Value().String() // "29.99"
transaction.RefundedAmount() // Money
transaction.AvailableRefundAmount() // Money
transaction.IsFullyRefunded() // bool
transaction.Refunds() // []*dpay.TransactionRefund
Unknown fields in the response do not break parsing - the full payload is available through transaction.Raw().
Refunds
// full refund
refund, err := client.Refunds.Create(ctx, "transaction-identifier")
// partial refund with a reason
refund, err := client.Refunds.Create(ctx, "transaction-identifier",
dpay.WithRefundAmount(dpay.PLN(500)),
dpay.WithRefundReason("complaint"))
refund.IsAccepted()
Before refunding you can check availability - the method returns a business result (not an error), even when the refund is impossible:
availability, err := client.Refunds.CheckAvailability(ctx, "transaction-identifier",
dpay.WithRefundAmount(dpay.PLN(500)))
if err == nil && !availability.IsAvailable() {
availability.Message() // e.g. "Transaction has not been paid"
availability.HTTPStatus() // stable reason code, see the table
}
| HTTP code | Reason for refusal |
|---|---|
400 | Amount exceeds the available refund amount |
401 | Payment channel does not support refunds |
402 | Transaction has not been paid |
406 | Insufficient balance to cover the refund |
409 | A refund request has already been submitted |
410 | Transaction has already been refunded |
411 | Charge transactions are not refundable |
Banks
banks, err := client.Banks.All(ctx) // all dpay.pl banks
banks, err := client.Banks.ForService(ctx) // banks available for your service
banks[0].ID()
banks[0].Name()
BLIK - OneClick and Recurring aliases
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()
Alias registration and alias payments happen through payment registration (the BlikCode + RegisterBlikAlias fields, payment through BlikAlias).
Server-to-server cards
Card data is encrypted with an RSA key fetched before every payment attempt:
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, // payer browser data
EncryptedCardData: dpay.String(encrypted),
CardHolderFirstName: dpay.String("John"),
CardHolderLastName: dpay.String("Smith"),
ChannelID: dpay.Int(31),
})
switch {
case result.IsSuccess():
// payment captured
case result.RequiresThreeDSForm():
html := result.ThreeDSFormHTML() // render in the payer's browser
case result.RequiresRedirect():
http.Redirect(w, r, result.RedirectURL(), http.StatusSeeOther)
case result.HasDCCOffer():
offer := result.DCCOffer() // show the DCC offer to the payer
// send the decision back with another PayOTP, field DCCDecision: dpay.DCCDecisionAccept
}
Also available: PreAuth (pre-authorisation), Capture and Cancel, GooglePay and ApplePay. A payment rejected with HTTP 200 returns an error matching dpay.ErrCardPayment, with the code in ErrorCode.
Capturing the full amount is done by passing nil instead of an amount:
client.Cards.Capture(ctx, transactionID, nil) // full amount
client.Cards.Capture(ctx, transactionID, &amount) // partial amount
The public key is rotated - fetch it before every payment attempt, do not cache it. RSA encryption uses the standard library crypto/rsa package, so S2S cards require no additional dependency.
S2S cards require PCI-DSS compliance on the merchant side. Flow details are described in the S2S cards documentation.
1:1 payouts
details, err := client.Payouts.Details(ctx, 12345)
details.IsProcessed()
details.Net().String()
details.Receiver()
Error handling
The SDK has no exception hierarchy - you classify an error with errors.Is and read its details with 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):
// it is unknown whether the request arrived - verify with Payments.Details
}
| Sentinel | When |
|---|---|
ErrTransport | Network failure - payment status unknown, use Payments.Details |
ErrAuthentication | 401 - invalid checksum or secret hash |
ErrInvalidRequest | 400/422 - validation errors (FieldErrors) |
ErrAccessDenied | 403 - no permission for the operation |
ErrNotFound | 404 - resource does not exist |
ErrRateLimit | 429 - request limit (*RateLimitError carries RetryAfter) |
ErrPaymentRejected | Registration rejected (e.g. invalid BLIK code; TransactionID) |
ErrCardPayment | Card payment rejected (ErrorCode) |
ErrSignature | Invalid IPN signature |
ErrServer | 5xx or malformed API response |
ErrInvalidArgument | Invalid argument or request field |
ErrAPI | Matches any error returned by the API |
*APIError carries HTTPStatus, ErrorCode, FieldErrors and RawBody - the raw response body, useful for diagnostics.
Testing your integration
The SDK does not impose its own mock - httptest from the standard library is enough:
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,
}))
If you prefer to inspect sent requests without starting a server, implement dpay.HTTPDoer - a single-method interface Do(*http.Request) (*http.Response, error) that *http.Client also satisfies.