Skip to content

fintech-sdk/adyen-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

adyen-go

A production-grade, zero-external-dependency Go client for the Adyen payment platform, built with Domain-Driven Design (DDD) principles and the latest Go 1.22 idioms.


Features

Capability Details
DDD Architecture Separate packages per bounded context: checkout, management, platforms, classic, terminal, webhooks
All major Adyen APIs Checkout v72, Management v3, Balance Platform v2, Classic PAL, Terminal API, Webhook notifications
Resilience Exponential-backoff retry · Token-bucket rate limiter · Three-state circuit breaker
Observability Telemetry event bus (start/stop hooks per call, timing, HTTP status)
PCI safety Automatic redaction of sensitive card fields from logs/telemetry
Idempotency Auto-generated UUIDv4 idempotency keys on every POST
HMAC validation Constant-time HMAC-SHA256 webhook signature verification
Type safety Generics-based HTTP helpers, typed error hierarchy, strongly typed event codes
Zero external deps Entire package uses only the Go standard library

Installation

go get github.com/iamkanishka/adyen-go

Requires Go 1.22+.


Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    adyen "github.com/iamkanishka/adyen-go"
    "github.com/iamkanishka/adyen-go/config"
    "github.com/iamkanishka/adyen-go/domain"
    "github.com/iamkanishka/adyen-go/domain/checkout"
)

func main() {
    // 1. Build configuration
    cfg, err := config.New(
        "AQEy...your-api-key...",
        config.WithEnvironment(config.EnvironmentTest),
        config.WithMerchantAccount("YourMerchantECOM"),
        config.WithWebhookHMACKey("your-hmac-key-hex"),
        config.WithMaxRetries(3),
    )
    if err != nil {
        log.Fatal(err)
    }

    // 2. Create the unified client
    client, err := adyen.New(cfg)
    if err != nil {
        log.Fatal(err)
    }

    // 3. Call the Checkout API
    ctx := context.Background()
    session, err := client.Checkout.CreateSession(ctx, checkout.CreateSessionRequest{
        Amount:          domain.Amount{Currency: "EUR", Value: 1000}, // €10.00
        MerchantAccount: cfg.MerchantAccount,
        Reference:       "order-12345",
        ReturnURL:       "https://yourshop.com/return",
        CountryCode:     "NL",
        ShopperLocale:   "nl-NL",
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Session ID:  ", session.ID)
    fmt.Println("Session data:", session.SessionData)
}

Architecture (DDD)

adyen-go/
├── adyen.go                    ← Top-level facade (New, MustNew)
├── application/
│   └── client.go               ← AdyenClient: wires all domain services
├── config/
│   └── config.go               ← AdyenConfig value object + URL resolution
├── domain/
│   ├── types.go                ← Shared value objects (Amount, Address, LineItem, Split…)
│   ├── checkout/               ← Checkout API v72
│   │   └── service.go
│   ├── management/             ← Management API v3
│   │   └── service.go
│   ├── platforms/              ← Balance Platform API v2
│   │   └── service.go
│   ├── classic/                ← Classic PAL API
│   │   └── service.go
│   ├── terminal/               ← Terminal API (LTC + Cloud + SoftPOS)
│   │   └── service.go
│   └── webhooks/               ← Webhook parsing, HMAC, dispatcher
│       └── service.go
├── infrastructure/
│   └── http/
│       └── client.go           ← Transport: retry, circuit breaker, rate limiter, telemetry
├── pkg/
│   └── errors/
│       └── errors.go           ← Typed AdyenError hierarchy
└── adyen_test.go               ← 71 tests covering all layers

API Services

Checkout (client.Checkout)

// Drop-in / Components session
session, err := client.Checkout.CreateSession(ctx, checkout.CreateSessionRequest{...})

// Direct payment (server-side)
resp, err := client.Checkout.CreatePayment(ctx, checkout.CreatePaymentRequest{
    Amount:          domain.Amount{Currency: "EUR", Value: 2000},
    MerchantAccount: "YourMerchant",
    Reference:       "order-99",
    ReturnURL:       "https://shop.com/return",
    PaymentMethod:   domain.CardPaymentMethod{
        Type:                  "scheme",
        EncryptedCardNumber:   "enc:4111...",
        EncryptedSecurityCode: "enc:737...",
        EncryptedExpiryMonth:  "enc:03...",
        EncryptedExpiryYear:   "enc:2030...",
    },
})

// 3DS2 detail submission
resp, err := client.Checkout.SubmitDetails(ctx, checkout.SubmitDetailsRequest{
    Details:     map[string]any{"redirectResult": "eyJ..."},
    PaymentData: "eyJ...paymentdata",
})

// Payment modifications
resp, err := client.Checkout.CapturePayment(ctx, pspRef, checkout.ModificationRequest{...})
resp, err := client.Checkout.RefundPayment(ctx, pspRef, checkout.ModificationRequest{...})
resp, err := client.Checkout.CancelPayment(ctx, pspRef, checkout.ModificationRequest{...})
resp, err := client.Checkout.ReversePayment(ctx, pspRef, checkout.ModificationRequest{...})

// Payment links
link, err := client.Checkout.CreatePaymentLink(ctx, checkout.CreatePaymentLinkRequest{...})
link, err := client.Checkout.GetPaymentLink(ctx, linkID)
link, err := client.Checkout.UpdatePaymentLink(ctx, linkID, checkout.UpdatePaymentLinkRequest{Status: "expired"})

// Stored tokens (recurring)
list, err := client.Checkout.ListStoredTokens(ctx, merchantAccount, shopperReference)
err  =  client.Checkout.DeleteToken(ctx, recurringID)

// Gift card / partial payments
balance, err := client.Checkout.GetBalance(ctx, checkout.GetBalanceRequest{...})
order,   err := client.Checkout.CreateOrder(ctx, checkout.CreateOrderRequest{...})
cancel,  err := client.Checkout.CancelOrder(ctx, checkout.CancelOrderRequest{...})

// Donations
campaigns, err := client.Checkout.ListDonationCampaigns(ctx, checkout.DonationCampaignRequest{...})
donation,  err := client.Checkout.CreateDonation(ctx, checkout.CreateDonationRequest{...})

// Utility
keys,    err := client.Checkout.CreateOriginKeys(ctx, checkout.OriginKeysRequest{...})
session, err := client.Checkout.GetApplePaySession(ctx, checkout.ApplePaySessionRequest{...})

Management (client.Management)

// Merchants
merchants, err := client.Management.ListMerchants(ctx, management.ListParams{PageSize: 25})
merchant,  err := client.Management.GetMerchant(ctx, merchantID)
created,   err := client.Management.CreateMerchant(ctx, management.CreateMerchantRequest{...})
updated,   err := client.Management.UpdateMerchant(ctx, merchantID, management.UpdateMerchantRequest{...})

// Stores
stores, err := client.Management.ListStores(ctx, merchantID, management.ListParams{})
store,  err := client.Management.CreateStore(ctx, merchantID, management.CreateStoreRequest{...})

// API Credentials
cred,    err := client.Management.CreateAPICredential(ctx, merchantID, management.CreateAPICredentialRequest{...})
apiKey,  err := client.Management.GenerateAPIKey(ctx, merchantID, credentialID)
origins, err := client.Management.ListAllowedOrigins(ctx, merchantID, credentialID)
origin,  err := client.Management.CreateAllowedOrigin(ctx, merchantID, credentialID, management.CreateOriginRequest{Domain: "https://shop.com"})

// Webhooks
webhook, err := client.Management.CreateWebhook(ctx, merchantID, management.CreateWebhookRequest{
    Type:                "standard",
    URL:                 "https://shop.com/webhooks",
    Active:              true,
    CommunicationFormat: "json",
})
hmac,    err := client.Management.GenerateWebhookHMACKey(ctx, merchantID, webhookID)
result,  err := client.Management.TestWebhook(ctx, merchantID, webhookID, management.TestWebhookRequest{...})

// Terminal orders
order, err := client.Management.CreateTerminalOrder(ctx, merchantID, management.CreateTerminalOrderRequest{...})

Balance Platform (client.Platforms)

// Account holders
holder,  err := client.Platforms.CreateAccountHolder(ctx, platforms.CreateAccountHolderRequest{LegalEntityID: "LE-001"})
holder,  err := client.Platforms.GetAccountHolder(ctx, holderID)
accounts,err := client.Platforms.GetAllBalanceAccountsForHolder(ctx, holderID, platforms.ListParams{})

// Balance accounts
account, err := client.Platforms.CreateBalanceAccount(ctx, platforms.CreateBalanceAccountRequest{AccountHolderID: holderID})
account, err := client.Platforms.GetBalanceAccount(ctx, accountID)

// Payment instruments (cards)
card,    err := client.Platforms.CreatePaymentInstrument(ctx, platforms.CreatePaymentInstrumentRequest{
    Type:             "card",
    BalanceAccountID: accountID,
    IssuingCountry:   "NL",
})
err =    client.Platforms.UpdatePaymentInstrument(ctx, instrumentID, platforms.UpdatePaymentInstrumentRequest{Status: "suspended"})

// Transfers
transfer, err := client.Platforms.CreateTransfer(ctx, platforms.CreateTransferRequest{
    Amount:       domain.Amount{Currency: "EUR", Value: 10000},
    Category:     "internal",
    Counterparty: platforms.Counterparty{BalanceAccountID: "BA-002"},
})

// Transactions
txns, err := client.Platforms.ListTransactions(ctx, platforms.TransactionListParams{
    BalanceAccountID: accountID,
    CreatedSince:     "2026-01-01T00:00:00Z",
    Limit:            50,
})

// Transaction rules (spending controls)
rule, err := client.Platforms.CreateTransactionRule(ctx, platforms.TransactionRule{
    Type: "velocity",
    Interval: &platforms.RuleInterval{Type: "daily"},
    RuleRestrictions: &platforms.RuleRestrictions{
        Amount: &platforms.RuleAmount{Value: 100000, Currency: "EUR"},
    },
})

Classic PAL (client.Classic)

// Authorise
resp, err := client.Classic.Authorise(ctx, classic.AuthoriseRequest{
    Amount:          domain.Amount{Currency: "GBP", Value: 3000},
    MerchantAccount: "YourMerchant",
    Reference:       "ref-1",
    Card: &classic.Card{
        Number:      "4111111111111111",
        ExpiryMonth: "03",
        ExpiryYear:  "2030",
        CVC:         "737",
    },
})

// 3DS
resp, err := client.Classic.Authorise3DS(ctx, classic.Authorise3DSRequest{MD: "...", PaResponse: "..."})
resp, err := client.Classic.Authorise3DS2(ctx, classic.Authorise3DS2Request{...})

// Modifications
resp, err := client.Classic.Capture(ctx, classic.ModificationRequest{OriginalReference: pspRef})
resp, err := client.Classic.Refund(ctx, classic.ModificationRequest{OriginalReference: pspRef})
resp, err := client.Classic.Cancel(ctx, classic.ModificationRequest{OriginalReference: pspRef})
resp, err := client.Classic.CancelOrRefund(ctx, classic.ModificationRequest{OriginalReference: pspRef})
resp, err := client.Classic.AdjustAuthorisation(ctx, classic.ModificationRequest{...})

// Recurring
details, err := client.Classic.ListRecurringDetails(ctx, classic.ListRecurringDetailsRequest{
    MerchantAccount:  "YourMerchant",
    ShopperReference: "shopper-42",
})
resp, err := client.Classic.DisableRecurring(ctx, classic.DisableRecurringRequest{
    MerchantAccount:          "YourMerchant",
    ShopperReference:         "shopper-42",
    RecurringDetailReference: "RECURRING-001",
})

// Payout
resp, err := client.Classic.Payout(ctx, classic.AuthoriseRequest{...})
resp, err := client.Classic.ConfirmThirdParty(ctx, classic.ConfirmThirdPartyRequest{...})

// BIN lookup
info, err := client.Classic.GetBINLookup(ctx, classic.BINLookupRequest{CardNumber: "411111"})

Terminal API (client.Terminal)

// Synchronous (Local Terminal Communication)
resp, err := client.Terminal.SyncPayment(ctx, terminal.TerminalAPIRequest{
    SaleToPOIRequest: terminal.SaleToPOIRequest{
        MessageHeader: terminal.MessageHeader{
            MessageCategory: "Payment",
            MessageClass:    "Service",
            MessageType:     "Request",
            POIID:           "V400m-123456789",
            SaleID:          "POS-001",
            ServiceID:       "svc-001",
            ProtocolVersion: "3.0",
        },
        PaymentRequest: &terminal.PaymentRequest{
            SaleData: terminal.SaleData{
                SaleTransactionID: terminal.SaleTransactionID{
                    TransactionID: "txn-001",
                    TimeStamp:     "2026-01-01T12:00:00Z",
                },
            },
            PaymentTransaction: terminal.PaymentTransaction{
                AmountsReq: terminal.AmountRequest{
                    Currency:        "EUR",
                    RequestedAmount: 10.00,
                },
            },
        },
    },
})
if resp.SaleToPOIResponse.PaymentResponse.Response.IsSuccess() {
    fmt.Println("Terminal payment approved!")
}

// Cloud (async via Adyen)
resp, err := client.Terminal.CloudPayment(ctx, terminalID, req)

Webhooks (client.Webhooks)

// Parse & validate in one step (requires WebhookHMACKey in config)
notifications, err := client.Webhooks.ParseAndValidate(r.Body)
if err != nil {
    http.Error(w, "invalid webhook", 400)
    return
}

// Dispatcher pattern
dispatcher := webhooks.NewDispatcher()

dispatcher.On([]webhooks.EventCode{webhooks.EventAuthorisation}, func(ctx context.Context, n webhooks.ParsedNotification) error {
    auth, _ := n.AsAuthorisation()
    fmt.Printf("Authorised PSP: %s  amount: %s %d\n",
        auth.PSPReference, auth.Amount.Currency, auth.Amount.Value)
    return nil
})

dispatcher.On([]webhooks.EventCode{webhooks.EventChargeback, webhooks.EventSecondChargeback},
    func(ctx context.Context, n webhooks.ParsedNotification) error {
        cb, _ := n.AsChargeback()
        fmt.Printf("Chargeback received: %s reason: %s\n", cb.PSPReference, cb.Reason)
        return nil
    })

dispatcher.OnAny(func(ctx context.Context, n webhooks.ParsedNotification) error {
    fmt.Printf("Unhandled event: %s\n", n.EventCode)
    return nil
})

errs := dispatcher.Dispatch(r.Context(), notifications)
w.WriteHeader(200)
w.Write([]byte("[accepted]"))

Error Handling

All API errors are returned as *errors.AdyenError:

import adyenerrors "github.com/iamkanishka/adyen-go/pkg/errors"

resp, err := client.Checkout.CreatePayment(ctx, req)
if err != nil {
    var ae *adyenerrors.AdyenError
    if errors.As(err, &ae) {
        switch ae.Type {
        case adyenerrors.ErrorTypeAuth:
            // Check your API key
        case adyenerrors.ErrorTypeValidation:
            // Fix request fields; ae.ErrorCode has the Adyen error code
        case adyenerrors.ErrorTypeRateLimited:
            // Back off; ae.Retryable == true
        case adyenerrors.ErrorTypeCircuitOpen:
            // Circuit breaker is open; the downstream is unhealthy
        }
        fmt.Println("HTTP status:", ae.HTTPStatus)
        fmt.Println("Error code: ", ae.ErrorCode)
        fmt.Println("PSP ref:    ", ae.PSPReference)
        fmt.Println("Retryable:  ", ae.Retryable)
    }
}

Error types

ErrorType HTTP status Retryable Description
ErrorTypeAuth 401, 403 No Invalid/expired API key or insufficient permissions
ErrorTypeValidation 422 No Request field validation failure
ErrorTypeNotFound 404 No Resource not found
ErrorTypeRateLimited 429 Yes Rate limit exceeded
ErrorTypeServer 5xx Yes Adyen server error
ErrorTypeNetwork Yes Network / TCP / TLS failure
ErrorTypeTimeout Yes Read or connect timeout
ErrorTypeWebhookValidation No HMAC mismatch or missing signature
ErrorTypeCircuitOpen No Circuit breaker has tripped
ErrorTypeConfig No Invalid/missing configuration

Telemetry

Subscribe to observe every API call's outcome:

unsubscribe := client.Telemetry.Subscribe(func(ev httpclient.TelemetryEvent) {
    if ev.Type == httpclient.TelemetryRequestStop {
        log.Printf("[adyen] %s %s → %d (%dms) ok=%v",
            ev.Method, ev.URL, ev.HTTPStatus, ev.DurationMs, ev.OK)
    }
})
defer unsubscribe()

Integrate with Prometheus, Datadog, OpenTelemetry, or any metrics backend.


Configuration Reference

cfg, err := config.New(
    "AQEy...apikey",                              // required
    config.WithEnvironment(config.EnvironmentLive), // default: EnvironmentTest
    config.WithMerchantAccount("MyMerchant"),
    config.WithWebhookHMACKey("hex-encoded-hmac-key"),
    config.WithTimeout(30*time.Second),            // HTTP read timeout (default 30s)
    config.WithConnectTimeout(10*time.Second),     // TCP connect timeout (default 10s)
    config.WithMaxRetries(3),                      // retries on 5xx/429 (default 3)
    config.WithRetryDelay(500*time.Millisecond),   // initial backoff (doubles per retry)
    config.WithCheckoutAPIVersion("72"),           // override individual API versions
)

Circuit Breaker

The built-in circuit breaker protects your application from cascading failures:

  • Closed → normal operation
  • Open → after 5 consecutive failures; all calls return ErrorTypeCircuitOpen immediately
  • Half-open → after 30 seconds; one probe call is allowed through
fmt.Println(client.CircuitBreakerState()) // "closed", "open", or "half-open"

PCI Compliance

The following fields are automatically redacted in telemetry and log-safe structures:

cardNumber, cvv, cvc, number, holderName, expiryMonth, expiryYear, encryptedCardNumber, encryptedSecurityCode, encryptedExpiryMonth, encryptedExpiryYear

Use httpclient.Redact(payload) to sanitize any map before logging.


Testing

go test ./... -v -count=1

The test suite covers:

  • Config validation (empty key, invalid environment, all URL builders)
  • Error types (classification, retryability, error string)
  • HTTP client (GET/POST, 4xx/5xx, retries, circuit breaker, idempotency, telemetry, PCI redaction, context cancellation)
  • Checkout (sessions, payments, 3DS, modifications, links, balance, orders)
  • Management (merchants, stores, credentials, webhooks)
  • Balance Platform (account holders, transfers, payment instruments)
  • Classic PAL (authorise, modifications, recurring)
  • Terminal API (sync payment, response parsing)
  • Webhooks (HMAC validation, HMAC tampering, dispatcher, typed event accessors)
  • Top-level facade (New, MustNew, nil config, circuit breaker state)

Supported Adyen APIs

API Version Package
Checkout API v72 domain/checkout
Management API v3 domain/management
Balance Platform API v2 domain/platforms
Transfers API v4 domain/platforms
Classic Payment API (PAL) v68 domain/classic
Classic Recurring API v68 domain/classic
Classic Payout API v68 domain/classic
BIN Lookup API v54 domain/classic
Terminal API (Sync/Cloud) v1 domain/terminal
SoftPOS API v3 domain/terminal
Webhook Notifications domain/webhooks
Legal Entity API v4 config URL only
Capital API v1 config URL only

License

MIT — see LICENSE.

About

A production-grade, zero-external-dependency Go client for the [Adyen](https://www.adyen.com) payment platform, built with Domain-Driven Design (DDD) principles and the latest Go 1.22 idioms

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages