Skip to content

Go SDK

The Go SDK is a single file, anonyv2.go (package anonyv2), that depends only on the Go standard library and provides every crypto / signing / anti-replay primitive the v2 protocol needs. Unlike the PHP / Node versions, the Go version is a collection of pure functions: there is no Client struct, no built-in HTTP calls, and no built-in nonce dedup store — you make HTTP requests with whatever library you like (all examples on this page use the standard net/http), and you bring your own nonce dedup storage.

Before you start, have all 4 credentials ready (APP-ID / secret / platform public key public_t / merchant private key private_u) — see Credentials & Keys. For protocol details (OAEP-SHA1, the canonical string, the ±5-minute time window, etc.), see Encryption & Signing Protocol.

Getting the SDK

The SDK ships in the integration package provided by the platform, at sdk/go/anonyv2.go. Just copy that file into your project, for example:

your-project/
└── internal/
    └── anonyv2/
        └── anonyv2.go   // package anonyv2

No go get of any third-party dependency is required.

Exported functions

FunctionSignaturePurpose
EncryptEncrypt(plaintext []byte, serverPublicKeyB64 string) (string, error)RSA-OAEP (MGF1 = SHA-1) encryption: the plaintext is split into 214-byte chunks, each chunk encrypted, the ciphertexts concatenated and Base64-encoded as a whole
DecryptDecrypt(cipherB64 string, merchantPrivateKeyB64 string) ([]byte, error)Base64-decodes, then OAEP-decrypts in 256-byte chunks and concatenates
SignSign(canonical string, merchantPrivateKeyB64 string) (string, error)SHA256withRSA (PKCS#1 v1.5) signature over the canonical string, returned as Base64
VerifyVerify(canonical string, serverPublicKeyB64 string, signB64 string) boolSHA256withRSA signature verification (responses / callbacks are verified with the platform public key)
CanonicalCanonical(ts, nonce, payload string) stringBuilds the canonical signing string: ts + "\n" + nonce + "\n" + payload
NewNonceNewNonce() (string, error)Generates 16 bytes from a CSPRNG, hex-encoded (32 characters)
AuthHeaderAuthHeader(appID, secret string) stringComputes the Authorization header value for getToken: "auth " + md5(appID + "&" + secret)
VerifyTimestampVerifyTimestamp(ts string) boolChecks that a second-precision timestamp is within the ±300-second window

Two key parameters run through every function — do not swap their roles:

ParameterCredentialUsed for
serverPublicKeyB64Platform public key public_tEncrypt on outbound requests, Verify on inbound signatures
merchantPrivateKeyB64Merchant private key private_uSign on outbound requests, Decrypt on inbound data

Both are in base64(PEM) format — the entire PEM text Base64-encoded once more (see Credentials & Keys).

Implementation details

  • The OAEP hash is hardwired to SHA-1 inside the SDK (rsa.EncryptOAEP(sha1.New(), ...)). Do not change it to SHA-256, or you and the server will be unable to decrypt each other's ciphertexts.
  • Private keys are parsed as PKCS#8 first (BEGIN PRIVATE KEY), falling back to PKCS#1 (BEGIN RSA PRIVATE KEY) — both formats work. Public keys must be SPKI (BEGIN PUBLIC KEY).
  • Every call Base64-decodes and re-parses the key. At low call rates you won't notice; under high concurrency, cache the parsed keys yourself at a layer above.

Step one: get a token (getToken)

The getToken request is neither encrypted nor signed — it carries only the ANONY-APP-ID and Authorization headers (AuthHeader computes the latter). The response, however, is the same "encrypted + signed" envelope as every other endpoint: you must verify the signature and decrypt before you can read the token field — it is not a plaintext token.

Flow: AuthHeader(appID, secret) computes AuthorizationPOST getToken → parse the outer JSON {code, data, message} → check the response-header timestamp → Verify the signature → Decrypt → read token.

go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"your-project/internal/anonyv2"
)

const (
	baseURL   = "https://api.anonypay.io/api/merchant/"
	appID     = "your merchant ID"
	secret    = "your secret"
	serverPub = "<public_t: platform public key, base64(PEM)>"
	merchPriv = "<private_u: merchant private key, base64(PEM)>"
)

// envelope is the outer structure of every response (always parse it with the
// standard JSON library — escaped \/ inside data is unescaped automatically)
type envelope struct {
	Code    int    `json:"code"`
	Data    string `json:"data"`
	Message string `json:"message"`
}

// openEnvelope processes an inbound envelope: parse outer JSON → check time window →
// verify signature → decrypt, returning the plaintext business JSON.
func openEnvelope(resp *http.Response, body []byte) ([]byte, error) {
	var env envelope
	if err := json.Unmarshal(body, &env); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}
	if env.Code != 10000 {
		return nil, fmt.Errorf("API returned failure: %s", env.Message)
	}
	ts := resp.Header.Get("ANONY-TIMESTAMP")
	nonce := resp.Header.Get("ANONY-NONCE")
	sign := resp.Header.Get("ANONY-SIGN")
	if !anonyv2.VerifyTimestamp(ts) {
		return nil, fmt.Errorf("response timestamp outside the ±5 minute window")
	}
	if !anonyv2.Verify(anonyv2.Canonical(ts, nonce, env.Data), serverPub, sign) {
		return nil, fmt.Errorf("response signature verification failed")
	}
	return anonyv2.Decrypt(env.Data, merchPriv)
}

func getToken() (string, error) {
	req, err := http.NewRequest(http.MethodPost, baseURL+"getToken", nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("ANONY-APP-ID", appID)
	req.Header.Set("Authorization", anonyv2.AuthHeader(appID, secret))

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

	plain, err := openEnvelope(resp, body)
	if err != nil {
		return "", err
	}
	var out struct {
		Token string `json:"token"`
	}
	if err := json.Unmarshal(plain, &out); err != nil {
		return "", err
	}
	return out.Token, nil
}

Token lifetime

A tokenExpired response means the token is no longer valid — just call getToken again. Current implementation details: tokens use an 8-hour sliding expiry with a 24-hour absolute cap. Reuse the token while it is valid; do not fetch a fresh one for every request. See getToken for the endpoint details.

Business requests: the full flow

Every business endpoint other than getToken follows the same sequence:

  1. Serialize the business parameters to JSON;
  2. Encrypt with the platform public key to get the Base64 ciphertext (this is the HTTP body);
  3. NewNonce() to generate a nonce, and take the current second-precision Unix timestamp;
  4. Build the signing string with Canonical(ts, nonce, ciphertext) and Sign it with the merchant private key;
  5. Set the 5 request headers ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN and POST;
  6. Run the response through openEnvelope (time window → signature → decrypt) to get the business JSON back.

Wrap steps 2–6 in a generic post function:

go
import (
	"strconv"
	"strings"
	"time"
)

// post sends one v2 business request and returns the decrypted business JSON.
func post(path string, params any, token string) ([]byte, error) {
	// 1. business JSON
	plain, err := json.Marshal(params)
	if err != nil {
		return nil, err
	}
	// 2. OAEP-encrypt with the platform public key → body
	cipher, err := anonyv2.Encrypt(plain, serverPub)
	if err != nil {
		return nil, err
	}
	// 3. nonce + second-precision timestamp
	nonce, err := anonyv2.NewNonce()
	if err != nil {
		return nil, err
	}
	ts := strconv.FormatInt(time.Now().Unix(), 10)
	// 4. canonical string → sign with the merchant private key
	sign, err := anonyv2.Sign(anonyv2.Canonical(ts, nonce, cipher), merchPriv)
	if err != nil {
		return nil, err
	}
	// 5. the 5 request headers + POST
	req, err := http.NewRequest(http.MethodPost, baseURL+path, strings.NewReader(cipher))
	if err != nil {
		return nil, err
	}
	req.Header.Set("ANONY-APP-ID", appID)
	req.Header.Set("ANONY-TOKEN", token)
	req.Header.Set("ANONY-TIMESTAMP", ts)
	req.Header.Set("ANONY-NONCE", nonce)
	req.Header.Set("ANONY-SIGN", sign)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	// 6. time window → verify signature → decrypt
	return openEnvelope(resp, body)
}

Calling a business endpoint is then just a matter of passing parameters:

go
func main() {
	token, err := getToken()
	if err != nil {
		panic(err)
	}

	// Create a deposit address
	data, err := post("createAddress", map[string]string{
		"userOrder":   "A1001",
		"addressType": "trx",
	}, token)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data)) // {"address":"T...","apiOrder":"...","addressDatas":...}

	// Create a withdrawal order
	data, err = post("createWithdrawOrder", map[string]string{
		"userOrder":    "W2001",
		"address":      "T<destination address>",
		"amount":       "100",
		"currency":     "trx.usdt",
		"withdrawType": "normal",
	}, token)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}

USDC deposits on TRON are closed

Use TRON (trx) deposit addresses for TRX / USDT only. USDC sent to a TRON address is not credited and triggers no callback — the funds land on-chain but are never added to your merchant balance. The withdrawal currency enum likewise does not include trx.usdc (see Create Withdrawal Order for the 13 supported values).

Pass amount fields as strings to avoid precision issues. The request body (ciphertext) is capped at 128KB by default. For each endpoint's parameters and response fields, see the API Reference.

Handling callbacks

The platform POSTs to your callback_url when a deposit is credited or a withdrawal changes status. The HTTP body is the raw Base64 ciphertext (no outer JSON wrapper), and the request carries the ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN headers. Callbacks use the same verify + decrypt logic as responses, with one difference: nonce deduplication on the callback path is your responsibility — the Go SDK ships no dedup store, and skipping dedup means callbacks can be replayed.

Processing order: read headers and body → check the time window (VerifyTimestamp) → dedup the nonce → Verify the signature → Decrypt → dispatch on businessType → return the success response.

go
import "sync"

// —— nonce dedup: in-memory implementation for demo purposes. In production,
//    switch to an atomic store such as Redis, e.g.
//    SET anony:cb:nonce:<nonce> 1 NX EX 900 (TTL of at least 15 minutes).
var (
	seenMu sync.Mutex
	seen   = map[string]int64{}
)

func isFreshNonce(nonce string) bool {
	seenMu.Lock()
	defer seenMu.Unlock()
	now := time.Now().Unix()
	for k, exp := range seen {
		if exp < now {
			delete(seen, k)
		}
	}
	if _, dup := seen[nonce]; dup {
		return false
	}
	seen[nonce] = now + 900
	return true
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}
	ts := r.Header.Get("ANONY-TIMESTAMP")
	nonce := r.Header.Get("ANONY-NONCE")
	sign := r.Header.Get("ANONY-SIGN")

	// 1. ±5 minute time window + 2. nonce dedup (rejects replayed callbacks)
	if nonce == "" || !anonyv2.VerifyTimestamp(ts) || !isFreshNonce(nonce) {
		http.Error(w, "replay rejected", http.StatusForbidden)
		return
	}
	// 3. verify the signature with the platform public key (payload = the raw ciphertext body)
	cipher := string(body)
	if !anonyv2.Verify(anonyv2.Canonical(ts, nonce, cipher), serverPub, sign) {
		http.Error(w, "bad signature", http.StatusForbidden)
		return
	}
	// 4. decrypt with the merchant private key
	plain, err := anonyv2.Decrypt(cipher, merchPriv)
	if err != nil {
		http.Error(w, "decrypt failed", http.StatusBadRequest)
		return
	}

	var evt struct {
		BusinessType string `json:"businessType"`
		UserOrder    string `json:"userOrder"`
		ApiOrder     string `json:"apiOrder"`
		Amount       string `json:"amount"`
		Currency     string `json:"currency"`
		Txid         string `json:"txid"`
		OrderStatus  string `json:"orderStatus"`
	}
	if err := json.Unmarshal(plain, &evt); err != nil {
		http.Error(w, "bad payload", http.StatusBadRequest)
		return
	}

	switch evt.BusinessType {
	case "deposit":
		// Deposit credited: make your crediting logic idempotent (dedupe on txid / apiOrder)
	case "withdraw":
		// Withdrawal status change: update your local order by orderStatus / txid
	}

	// 5. return the success response once processing succeeds
	w.Write([]byte("SUCCESS"))
}

func startCallbackServer() {
	http.HandleFunc("/anony/callback", callbackHandler)
	http.ListenAndServe(":8080", nil)
}

How callback responses are interpreted

The current platform implementation judges the callback result by the response body: a deposit callback must return the plain text SUCCESS to count as delivered; for a withdrawal callback, SUCCESS means confirm and FAIL means reject (which triggers the refund flow) — anything else is retried with backoff. Withdrawals that require finance confirmation first receive a pending-confirmation callback with businessType = "withdrawalPendingConfirm". See Callbacks for the full field and response contract.

You may skip nonce dedup on the synchronous response path (openEnvelope), but on the callback path dedup is mandatory — it is the last line of defense against replay.

Pre-launch self-check (selfcheck)

Before going live, call the read-only health-check endpoint selfcheck with your real keys to exercise the full round trip. It reuses the post function above, with path v2/selfcheck:

go
data, err := post("v2/selfcheck", map[string]string{"hello": "anony"}, token)
if err != nil {
	panic(err)
}
fmt.Println(string(data))
// {"pong":true,"apiVersion":2,"serverTime":...,"echo":{"hello":"anony"}}

If you can verify the signature, decrypt, and get echo back, your encryption / signing / anti-replay implementation is fully aligned with the server. Once the self-check passes, notify the platform to complete activation.

Common pitfalls

  • OAEP must use SHA-1: in Go this is the explicit rsa.EncryptOAEP(sha1.New(), ...). The SDK pins it — changing it guarantees decryption failure.
  • The canonical string must match byte for byte: three segments joined with \n (LF), in the fixed order ts, nonce, body. A single extra space produces signatureFailed.
  • Key roles: outbound — encrypt with the platform public key, sign with the merchant private key; inbound — verify with the platform public key, decrypt with the merchant private key. Swapping them shows up as signature failures or garbage plaintext.
  • Clock sync: both VerifyTimestamp and the server-side window are ±300 seconds. Keep NTP running on your servers, or you will see mysterious signatureFailed errors.
  • Parse the outer JSON with the standard library: / inside the response data may be escaped as \/; encoding/json unescapes it automatically — do not hand-roll string slicing.

For more debugging help, see the Troubleshooting Guide and Error Codes. For SDKs in other languages, see the SDK Overview.