Skip to content

Go

Compatibility

Go 1.21+. No third-party dependencies.

Client Implementation

go
package coinepay

import (
    "bytes"
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

const (
    AppID  = "cp_a1b2c3d4..."
    Secret = "f1e2d3c4..."
    Base   = "https://api.coinepay.net"
)

var httpClient = &http.Client{Timeout: 30 * time.Second}

// Call invokes an OpenAPI endpoint
func Call(method, path string, body any, idempotencyKey string) ([]byte, error) {
    var bodyBytes []byte
    if body != nil {
        var err error
        bodyBytes, err = json.Marshal(body)
        if err != nil {
            return nil, err
        }
    } else {
        bodyBytes = []byte("{}")
    }

    nonceBytes := make([]byte, 16)
    if _, err := rand.Read(nonceBytes); err != nil {
        return nil, err
    }
    nonce := hex.EncodeToString(nonceBytes)
    ts := fmt.Sprintf("%d", time.Now().Unix())
    sum := sha256.Sum256(bodyBytes)
    bodyHash := hex.EncodeToString(sum[:])

    signInput := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s", method, path, ts, nonce, bodyHash)
    mac := hmac.New(sha256.New, []byte(Secret))
    mac.Write([]byte(signInput))
    sig := hex.EncodeToString(mac.Sum(nil))

    req, err := http.NewRequest(method, Base+path, bytes.NewReader(bodyBytes))
    if err != nil {
        return nil, err
    }
    req.Header.Set("X-App-Id", AppID)
    req.Header.Set("X-Timestamp", ts)
    req.Header.Set("X-Nonce", nonce)
    req.Header.Set("X-Signature", sig)
    req.Header.Set("Content-Type", "application/json")
    if idempotencyKey != "" {
        req.Header.Set("Idempotency-Key", idempotencyKey)
    }

    resp, err := httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// VerifyWebhook verifies a webhook signature
func VerifyWebhook(sigHeader, tsHeader string, rawBody []byte, webhookSecret string) bool {
    const prefix = "v1,"
    if len(sigHeader) <= len(prefix) || sigHeader[:len(prefix)] != prefix {
        return false
    }
    sig := sigHeader[len(prefix):]
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write([]byte(tsHeader))
    mac.Write([]byte("."))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Usage

go
package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/google/uuid"

    "yourapp/coinepay"
)

type ApplyResp struct {
    Code int `json:"code"`
    Data struct {
        CardID     string `json:"card_id"`
        Status     int    `json:"status"`
        StatusDesc string `json:"status_desc"`
    } `json:"data"`
}

func main() {
    body := map[string]any{
        "header_id":  "hdr_5",
        "package_id": "pkg_12",
        "first_name": "John",
        "last_name":  "Doe",
    }
    raw, err := coinepay.Call("POST", "/api/v1/openapi/card/apply", body, uuid.New().String())
    if err != nil {
        log.Fatal(err)
    }
    var resp ApplyResp
    if err := json.Unmarshal(raw, &resp); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created card: %s, status=%d\n", resp.Data.CardID, resp.Data.Status)
}

Webhook Receiver (net/http)

go
package main

import (
    "encoding/json"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"

    "yourapp/coinepay"
)

var webhookSecret = os.Getenv("COINEPAY_WEBHOOK_SECRET")

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    raw, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "read error", http.StatusBadRequest)
        return
    }

    sig := r.Header.Get("Webhook-Signature")
    ts  := r.Header.Get("Webhook-Timestamp")
    eventID := r.Header.Get("Webhook-Id")
    eventType := r.Header.Get("Webhook-Type")

    if !coinepay.VerifyWebhook(sig, ts, raw, webhookSecret) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }
    tsInt, err := strconv.ParseInt(ts, 10, 64)
    if err != nil || abs(time.Now().Unix()-tsInt) > 300 {
        http.Error(w, "timestamp expired", http.StatusUnauthorized)
        return
    }
    if alreadyProcessed(eventID) {
        w.WriteHeader(http.StatusOK)
        return
    }

    var payload map[string]any
    if err := json.Unmarshal(raw, &payload); err != nil {
        http.Error(w, "invalid json", http.StatusBadRequest)
        return
    }
    if err := enqueue(eventType, payload); err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    markProcessed(eventID)

    w.WriteHeader(http.StatusOK)
}

func abs(n int64) int64 {
    if n < 0 {
        return -n
    }
    return n
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    http.ListenAndServe(":3000", nil)
}

Webhook Receiver (Gin)

go
import (
    "github.com/gin-gonic/gin"
    "io"
)

func setupRouter() *gin.Engine {
    r := gin.Default()
    r.POST("/webhook", func(c *gin.Context) {
        raw, err := io.ReadAll(c.Request.Body)
        if err != nil {
            c.AbortWithStatus(400)
            return
        }
        sig := c.GetHeader("Webhook-Signature")
        ts  := c.GetHeader("Webhook-Timestamp")
        if !coinepay.VerifyWebhook(sig, ts, raw, webhookSecret) {
            c.AbortWithStatus(401)
            return
        }
        // ... handle ...
        c.Status(200)
    })
    return r
}

Released under MIT-equivalent terms.