Skip to content

fintech-sdk/paysafe-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

paysafe-go

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.

Install

go get github.com/iamkanishka/paysafe-go

Requires Go 1.22+. Zero third-party dependencies — standard library only.

Quick start

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.

Configuration options

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

Error handling

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

Webhooks

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.

Telemetry

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(...))

Architecture

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.

Testing

go test ./...           # unit + httptest-based integration tests
go test -race ./...     # with the race detector
go vet ./...
gofmt -l .              # should print nothing

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

Important operational notes

  • Refunds are addressed by settlement ID, not payment ID. If SettleWithAuth was true, the settlement ID equals the payment ID. Otherwise use the ID returned by Settlements.Create.
  • Always re-verify via Get after 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 when Decision == DecisionError (a transient provider issue). A DecisionFail result 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/GroupID on CreatePaymentRequest) requires pre-enablement on the merchant account and is only available for Visa/Mastercard on UK/EEA-acquired merchants.

License

MIT — see LICENSE.

About

Production-grade, dependency-free Go SDK for the Paysafe API.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages