Skip to main content

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:

OptionTypeDescription
dpay.New(service, secretHash)string, stringService name (Payment Point) and Hash key from panel.dpay.pl (required)
dpay.WithTimeouttime.DurationTimeout of the default HTTP client (defaults to 30s)
dpay.WithHTTPClientdpay.HTTPDoerCustom transport - proxy, retries, instrumentation, tests
dpay.WithBaseURLsdpay.BaseURLsOverride API hosts (fields APIPayments, Panel, Gateway)

The client exposes services as fields:

ServiceScope
client.PaymentsPayment registration, transaction details
client.RefundsRefunds and refund availability checks
client.BanksPay-by-link bank list
client.BlikBLIK OneClick and Recurring aliases
client.CardsServer-to-server card payments
client.Payouts1:1 payout details

The client is safe to share across goroutines - create it once at application startup.

Redirects

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

FieldDescription
DescriptionTransaction description shown to the customer
CustomYour own order identifier (returned in the IPN)
PayerCustomer e-mail, first and last name
ChannelDirect payment through a specific bank channel
CreditCard, PayPal, Paysafecard, Installment, BlikEnable or disable methods on the gateway
NoBanksHide the bank list
BlikCode + UserAgent + UserIPBLIK Level 0 payment (6-digit code)
BlikAlias + UserAgent + UserIPPayment with a BLIK OneClick alias
RegisterBlikAliasRegister a OneClick alias during payment
CardRecurringRegister a card recurring mandate
CardRecurringAliasCharge a stored card (MIT)
Payout1:1 payout instruction
Efaktura + InvoicePayment for an e-invoice (KSeF)
PhoneNumber + CurrencyCodeMB WAY
BillingAddress, ShippingAddress, ProductsFree-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)
}
The response must be exactly OK

dpay.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.

info

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 codeReason for refusal
400Amount exceeds the available refund amount
401Payment channel does not support refunds
402Transaction has not been paid
406Insufficient balance to cover the refund
409A refund request has already been submitted
410Transaction has already been refunded
411Charge 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.

info

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
}
SentinelWhen
ErrTransportNetwork failure - payment status unknown, use Payments.Details
ErrAuthentication401 - invalid checksum or secret hash
ErrInvalidRequest400/422 - validation errors (FieldErrors)
ErrAccessDenied403 - no permission for the operation
ErrNotFound404 - resource does not exist
ErrRateLimit429 - request limit (*RateLimitError carries RetryAfter)
ErrPaymentRejectedRegistration rejected (e.g. invalid BLIK code; TransactionID)
ErrCardPaymentCard payment rejected (ErrorCode)
ErrSignatureInvalid IPN signature
ErrServer5xx or malformed API response
ErrInvalidArgumentInvalid argument or request field
ErrAPIMatches 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.


More information