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
| Field | Type | Default | Description |
|---|---|---|---|
APIKey | string | (required) | Your secret API key |
Channel | string | — | Default payment channel (e.g. alipay) |
BaseURL | string | https://api.itpay.ai/v1 | API base URL |
Timeout | time.Duration | 30s | Request timeout |
HTTPClient | *http.Client | http.DefaultClient | Custom 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)
| Field | Type | Required | Description |
|---|---|---|---|
ID | string | Yes | Unique UUIDv7 identifier (idempotency key) |
ServiceID | string | Yes | ServiceManifest ID from registration |
Type | PaymentType | Yes | PaymentTypeOneTime, PaymentTypeCumulative, or PaymentTypeSubscription |
Amount | Money | Yes | { Currency: string, Value: int64 } |
Description | string | No | Human-readable payment label |
Payer | Party | Yes | { AgentID: string, HumanID?: string } |
Metadata | map[string]string | No | Arbitrary 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
| Type | HTTP Status | Description |
|---|---|---|
*AuthenticationError | 401 | Invalid or missing API key |
*ValidationError | 422 | Request validation failed |
*RateLimitError | 429 | Rate limit exceeded |
*APIError | 5xx | Server-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
APIKeyorWebhookSecret. - Always verify webhook signatures using
hmac.Equalfor 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.Bodyafter reading in HTTP middleware if other handlers need it; the defaulthttp.Requestconsumes 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
- Core Objects: PaymentIntent — Complete state machine reference
- Security Model — Webhook verification and request signing details
- Channel Matrix — Supported payment channels and regions
- Quick Start — End-to-end payment flow walkthrough