Skip to content

Repository files navigation

Payment for Go

CI Coverage Go Reference Go 1.24 or newer MIT License

English | پارسی

Payment

payment is a provider-neutral payment package for Go. It provides a small common API for purchasing, verifying, and optionally refunding payments while keeping provider configuration and protocol details in dedicated packages.

Each client is bound to one gateway. There is no global registry or mutable default, so applications remain in control of provider selection, persistence, retries, reconciliation, and business rules.

Requirements

  • Go 1.24 or newer

Installation

go get github.com/codenaline/payment@latest

Import the root package and only the provider packages your application uses:

import (
	"github.com/codenaline/payment"
	"github.com/codenaline/payment/zarinpal"
)

Supported providers

Provider Purchase Verify Refund Currencies Sandbox
ZarinPal IRR
NextPay IRR, IRT
SEP IRR

Refunding is an optional capability. Calling Client.Refund with a gateway that does not implement payment.Refunder returns payment.ErrUnsupported.

Quick start

Create a gateway, bind it to a client, and initiate a payment:

package main

import (
	"context"
	"fmt"

	"github.com/codenaline/payment"
	"github.com/codenaline/payment/zarinpal"
)

func main() {
	gateway, err := zarinpal.New(zarinpal.Config{
		MerchantID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
		Sandbox:    true,
	})
	if err != nil {
		panic(err)
	}

	client := payment.NewClient(gateway)
	result, err := client.Purchase(context.Background(), payment.PurchaseRequest{
		OrderID: "order-1234",
		Amount: payment.Money{
			Amount:   100_000,
			Currency: payment.CurrencyIRR,
		},
		CallbackURL: "https://example.com/payments/callback",
		Description: "Order #1234",
	})
	if err != nil {
		panic(err)
	}

	// Store the transaction before redirecting the customer.
	fmt.Println(result.Transaction.ID)
	fmt.Println(result.RedirectURL)
}

Persist the transaction ID, order ID, amount, currency, and current status before redirecting the customer to PurchaseResponse.RedirectURL.

Money.Amount is an integer expressed in the selected currency unit. The package does not convert currencies or units; pass the unit expected by the configured provider.

Verify a payment

The browser callback is not proof of payment. After the provider redirects the customer, verify the transaction from a trusted server using the stored transaction ID and original amount:

transaction, err := client.Verify(ctx, payment.VerifyRequest{
	TransactionID: storedTransactionID,
	Amount: payment.Money{
		Amount:   100_000,
		Currency: payment.CurrencyIRR,
	},
})
if err != nil {
	return err
}

if transaction.Status == payment.StatusPaid {
	// Persist the paid state before fulfilling the order.
}

Make callback processing idempotent. Store the verified state before fulfillment and return the existing successful result when the same paid callback is received again.

ZarinPal response codes 100 (verified) and 101 (already verified) both return a paid transaction without an error.

SEP returns a new RefNum to the callback after payment. For SEP, validate the callback fields and pass that RefNum as VerifyRequest.TransactionID; do not pass the purchase token. Prevent a RefNum from being used for more than one order.

Provider configuration

ZarinPal

gateway, err := zarinpal.New(zarinpal.Config{
	MerchantID: "your-merchant-id",
	Sandbox:    true,       // Optional; false by default.
	HTTPClient: httpClient, // Optional.
})

ZarinPal accepts IRR. Purchases require a positive amount, an absolute callback URL, and a non-empty description. If HTTPClient is nil, the provider uses an HTTP client with a 30-second timeout.

NextPay

gateway, err := nextpay.New(nextpay.Config{
	APIKey:     "your-api-key",
	HTTPClient: httpClient, // Optional.
})

NextPay accepts IRR and IRT. Purchases require a positive amount, a non-empty OrderID, and an absolute callback URL. If HTTPClient is nil, the provider uses an HTTP client with a 30-second timeout.

NextPay forwards these optional PurchaseRequest.Metadata keys when present: customer_phone, payer_name, and allowed_card.

SEP

gateway, err := sep.New(sep.Config{
	TerminalID: 12345678,
	HTTPClient: httpClient, // Optional.
})

SEP accepts IRR. Purchases require a positive amount, a non-empty OrderID, and an absolute callback URL. If HTTPClient is nil, the provider uses an HTTP client with a 30-second timeout.

SEP requires the merchant server's public IP to be registered. After a successful callback, verify its RefNum within 30 minutes and compare it with the stored order and amount. SEP does not publish a general sandbox endpoint.

Multiple gateways

Create one client for each configured gateway and keep provider selection in the application:

zarinpalGateway, err := zarinpal.New(zarinpal.Config{
	MerchantID: "your-merchant-id",
})
if err != nil {
	return err
}

nextpayGateway, err := nextpay.New(nextpay.Config{
	APIKey: "your-api-key",
})
if err != nil {
	return err
}

zarinpalClient := payment.NewClient(zarinpalGateway)
nextpayClient := payment.NewClient(nextpayGateway)

// Select a client using application-owned business rules.
_ = zarinpalClient
_ = nextpayClient

payment.NewClient panics when passed a nil gateway. A client cannot switch gateways after construction.

Refunds

NextPay implements the optional payment.Refunder capability. ZarinPal and SEP currently do not.

refund, err := client.Refund(ctx, payment.RefundRequest{
	TransactionID: transactionID,
	Amount:        amount,
	Reason:        "customer request",
})
if errors.Is(err, payment.ErrUnsupported) {
	// Use a provider-specific or manual refund process.
}

Applications should persist the refund result and reconcile it according to their own policies.

Error handling

The root package exposes portable sentinel errors. Use errors.Is for provider-independent decisions:

switch {
case errors.Is(err, payment.ErrInvalidRequest):
	// Fix the request; retrying it unchanged will not help.
case errors.Is(err, payment.ErrNetwork):
	// Apply the application's retry and reconciliation policy.
case errors.Is(err, payment.ErrDeclined):
	// Ask the customer to use another payment method.
case errors.Is(err, payment.ErrTransactionNotFound):
	// Reconcile the stored transaction details.
case errors.Is(err, payment.ErrCanceled):
	// Record the canceled payment.
case errors.Is(err, payment.ErrProvider):
	// Handle an unclassified provider failure.
}

Provider packages expose typed errors for diagnostics. Use errors.As only when provider-specific details are needed:

var providerError *zarinpal.Error
if errors.As(err, &providerError) {
	fmt.Printf("ZarinPal %s failed with code %d: %s\n",
		providerError.Operation,
		providerError.Code,
		providerError.Message,
	)
}

Avoid showing raw provider errors to customers because they may contain operational details.

Custom gateways

Implement payment.Gateway to add a provider without global registration:

type CustomGateway struct{}

func (*CustomGateway) Purchase(
	ctx context.Context,
	request payment.PurchaseRequest,
) (payment.PurchaseResponse, error) {
	return payment.PurchaseResponse{}, nil
}

func (*CustomGateway) Verify(
	ctx context.Context,
	request payment.VerifyRequest,
) (payment.Transaction, error) {
	return payment.Transaction{}, nil
}

client := payment.NewClient(&CustomGateway{})

Implement payment.Refunder when the provider supports refunds. Custom providers should wrap the root sentinel errors and expose a provider-specific error type when callers need more detail.

Application responsibilities

This package intentionally does not manage:

  • Gateway selection or routing
  • Transaction, order, and refund persistence
  • Callback authentication and idempotency
  • Retry, timeout, and reconciliation policies
  • Logging, metrics, or tracing
  • Fulfillment and other business rules

Never commit credentials or include merchant IDs, API keys, customer information, or complete callback data in logs and public bug reports.

Contributing and support

Contributions are welcome. Read CONTRIBUTING.md before opening a pull request. Use GitHub Discussions for usage questions and follow SECURITY.md to report vulnerabilities privately.

License

Released under the MIT License.

About

A unified payment library for Go with first-class support for multiple payment providers. Write your payment flow once and switch or combine gateways without changing your application code.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages