Production-grade, dependency-free Go SDK for the Paysafe API.
Full coverage of the Paysafe developer platform: Payment Handles, Payments, Settlements, Refunds, Payouts (standalone & original credits), Verifications, Customer Vault, the Payment Scheduler (Plans & Subscriptions), merchant onboarding Applications, Value Added Services (FX Rates, Customer Identity/KYC, Bank Account Validation, Interac Verification Service), and webhook signature verification.
go get github.com/iamkanishka/paysafe-goRequires Go 1.22+. Zero third-party dependencies — standard library only.
import "github.com/iamkanishka/paysafe-go/pkg/paysafe"
client, err := paysafe.NewClient(
paysafe.WithUsername("1001062690"),
paysafe.WithPassword("B-qa2-0-..."),
paysafe.WithEnvironment(paysafe.Test),
paysafe.WithAccountID("1009688230"),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
handle, err := client.PaymentHandles.Create(ctx, paysafe.CreatePaymentHandleRequest{
MerchantRefNum: "order-1",
Amount: 5000, // $50.00 in minor units
CurrencyCode: "USD",
PaymentType: "CARD",
TransactionType: paysafe.TxnPayment,
Card: &paysafe.Card{
CardNum: "4111111111111111",
CardExpiry: &paysafe.CardExpiry{Month: 12, Year: 2030},
CVV: "123",
HolderName: "Jane Doe",
},
BillingDetails: &paysafe.BillingDetails{
Street: "123 Main St", City: "New York", State: "NY",
Country: "US", Zip: "10001",
},
})
if err != nil {
log.Fatal(err)
}
if action, redirectURL := client.HandleAction(handle); action == paysafe.ActionRedirect {
// 3DS or an APM requires a customer redirect before continuing.
fmt.Println("redirect customer to:", redirectURL)
return
}
payment, err := client.Payments.Create(ctx, paysafe.CreatePaymentRequest{
MerchantRefNum: handle.MerchantRefNum,
Amount: handle.Amount,
CurrencyCode: handle.CurrencyCode,
PaymentHandleToken: handle.PaymentHandleToken,
})Or configure from environment variables (PAYSAFE_USERNAME, PAYSAFE_PASSWORD, PAYSAFE_ENVIRONMENT, PAYSAFE_ACCOUNT_ID):
client, err := paysafe.NewClientFromEnv()See examples/ for runnable end-to-end programs: a basic card payment, a webhook receiver, and a full subscription/recurring-billing flow.
| Option | Default | Description |
|---|---|---|
WithUsername / WithPassword |
— (required) | API credentials from the Paysafe Business Portal |
WithEnvironment |
Test |
paysafe.Test or paysafe.Production |
WithAccountID |
— | Default account ID used when a call doesn't specify one |
WithBaseURLOverride |
— | Override the resolved base URL (mock servers, proxies) |
WithTimeout |
30s | Per-request HTTP timeout |
WithMaxRetries |
3 | Max retries on retryable failures (429/5xx, timeouts, specific API error codes) |
WithRetryBaseDelay |
500ms | Base delay for exponential backoff + jitter |
WithRateLimit |
100 req/s | Local token-bucket rate limit, keyed per credential |
WithHTTPClient |
— | Supply a custom *http.Client (custom transport, mTLS, test interception) |
WithTelemetryPrefix |
"paysafe" |
Prefix used in telemetry events |
Every operation returns (T, error). On failure, error can always be type-asserted to *paysafe.Error for structured detail:
payment, err := client.Payments.Create(ctx, req)
if err != nil {
if pErr, ok := err.(*paysafe.Error); ok {
switch pErr.Kind {
case paysafe.KindAPIError:
fmt.Println(pErr.Code, pErr.Message, pErr.FieldErrors)
case paysafe.KindRateLimited:
// back off and retry later
case paysafe.KindTimeout, paysafe.KindHTTPError:
// transport-level issue
}
}
}| Kind | Meaning |
|---|---|
KindAPIError |
Paysafe returned a structured API error (4xx/5xx with an error body) |
KindHTTPError |
Transport failure (DNS, connection refused, TLS) |
KindTimeout |
Request exceeded the configured timeout |
KindRateLimited |
The local rate limiter rejected the request before sending |
KindInvalidConfig |
Config validation failed at construction |
KindInvalidParams |
Request could not be encoded/built client-side |
KindWebhookSignatureMismatch |
HMAC-SHA256 webhook signature verification failed |
KindDecodeError |
Response (or webhook) body could not be parsed as JSON |
KindContextCanceled |
The caller's context.Context was canceled mid-request/retry |
http.HandleFunc("/webhooks/paysafe", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("Signature")
event, err := client.Webhooks.VerifyAndParse(body, signature, hmacKey)
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
switch event.Topic() {
case paysafe.TopicPaymentHandle:
log.Println(event.ResourceID, event.PaymentHandleStatus())
case paysafe.TopicSubscription:
// handle recurring billing events
}
w.WriteHeader(http.StatusOK)
})Signature verification uses constant-time comparison (crypto/hmac.Equal) to avoid timing attacks. Always verify the signature before trusting Get-derived status over a webhook-derived one when in doubt — re-fetch the resource via client.PaymentHandles.Get / client.Payments.Get etc. for authoritative state.
Attach a hook to observe every outbound request (start, stop, exception/panic):
type loggingHook struct{ paysafe.NoopTelemetryHook }
func (loggingHook) OnStop(ctx context.Context, meta paysafe.TelemetryStopMeta) {
log.Printf("%s %s -> %d in %s (ok=%v)", meta.Method, meta.Operation, meta.HTTPStatus, meta.Duration, meta.OK)
}
client, err := paysafe.NewClientWithTelemetry(loggingHook{}, paysafe.WithUsername(...), paysafe.WithPassword(...))The module follows Domain-Driven Design boundaries:
paysafe-go/
├── pkg/paysafe/ # Public API surface: Client facade + re-exported types/options
├── internal/
│ ├── domain/ # Pure entities, value objects, enums — no I/O
│ │ ├── shared/ Error, Link
│ │ ├── payment/ PaymentHandle, Payment, Settlement, Refund, Payout, Verification
│ │ ├── customer/ Customer Vault profile & saved instruments
│ │ ├── scheduler/ Plan, Subscription
│ │ ├── application/ Merchant onboarding Application
│ │ ├── webhook/ Event, Topic classification
│ │ └── vas/ FxRate, IdentityProfile, BankVerification
│ ├── application/ # Use-case orchestration / repositories — one package per bounded context
│ └── infrastructure/ # Cross-cutting technical concerns
│ ├── config/ Config, functional options, URL builders
│ ├── httpclient/ Generic typed HTTP transport (retry, auth, JSON)
│ ├── ratelimiter/ Per-credential token bucket
│ └── telemetry/ Start/stop/exception event hooks
└── examples/ # Runnable example programs
internal/ is intentionally inaccessible outside this module — the public API is entirely defined by pkg/paysafe, which re-exports domain types as Go type aliases (zero-cost, no conversion needed) so callers never import internal packages directly.
go test ./... # unit + httptest-based integration tests
go test -race ./... # with the race detector
go vet ./...
gofmt -l . # should print nothingThe test suite covers: config validation, structured error classification, HMAC webhook verification (success/failure/topic routing), token-bucket rate limiting (including refill-over-time), and end-to-end HTTP behavior via httptest — payment handle creation (sync + redirect), structured API error propagation, exponential-backoff retry on transient failures, and rate-limit rejection.
- Refunds are addressed by settlement ID, not payment ID. If
SettleWithAuthwastrue, the settlement ID equals the payment ID. Otherwise use the ID returned bySettlements.Create. - Always re-verify via
Getafter a webhook, especially for asynchronous payment methods — webhook delivery is at-least-once but not guaranteed-ordered. - Customer Identity (
CustomerIdentity.Rerun) should only be called whenDecision == DecisionError(a transient provider issue). ADecisionFailresult is final — rerunning returns the same result. - Plan amount increases should stay below 20% to comply with card network guidelines on recurring billing.
- Partial Authorization Service (PAS) (
AllowPartialAuth/GroupIDonCreatePaymentRequest) requires pre-enablement on the merchant account and is only available for Visa/Mastercard on UK/EEA-acquired merchants.
MIT — see LICENSE.