Skip to main content

DRP Go SDK

The official Go SDK for integrating the Digital Receipt Protocol into Go applications, microservices, and high-performance backends.

Installation

go get github.com/digitalreceiptprotocol/drp-go
Requirements:
  • Go 1.19 or higher

Quick Start

Merchant Integration

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    
    "github.com/digitalreceiptprotocol/drp-go"
)

func main() {
    // Initialize client
    client := drp.NewClient(
        os.Getenv("DRP_API_KEY"),
        "issuer-bank.com",
        drp.Production, // or drp.Sandbox
    )
    
    // Load merchant private key
    if err := client.LoadPrivateKeyFromFile("./merchant-private-key.pem"); err != nil {
        log.Fatal(err)
    }
    
    ctx := context.Background()
    
    // Send a receipt
    if err := sendReceipt(ctx, client, "tok_card_abc123", "txn_auth_456"); err != nil {
        log.Printf("Error sending receipt: %v", err)
    }
}

func sendReceipt(ctx context.Context, client *drp.Client, cardToken, txnID string) error {
    // 1. Get customer's public key
    keyData, err := client.PublicKeys.Get(ctx, cardToken, &drp.PublicKeyOptions{
        TransactionID: txnID,
    })
    if err != nil {
        return fmt.Errorf("failed to get public key: %w", err)
    }
    
    // 2. Build receipt data
    receipt := buildReceiptData() // Your receipt JSON-LD
    
    // 3. Send encrypted receipt (SDK handles signing and encryption)
    result, err := client.Receipts.Send(ctx, &drp.SendReceiptParams{
        ReceiptID:           fmt.Sprintf("rcpt_%d", time.Now().Unix()),
        Receipt:             receipt,
        CustomerPublicKey:   keyData.PublicKey,
        CustomerPublicKeyID: keyData.KeyID,
        CardLastFour:        cardToken[len(cardToken)-4:],
        TransactionID:       txnID,
        MerchantID:          "mch_coffee_shop_001",
        Amount:              12.50,
        Currency:            "USD",
    })
    if err != nil {
        if drp.IsCustomerNotEnrolled(err) {
            log.Println("Customer not enrolled - skip DRP receipt")
            return nil
        }
        return fmt.Errorf("failed to send receipt: %w", err)
    }
    
    log.Printf("Receipt sent: %s", result.DeliveryConfirmation)
    return nil
}

func buildReceiptData() map[string]interface{} {
    return map[string]interface{}{
        "@context": "https://schema.org",
        "@type":    "Receipt",
        "totalPaymentDue": map[string]interface{}{
            "@type":         "PriceSpecification",
            "price":         12.50,
            "priceCurrency": "USD",
        },
        // ... rest of receipt
    }
}

HTTP Server Integration

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    
    "github.com/digitalreceiptprotocol/drp-go"
)

var drpClient *drp.Client

func init() {
    drpClient = drp.NewClient(
        os.Getenv("DRP_API_KEY"),
        "issuer-bank.com",
        drp.Production,
    )
    drpClient.LoadPrivateKeyFromFile("./merchant-private-key.pem")
}

type CheckoutRequest struct {
    CardToken     string                 `json:"card_token"`
    TransactionID string                 `json:"transaction_id"`
    Receipt       map[string]interface{} `json:"receipt"`
    Amount        float64                `json:"amount"`
}

func handleCheckout(w http.ResponseWriter, r *http.Request) {
    var req CheckoutRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    ctx := r.Context()
    
    // Get public key
    keyData, err := drpClient.PublicKeys.Get(ctx, req.CardToken, nil)
    if err != nil {
        log.Printf("DRP error: %v", err)
        json.NewEncoder(w).Encode(map[string]interface{}{
            "success": true,
            "drp":     false,
        })
        return
    }
    
    // Send receipt
    result, err := drpClient.Receipts.Send(ctx, &drp.SendReceiptParams{
        ReceiptID:           generateReceiptID(),
        Receipt:             req.Receipt,
        CustomerPublicKey:   keyData.PublicKey,
        CustomerPublicKeyID: keyData.KeyID,
        CardLastFour:        req.CardToken[len(req.CardToken)-4:],
        TransactionID:       req.TransactionID,
        MerchantID:          os.Getenv("MERCHANT_ID"),
        Amount:              req.Amount,
        Currency:            "USD",
    })
    if err != nil {
        log.Printf("DRP error: %v", err)
        json.NewEncoder(w).Encode(map[string]interface{}{
            "success": true,
            "drp":     false,
        })
        return
    }
    
    json.NewEncoder(w).Encode(map[string]interface{}{
        "success":      true,
        "confirmation": result.DeliveryConfirmation,
    })
}

func main() {
    http.HandleFunc("/checkout", handleCheckout)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Configuration

Client Options

import "github.com/digitalreceiptprotocol/drp-go"

// Basic client
client := drp.NewClient(
    "mch_live_abc123def456",  // API key
    "issuer-bank.com",         // Issuer domain
    drp.Production,            // Environment (Production or Sandbox)
)

// With custom options
client := drp.NewClientWithOptions(&drp.ClientOptions{
    APIKey:        "mch_live_abc123def456",
    IssuerDomain:  "issuer-bank.com",
    Environment:   drp.Production,
    Timeout:       30 * time.Second,
    MaxRetries:    3,
    Logger:        customLogger,
})

// Load private key
if err := client.LoadPrivateKeyFromFile("./merchant-private-key.pem"); err != nil {
    log.Fatal(err)
}

// Or set private key directly
privateKeyPEM := []byte(`-----BEGIN PRIVATE KEY-----...`)
if err := client.SetPrivateKey(privateKeyPEM); err != nil {
    log.Fatal(err)
}

API Reference

PublicKeys Methods

Get(ctx, cardToken, options)

keyData, err := client.PublicKeys.Get(
    ctx,
    "tok_card_abc123",
    &drp.PublicKeyOptions{
        TransactionID: "txn_auth_456",
    },
)
if err != nil {
    return err
}

fmt.Println(keyData.PublicKey)  // PEM-formatted public key
fmt.Println(keyData.KeyID)      // Key identifier
fmt.Println(keyData.ExpiresAt)  // Expiration time
Returns: *PublicKeyData, error

Receipts Methods

Send(ctx, params)

result, err := client.Receipts.Send(ctx, &drp.SendReceiptParams{
    ReceiptID:           "rcpt_unique_123",
    Receipt:             receiptData,
    CustomerPublicKey:   keyData.PublicKey,
    CustomerPublicKeyID: keyData.KeyID,
    CardLastFour:        "1234",
    TransactionID:       "txn_auth_456",
    MerchantID:          "mch_coffee_shop_001",
    Amount:              12.50,
    Currency:            "USD",
})
if err != nil {
    return err
}

fmt.Println(result.DeliveryConfirmation)
fmt.Println(result.IssuerSignature)
Returns: *ReceiptResponse, error

List(ctx, filters) - User Client Only

userClient := drp.NewUserClient(
    userOAuthToken,
    "your-bank.com",
)

response, err := userClient.Receipts.List(ctx, &drp.ReceiptsListOptions{
    StartDate:  "2025-11-01",
    EndDate:    "2025-12-07",
    MerchantID: "mch_coffee_shop_001",
    Limit:      20,
    Offset:     0,
})
if err != nil {
    return err
}

for _, receipt := range response.Receipts {
    fmt.Printf("Receipt from %s: $%.2f\n", receipt.MerchantName, receipt.Amount)
}
Returns: *ReceiptsListResponse, error

Merchants Methods

Register(ctx, data)

registration, err := drp.RegisterMerchant(ctx, &drp.RegistrationParams{
    RegistrationKey: "reg_key_xyz789",
    IssuerDomain:    "issuer-bank.com",
    MerchantID:      "mch_coffee_shop_001",
    MerchantName:    "Joe's Coffee Shop",
    ContactEmail:    "[email protected]",
    CallbackURL:     "https://coffeeshop.com/drp/webhook",
    PublicKey:       publicKeyPEM,
    BusinessAddress: &drp.Address{
        Street:     "123 Main St",
        City:       "Seattle",
        State:      "WA",
        PostalCode: "98101",
        Country:    "US",
    },
})
if err != nil {
    return err
}

// IMPORTANT: Save these securely!
fmt.Println(registration.APIKey)
fmt.Println(registration.WebhookSecret)
Returns: *RegistrationResponse, error

Crypto Utilities

// Encrypt data
encrypted, err := client.Crypto.Encrypt(
    []byte(receiptJSON),
    customerPublicKey,
)

// Decrypt data
decrypted, err := client.Crypto.Decrypt(
    encryptedData,
    userPrivateKey,
)

// Sign data
signature, err := client.Crypto.Sign(
    []byte(receiptJSON),
    merchantPrivateKey,
)

// Verify signature
valid, err := client.Crypto.Verify(
    []byte(receiptJSON),
    signature,
    merchantPublicKey,
)

Error Handling

import (
    "errors"
    "github.com/digitalreceiptprotocol/drp-go"
)

result, err := client.Receipts.Send(ctx, params)
if err != nil {
    // Check for specific error types
    var rateLimitErr *drp.RateLimitError
    var authErr *drp.AuthenticationError
    
    switch {
    case errors.As(err, &rateLimitErr):
        log.Printf("Rate limited. Retry after %d seconds", rateLimitErr.RetryAfter)
        time.Sleep(time.Duration(rateLimitErr.RetryAfter) * time.Second)
        // Retry logic
        
    case errors.As(err, &authErr):
        log.Fatal("Invalid API key")
        
    case drp.IsCustomerNotEnrolled(err):
        // Customer not enrolled - skip DRP receipt
        log.Println("Customer not enrolled")
        return nil
        
    case drp.IsValidationError(err):
        log.Printf("Validation error: %v", err)
        
    default:
        log.Printf("DRP error: %v", err)
        return err
    }
}

Error Types

type DRPError struct {
    Code    string
    Message string
    Details string
}

type RateLimitError struct {
    DRPError
    RetryAfter int
}

type AuthenticationError struct{ DRPError }
type ValidationError struct{ DRPError }
type NetworkError struct{ DRPError }
type EncryptionError struct{ DRPError }
type SignatureError struct{ DRPError }

Helper Functions

drp.IsCustomerNotEnrolled(err) bool
drp.IsRateLimitError(err) bool
drp.IsValidationError(err) bool
drp.IsAuthenticationError(err) bool

Context & Cancellation

All SDK methods accept context.Context for cancellation and timeouts:
import "context"

// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := client.Receipts.Send(ctx, params)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("Request timed out")
    }
    return err
}

// With cancellation
ctx, cancel := context.WithCancel(context.Background())

go func() {
    time.Sleep(2 * time.Second)
    cancel() // Cancel after 2 seconds
}()

result, err := client.Receipts.Send(ctx, params)

Concurrency

The Go SDK is thread-safe and designed for concurrent use:
var wg sync.WaitGroup

for _, transaction := range transactions {
    wg.Add(1)
    
    go func(txn Transaction) {
        defer wg.Done()
        
        if err := sendReceipt(ctx, client, txn.CardToken, txn.ID); err != nil {
            log.Printf("Error sending receipt for %s: %v", txn.ID, err)
        }
    }(transaction)
}

wg.Wait()

Connection Pooling

The SDK automatically manages HTTP connection pooling. Configure if needed:
import "net/http"

transport := &http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 10,
    IdleConnTimeout:     90 * time.Second,
}

client := drp.NewClientWithOptions(&drp.ClientOptions{
    APIKey:       os.Getenv("DRP_API_KEY"),
    IssuerDomain: "issuer-bank.com",
    HTTPClient:   &http.Client{Transport: transport},
})

Webhooks

Handle webhook events from issuers:
package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
    "os"
    
    "github.com/digitalreceiptprotocol/drp-go/webhooks"
)

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-DRP-Signature")
    webhookSecret := os.Getenv("DRP_WEBHOOK_SECRET")
    
    // Read body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Verify signature
    if !webhooks.Verify(body, signature, webhookSecret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    // Parse event
    var event webhooks.Event
    if err := json.Unmarshal(body, &event); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Handle event
    switch event.Type {
    case webhooks.ReceiptDelivered:
        log.Printf("Receipt %s delivered", event.ReceiptID)
        
    case webhooks.ReceiptDisputed:
        log.Printf("Receipt %s disputed: %s", 
            event.ReceiptID, 
            event.Details.DisputeReason,
        )
        // Handle dispute
    }
    
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
    http.HandleFunc("/drp/webhook", handleWebhook)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Testing

Mock Client

package main

import (
    "testing"
    
    "github.com/digitalreceiptprotocol/drp-go/mocks"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

func TestSendReceipt(t *testing.T) {
    // Create mock client
    mockClient := new(mocks.Client)
    
    // Set expectations
    mockClient.On("Send", mock.Anything, mock.Anything).Return(
        &drp.ReceiptResponse{
            Status:               "accepted",
            ReceiptID:            "rcpt_123",
            DeliveryConfirmation: "conf_456",
        },
        nil,
    )
    
    // Use mock in test
    result, err := mockClient.Send(ctx, params)
    
    assert.NoError(t, err)
    assert.Equal(t, "accepted", result.Status)
    mockClient.AssertExpectations(t)
}

Best Practices

Always pass context with timeout to prevent hanging requests:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

result, err := client.Receipts.Send(ctx, params)
Check for CustomerNotEnrolled explicitly - it’s not an error:
if drp.IsCustomerNotEnrolled(err) {
    // Normal case - skip DRP receipt
    return nil
}
Create one client instance and reuse it across requests. It manages connection pooling internally.
Send receipts concurrently but limit concurrency:
semaphore := make(chan struct{}, 10) // Max 10 concurrent

for _, txn := range transactions {
    semaphore <- struct{}{}
    go func(t Transaction) {
        defer func() { <-semaphore }()
        sendReceipt(ctx, client, t)
    }(txn)
}
Use environment variables or secret management (AWS Secrets Manager, HashiCorp Vault) for API keys and private keys.

Resources


Changelog

v1.0.0 (Latest)

  • Initial stable release
  • Full support for DRP API v1
  • Context support throughout
  • Thread-safe client
  • Connection pooling

v0.9.0 (Beta)

  • Beta release for testing
  • Core merchant and user APIs
See full changelog on GitHub.