Go SDK
The official HashNut Go SDK provides a convenient wrapper around the HashNut Payment API.
Installation
go get github.com/nuttybounty/hashnut-sdk-go/v4Initialize the Client
import hashnut "github.com/nuttybounty/hashnut-sdk-go/v4"
// Always pass false as the second argument — production (https://defi.hashnut.io/api/v4.0.0)
client := hashnut.NewClient("your-secret-key", false)import hashnut "github.com/nuttybounty/hashnut-sdk-go/v4"
// WithBaseURL must include /api/v4.0.0 — the SDK appends relative paths.
client := hashnut.NewClient(
"your-secret-key",
false,
hashnut.WithBaseURL("https://custom.endpoint.com/api/v4.0.0"),
)NOTE
The SDK takes only the secretKey. accessKeyId is a per-request parameter (CreateOrderRequest.AccessKeyID), not a client constructor argument.
Methods
CreateOrder
Create a new payment order.
import "github.com/nuttybounty/hashnut-sdk-go/v4/model"
order, err := client.CreateOrder(&model.CreateOrderRequest{
AccessKeyID: "your-access-key-id",
MerchantOrderID: "ORDER-001",
BlockChain: "ETH",
TokenSymbol: "usdt",
Amount: "25.00",
SplitterAddress: "0xYourSplitterAddress",
// Optional fields
Subject: "Monthly Subscription",
ExpireDuration: 1800, // seconds
CallbackURL: "https://yoursite.com/payment-result",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Pay Order ID: %s\n", order.PayOrderID)
fmt.Printf("Receipt Address: %s\n", order.ReceiptAddress)
fmt.Printf("Pay URL: %s\n", order.PayURL)IMPORTANT
Persist order.AccessSign alongside payOrderId in your own order table. Every later QueryOrder / ConfirmPaid call needs it, and you cannot recompute it — the server derives it by HMAC-ing payOrderId + merchantOrderId with your secretKey.
QueryOrder
Query the current state of an order. All three fields are required; the server errors out if any is missing.
result, err := client.QueryOrder(&model.QueryOrderRequest{
PayOrderID: order.PayOrderID,
MerchantOrderID: "ORDER-001",
AccessSign: order.AccessSign, // from the create-order response
})
if err != nil {
log.Fatal(err)
}
// Numeric fields like State and Amount are json.Number, not int
fmt.Printf("State: %s\n", result.State.String())
fmt.Printf("Amount: %s\n", result.Amount.String())
fmt.Printf("Tx Hash: %s\n", result.PayTxID)ConfirmPaid
Confirm a payment manually with its transaction hash. Returns only an error, not the order — call QueryOrder afterwards to read the new state.
err := client.ConfirmPaid(&model.ConfirmPaidRequest{
PayOrderID: order.PayOrderID,
MerchantOrderID: "ORDER-001",
AccessSign: order.AccessSign,
PayTxID: "0x9876543210fedcba...",
})
if err != nil {
log.Fatal(err)
}CancelOrder
Cancel an unpaid order. Takes only PayOrderID and likewise returns only an error.
err := client.CancelOrder(&model.CancelOrderRequest{
PayOrderID: order.PayOrderID,
})
if err != nil {
log.Fatal(err)
}
// The order moves to state -3 (CANCELED); confirm with QueryOrderQueryChains
Query all supported blockchain networks. Takes no arguments.
chains, err := client.QueryChains()
if err != nil {
log.Fatal(err)
}
for _, chain := range chains {
fmt.Printf("%s chainId=%d confirms=%d eip1559=%v\n",
chain.BlockChain, chain.ChainID, chain.TxConfirmCount, chain.EIP1559Support)
}QueryCoins
Query all supported tokens. Takes no arguments — fetch the full list and filter by BlockChain yourself.
coins, err := client.QueryCoins()
if err != nil {
log.Fatal(err)
}
for _, coin := range coins {
if coin.BlockChain != "ETH" {
continue
}
fmt.Printf("%s/%s decimals=%d contract=%s\n",
coin.BlockChain, coin.TokenSymbol, coin.Decimals, coin.ContractAddress)
}Error Handling
All methods return an error as the second return value. The SDK does not export a structured error type; API business errors are wrapped into an error whose message looks like api error (code=1): xxx:
_, err := client.CreateOrder(&model.CreateOrderRequest{
// ...
})
if err != nil {
// business error: api error (code=1): invalid request sign
// HTTP error: http 502: <body>
// network/parse: http request: ... / unmarshal response: ...
fmt.Printf("create order failed: %v\n", err)
return
}WARNING
Do not use errors.As to assert a concrete error type — the SDK returns plain errors built with fmt.Errorf. To branch on error codes, match the message, or take over the request yourself with WithHTTPClient.
WARNING
Numeric fields in API responses (such as amount and paidAmount) are returned as json.Number to handle both string and number representations from the backend. Use .String() to read the value or .Int64() / .Float64() for numeric conversion.
Complete Example
package main
import (
"fmt"
"log"
"time"
hashnut "github.com/nuttybounty/hashnut-sdk-go/v4"
"github.com/nuttybounty/hashnut-sdk-go/v4/model"
)
func main() {
client := hashnut.NewClient("your-secret-key", false) // second argument is always false
order, err := client.CreateOrder(&model.CreateOrderRequest{
AccessKeyID: "your-access-key-id",
MerchantOrderID: fmt.Sprintf("ORDER-%d", time.Now().Unix()),
BlockChain: "ETH",
TokenSymbol: "usdt",
Amount: "1.00",
SplitterAddress: "0xYourSplitterAddress",
Subject: "Test Order",
})
if err != nil {
log.Fatalf("create order: %v", err)
}
// In a real integration, persist PayOrderID + AccessSign — later calls need them
fmt.Printf("Order created: %s\n", order.PayOrderID)
fmt.Printf("Send funds to: %s usdt -> %s\n", order.Amount.String(), order.ReceiptAddress)
fmt.Printf("Pay URL: %s\n", order.PayURL)
// Poll for payment (in production, use webhooks instead)
for i := 0; i < 60; i++ {
time.Sleep(10 * time.Second)
result, err := client.QueryOrder(&model.QueryOrderRequest{
PayOrderID: order.PayOrderID,
MerchantOrderID: order.MerchantOrderID,
AccessSign: order.AccessSign,
})
if err != nil {
log.Printf("Query error: %v", err)
continue
}
// State is a json.Number: convert to int64 before comparing
state, err := result.State.Int64()
if err != nil {
log.Printf("bad state %q: %v", result.State.String(), err)
continue
}
fmt.Printf("State: %d\n", state)
if state == 3 { // 3 = SUCCESS, 4 = FINISH
fmt.Println("Payment successful!")
return
}
if state < 0 { // -1 FAILED / -2 EXPIRED / -3 CANCELED
log.Fatalf("order ended in state %d", state)
}
}
log.Println("Polling timed out; order still not paid")
}TIP
In production, prefer webhook notifications over polling. Polling is shown here for simplicity.