Skip to main content

Go SDK

The official Go SDK — github.com/itpay-ai/itpay-go — provides a fully typed client for the ItPay Protocol. It wraps the REST API with idiomatic Go interfaces and structs, automatic request signing, and a callback handler for HMAC verification.


Installation

go get github.com/itpay-ai/itpay-go@latest

Then import in your code:

import "github.com/itpay-ai/itpay-go/itpay"

Requirements: Go 1.21+ (uses slices and maps packages).


Quick Setup

Configure the client with your API key and optional defaults:

import "github.com/itpay-ai/itpay-go/itpay"

client := itpay.NewClient(&itpay.Config{
APIKey: "sk_liv...here",
Channel: "alipay",
})

The API key can also be loaded from the ITPAY_API_KEY environment variable:

client := itpay.NewClient(&itpay.Config{
APIKey: os.Getenv("ITPAY_API_KEY"),
Channel: "alipay",
})

Client Configuration

FieldTypeDefaultDescription
APIKeystring(required)Your secret API key
ChannelstringDefault payment channel (e.g. alipay)
BaseURLstringhttps://api.itpay.ai/v1API base URL
Timeouttime.Duration30sRequest timeout
HTTPClient*http.Clienthttp.DefaultClientCustom HTTP client

Creating a PaymentIntent

The payment flow starts by creating a PaymentIntent, then generating a QR code so the user can scan and pay.

package main

import (
"context"
"crypto/rand"
"fmt"
"log"
"time"

"github.com/itpay-ai/itpay-go/itpay"
)

func main() {
ctx := context.Background()

client := itpay.NewClient(&itpay.Config{
APIKey: "sk_liv...here",
Channel: "alipay",
})

// 1. Generate a unique PaymentIntent ID
intentID := fmt.Sprintf("pi_%x", randomBytes(16))

// 2. Create the PaymentIntent
intent, err := client.PaymentIntents.Create(ctx, &itpay.CreatePaymentIntentParams{
ID: intentID,
ServiceID: "01J7XYKZ1A2B3C4D5E6F7G8H9I",
Type: itpay.PaymentTypeOneTime,
Amount: itpay.Money{
Currency: "CNY",
Value: 699, // 6.99 CNY in minor units
},
Description: "AI document summary (42 pages)",
Payer: itpay.Party{
AgentID: "agent_cli_a1b2c3d4",
HumanID: "user_abc_789",
},
Metadata: map[string]string{
"session_id": "sess_xyz_456",
},
})
if err != nil {
log.Fatalf("Failed to create intent: %v", err)
}

fmt.Printf("Intent created: %s (status: %s)\n", intent.ID, intent.Status)
// → Intent created: pi_01J7XZ1A2B3C4D5E6F7G8H9IK (status: pending)

// 3. Generate the QR code
qrCharge, err := client.QR.Generate(ctx, intent.ID)
if err != nil {
log.Fatalf("Failed to generate QR: %v", err)
}

fmt.Printf("QR scan URL: %s\n", qrCharge.ScanURL)
fmt.Printf("QR expires at: %s\n", intent.ExpiresAt)

// 4. Display the QR to the user
// In a web UI: <img src="{{qrCharge.ScanURL}}" alt="Pay with Alipay" />
// In a terminal: open the URL via exec.Command("open", qrCharge.ScanURL)

// 5. Poll for completion (useful for development)
result, err := client.PaymentIntents.WaitForCompletion(ctx, intent.ID,
itpay.WithTimeout(5*time.Minute),
itpay.WithPollInterval(2*time.Second),
)
if err != nil {
log.Fatalf("Polling error: %v", err)
}

if result.Status == itpay.StatusSucceeded {
fmt.Println("✓ Payment received!")
} else {
fmt.Printf("✗ Payment failed: %s\n", result.Status)
}
}

func randomBytes(n int) []byte {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
log.Fatalf("Failed to generate random bytes: %v", err)
}
return b
}

API Reference

client.PaymentIntents.Create(ctx, params)

FieldTypeRequiredDescription
IDstringYesUnique UUIDv7 identifier (idempotency key)
ServiceIDstringYesServiceManifest ID from registration
TypePaymentTypeYesPaymentTypeOneTime, PaymentTypeCumulative, or PaymentTypeSubscription
AmountMoneyYes{ Currency: string, Value: int64 }
DescriptionstringNoHuman-readable payment label
PayerPartyYes{ AgentID: string, HumanID?: string }
Metadatamap[string]stringNoArbitrary key-value data (max 4 KB)

client.QR.Generate(ctx, paymentIntentID string)

Generates a QR charge for the given PaymentIntent. The PaymentIntent must be in pending status. Returns a *QRCharge with a ScanURL for rendering.

client.PaymentIntents.WaitForCompletion(ctx, id, opts...)

Polls the PaymentIntent until it reaches a terminal state (statusSucceeded, statusExpired, statusCancelled) or the timeout is hit. Accepts optional WithTimeout and WithPollInterval functional options.


Handling Callbacks

When a payment completes, the ItPay platform sends a signed webhook POST to your ServiceManifest's configured endpoint URL. Your handler must verify the HMAC-SHA256 signature and check for idempotency.

Here's a complete example using the standard net/http package:

package main

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)

// Must match the webhook secret from your ItPay dashboard
var webhookSecret = os.Getenv("ITPAY_WEBHOOK_SECRET")
const webhookMaxAge = 5 * time.Minute

// Track processed events (use Redis in production)
var (
processedMu sync.Mutex
processedEvents = make(map[string]struct{})
)

// WebhookEvent represents the ItPay webhook payload.
type WebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}

// PaymentIntentData represents the data payload for payment events.
type PaymentIntentData struct {
ID string `json:"id"`
Amount Money `json:"amount"`
Channel string `json:"channel"`
Status string `json:"status"`
Metadata map[string]string `json:"metadata,omitempty"`
FailureReason string `json:"failure_reason,omitempty"`
}

type Money struct {
Currency string `json:"currency"`
Value int64 `json:"value"`
}

// --- Webhook signature verification ---

func verifyWebhookSignature(
rawBody []byte,
signatureHeader string,
secret string,
maxAge time.Duration,
) bool {
// Parse the signature header: t=<timestamp>,v1=<hex_sig>
parts := make(map[string]string)
for _, pair := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}

timestampStr := parts["t"]
expectedSig := parts["v1"]

if timestampStr == "" || expectedSig == "" {
return false
}

// Reject expired webhooks
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return false
}
if time.Now().Unix()-timestamp > int64(maxAge.Seconds()) {
return false
}

// Reconstruct the signed payload: "<timestamp>.<raw_body>"
signedPayload := fmt.Sprintf("%s.%s", timestampStr, string(rawBody))

// Compute the expected HMAC-SHA256
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
computedSig := hex.EncodeToString(mac.Sum(nil))

// Constant-time comparison
return hmac.Equal([]byte(computedSig), []byte(expectedSig))
}

// --- Webhook handler ---

func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

// 1. Read signature header
signatureHeader := r.Header.Get("X-ItPay-Signature")
if signatureHeader == "" {
http.Error(w, "Missing X-ItPay-Signature header", http.StatusUnauthorized)
return
}

// 2. Read raw body (required for HMAC verification)
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusInternalServerError)
return
}
defer r.Body.Close()

// 3. Verify the HMAC signature
if !verifyWebhookSignature(rawBody, signatureHeader, webhookSecret, webhookMaxAge) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}

// 4. Parse the event
var event WebhookEvent
if err := json.Unmarshal(rawBody, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}

// 5. Idempotency check
processedMu.Lock()
if _, exists := processedEvents[event.ID]; exists {
processedMu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"status":"already_processed"}`)
return
}
processedEvents[event.ID] = struct{}{}
processedMu.Unlock()

// 6. Handle the event type
switch event.Type {
case "payment_intent.succeeded":
var intent PaymentIntentData
if err := json.Unmarshal(event.Data, &intent); err != nil {
log.Printf("Error parsing succeeded event data: %v", err)
break
}
log.Printf("✓ Payment succeeded — Intent: %s", intent.ID)
log.Printf(" Amount: %d %s", intent.Amount.Value, intent.Amount.Currency)
log.Printf(" Channel: %s", intent.Channel)
log.Printf(" Metadata: %v", intent.Metadata)

// TODO: Deliver the service to the user
// - Look up session_id from intent.Metadata
// - Unlock content or grant access

case "payment_intent.failed":
var intent PaymentIntentData
if err := json.Unmarshal(event.Data, &intent); err != nil {
log.Printf("Error parsing failed event data: %v", err)
break
}
log.Printf("✗ Payment failed — %s: %s", intent.ID, intent.FailureReason)

case "payment_intent.expired":
var intent PaymentIntentData
if err := json.Unmarshal(event.Data, &intent); err != nil {
log.Printf("Error parsing expired event data: %v", err)
break
}
log.Printf("⏰ Payment expired — %s", intent.ID)

default:
log.Printf("ℹ Unhandled event type: %s", event.Type)
}

// 7. Acknowledge receipt
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"status":"received"}`)
}

func main() {
if webhookSecret == "" {
log.Fatal("ITPAY_WEBHOOK_SECRET environment variable is required")
}

http.HandleFunc("/itpay/v1/webhook", webhookHandler)

port := "8080"
log.Printf("Webhook handler listening on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}

Testing the Webhook Locally

# Set your webhook secret
export ITPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx

# Start your webhook handler
go run webhook_handler.go

# Expose it to the internet with ngrok
ngrok http 8080
# → Forwarding https://abc123.ngrok.io → http://localhost:8080

# Update your ServiceManifest's endpoint to the ngrok URL
# https://abc123.ngrok.io/itpay/v1/webhook

Using the SDK's Built-in Verifier

The SDK includes a convenience function for webhook verification:

import "github.com/itpay-ai/itpay-go/itpay/webhooks"

isValid := webhooks.VerifySignature(
rawBody,
r.Header.Get("X-ItPay-Signature"),
webhookSecret,
webhooks.WithMaxAge(5*time.Minute),
)

Handling Callbacks with Gin

For Gin applications, use c.GetRawData() for the raw body and c.Request.Header for headers:

package main

import (
"github.com/gin-gonic/gin"
"github.com/itpay-ai/itpay-go/itpay/webhooks"
)

func main() {
r := gin.Default()

r.POST("/itpay/v1/webhook", func(c *gin.Context) {
rawBody, _ := c.GetRawData()
signatureHeader := c.GetHeader("X-ItPay-Signature")

if !webhooks.VerifySignature(rawBody, signatureHeader, webhookSecret) {
c.JSON(401, gin.H{"error": "Invalid signature"})
return
}

// ... handle event ...
c.JSON(200, gin.H{"status": "received"})
})

r.Run(":8080")
}

Error Handling

The SDK returns typed errors for predictable error handling:

import (
"errors"
"github.com/itpay-ai/itpay-go/itpay"
)

intent, err := client.PaymentIntents.Create(ctx, params)
if err != nil {
var authErr *itpay.AuthenticationError
var valErr *itpay.ValidationError
var rateErr *itpay.RateLimitError
var apiErr *itpay.APIError

switch {
case errors.As(err, &authErr):
log.Fatal("Invalid API key — check your credentials")
case errors.As(err, &valErr):
log.Printf("Validation failed: %v", err)
case errors.As(err, &rateErr):
log.Printf("Rate limit exceeded — wait and retry")
case errors.As(err, &apiErr):
log.Printf("ItPay API error (HTTP %d): %v", apiErr.StatusCode, err)
default:
log.Printf("Network or unexpected error: %v", err)
}
}

Error Types

TypeHTTP StatusDescription
*AuthenticationError401Invalid or missing API key
*ValidationError422Request validation failed
*RateLimitError429Rate limit exceeded
*APIError5xxServer-side error (retryable)

Best Practices

  • Generate unique PaymentIntent IDs — use UUIDv7 or strong random hex strings for idempotency.
  • Store secrets as environment variables — never hardcode APIKey or WebhookSecret.
  • Always verify webhook signatures using hmac.Equal for constant-time comparison.
  • Use io.ReadAll(r.Body) before parsing — the raw body is needed for HMAC verification.
  • Prefer webhooks over polling in production. The convenience poller is for development only.
  • Re-register r.Body after reading in HTTP middleware if other handlers need it; the default http.Request consumes it once.

Full Example: Complete Server

package main

import (
"context"
"fmt"
"log"
"net/http"
"os"

"github.com/itpay-ai/itpay-go/itpay"
)

func main() {
ctx := context.Background()

client := itpay.NewClient(&itpay.Config{
APIKey: os.Getenv("ITPAY_API_KEY"),
Channel: "alipay",
})

// Create a service manifest
manifest, err := client.Manifests.Create(ctx, &itpay.CreateManifestParams{
Name: "Smart Summary",
Description: "AI-powered document summarization",
PaymentMethods: itpay.PaymentMethods{
OneTime: true,
},
Pricing: itpay.Pricing{
OneTime: []itpay.PriceTier{
{Amount: 99, Currency: "USD", Label: "per summary"},
},
},
AcceptedChannels: []string{"alipay", "wechat"},
QRMode: "dynamic",
SettlementCurrency: "USD",
Endpoint: "https://abc123.ngrok.io/itpay/v1/webhook",
})
if err != nil {
log.Fatalf("Failed to create manifest: %v", err)
}

fmt.Printf("ServiceManifest created: %s\n", manifest.ID)

// Start the webhook handler
http.HandleFunc("/itpay/v1/webhook", webhookHandler)
log.Println("Server ready — listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Next Steps