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
}