There is no Go SDK for Maskbreak. There are Node, Python and PHP packages, and Go is conspicuously absent from that list — which comes up often enough in support that it is worth answering properly.
The honest answer: for a one-endpoint JSON API, you do not want one. The wrapper is about seventy lines of standard library, it has no third-party dependencies, and writing it yourself means the timeout, the failure mode and the retry policy are decisions you made rather than ones you inherited from a vendor. That is a better trade in Go than it is almost anywhere else.
The client
One POST, one JSON body, one bearer token. No streaming, no pagination, no state.
package fraud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
type Client struct {
Key string
HTTP *http.Client
}
func New(key string) *Client {
return &Client{
Key: key,
// Reuse connections: a fresh TLS handshake per signup is a
// self-inflicted 100ms.
HTTP: &http.Client{Timeout: 2 * time.Second},
}
}
type Request struct {
Token string `json:"token,omitempty"`
FingerprintEvent string `json:"fingerprintEventId,omitempty"`
Email string `json:"email,omitempty"`
AccountID string `json:"accountId,omitempty"`
}
type Verdict struct {
Decision string `json:"decision"` // allow | review | block
RiskScore int `json:"risk_score"`
Reasons []string `json:"reasons"`
IP string `json:"ip"`
Network struct {
VPN bool `json:"vpn"`
Proxy bool `json:"proxy"`
Datacenter bool `json:"datacenter"`
Tor bool `json:"tor"`
Residential bool `json:"residential"`
} `json:"network"`
Device struct {
Antidetect bool `json:"antidetect"`
Automation bool `json:"automation"`
VisitorID string `json:"visitor_id"`
} `json:"device"`
}
func (c *Client) Evaluate(ctx context.Context, in Request) (*Verdict, error) {
body, err := json.Marshal(in)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://maskbreak.com/v1/evaluate", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Key)
// Without this the body is not parsed and the API answers 400.
req.Header.Set("Content-Type", "application/json")
res, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("maskbreak: status %d", res.StatusCode)
}
var v Verdict
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
return nil, err
}
return &v, nil
}
Two details carry more weight than they look like they do. The Content-Type header is not optional — without it the body is never parsed and every call comes back as a 400 that reads like an authentication problem. And the shared http.Client is what keeps connections alive; constructing one per request adds a TLS handshake to every signup you handle.
The middleware
The verdict belongs in the request context so the handler decides what to do with it. Middleware that refuses requests on its own is middleware you cannot reuse across a signup form and a checkout.
type ctxKey struct{}
// Guard evaluates the session and stashes the verdict. It never refuses a
// request itself: allow/review/block is a product decision, and the same
// middleware has to serve a signup form and a payout endpoint.
func (c *Client) Guard(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Independent of the caller's deadline: a slow fraud check must
// not consume the budget the handler needs to answer.
ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel()
_ = r.ParseForm()
v, err := c.Evaluate(ctx, Request{
Token: r.FormValue("monocle"),
FingerprintEvent: r.FormValue("sentinel_fp"),
})
if err != nil {
// Fail open, and make the failure countable.
evalErrors.Add(1)
next.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, v)))
})
}
// FromContext returns the verdict, or nil when the check did not complete.
func FromContext(ctx context.Context) *Verdict {
v, _ := ctx.Value(ctxKey{}).(*Verdict)
return v
}
The nil return is the contract, and handlers have to respect it. FromContext returning nil means the check did not complete — not that the session was clean.
Using it
mux := http.NewServeMux()
guard := fraud.New(os.Getenv("MASKBREAK_KEY")).Guard
// Mounted per action, not globally.
mux.Handle("/signup", guard(http.HandlerFunc(signup)))
mux.Handle("/payout", guard(http.HandlerFunc(payout)))
func signup(w http.ResponseWriter, r *http.Request) {
v := fraud.FromContext(r.Context())
switch {
case v == nil:
// Check unavailable. Proceed, and let the rest of your controls work.
case v.Decision == "block":
http.Error(w, "unable to complete signup", http.StatusForbidden)
return
case v.Decision == "review":
requireEmailOTP(r) // step up, do not refuse
}
createAccount(w, r)
}
Mounting per action rather than on the whole mux matters more in Go than in frameworks where middleware registration is centralised and invisible. Wrapping every route triples call volume, adds latency to endpoints that gain nothing, and converts a vendor slowdown into a whole-site problem instead of a checkout problem.
A breaker for the bad afternoon
Failing open on a single timeout is correct. Failing open on ten thousand consecutive timeouts, each costing 800ms of a request handler, is a queue backing up. Ten lines fix it:
type breaker struct {
mu sync.Mutex
fails int
openTo time.Time
}
func (b *breaker) allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
return time.Now().After(b.openTo)
}
func (b *breaker) record(err error) {
b.mu.Lock()
defer b.mu.Unlock()
if err == nil {
b.fails = 0
return
}
if b.fails++; b.fails >= 5 {
b.openTo = time.Now().Add(30 * time.Second) // skip the call entirely
b.fails = 0
}
}
Five consecutive failures and the middleware stops calling out for thirty seconds, allowing traffic through with no verdict at all. You lose detection for half a minute. You do not lose the site, which is the trade you want during someone else’s incident.
Testing without a key
The sandbox key is public and returns every documented shape deterministically, so integration tests need no account and no network fixtures beyond the vendor itself:
func TestVerdictShapes(t *testing.T) {
c := fraud.New("sk_test_sandbox")
for token, want := range map[string]string{
"test_clean": "allow",
"test_vpn": "review",
"test_proxy": "block",
"test_tor": "block",
} {
v, err := c.Evaluate(context.Background(), fraud.Request{Token: token})
if err != nil {
t.Fatalf("%s: %v", token, err)
}
if v.Decision != want {
t.Errorf("%s: got %q want %q", token, v.Decision, want)
}
}
}
Note test_tor, which returns block at a risk score of 15. If your handler compares the score against a threshold instead of reading Decision, that test is where you find out. There is a longer argument about why, but the short version is that the score is a magnitude and the decision is the verdict.
Before it ships
- Key read from the environment, never compiled in, never shipped to a browser — including via WebAssembly.
Content-Type: application/jsonset on every call.- One shared
http.Client, not one per request. - A context deadline shorter than your handler’s own budget.
- Every error path allows the request and increments a counter you actually graph.
reviewsteps up; it does not route to the same branch asblock.
Frequently Asked Questions
Is there an official Go SDK?
Not today. The published SDKs are Node, Python and PHP. For a single-endpoint JSON API a Go wrapper is about seventy lines of net/http with no third-party dependencies, and writing it yourself means the timeout, the retry policy and the failure mode are decisions you made rather than ones you inherited.
Should the middleware run on every route?
No. Mount it on the handful of actions where abuse actually costs money — signup, login, checkout, payout, promo redemption. Wrapping every route triples your call volume, adds latency to endpoints that gain nothing from it, and makes a vendor slowdown a whole-site problem instead of a checkout problem.
What happens if the API times out?
The request is allowed. That is deliberate: a fraud check that fails closed turns a vendor incident into an outage of your own signup or checkout, which is nearly always the more expensive failure. The middleware in this post uses a 800ms context deadline, allows on any error, and increments a counter so a silent degradation still shows up in your metrics.
Where does the token come from in a Go application?
From the client SDK, which injects two hidden inputs into your form: monocle for the network layer and sentinel_fp for the device event id. Your handler reads them off the parsed form or the JSON body and forwards them. Neither is a secret and neither is trusted on its own — the server side resolves both against the vendor before any verdict exists.
Does the API key ever belong in client-side Go or WASM?
No. A live key authenticates your account and belongs only in server-side code, read from the environment. Anything compiled to WebAssembly and shipped to a browser is client-side code, whatever language it started as, and a key embedded there is a key you have published.
One endpoint, no SDK required
A free key, a POST, and a decision in under 40ms. The sandbox key returns every documented shape before you write a line.
Try Maskbreak free →