Go SDK
github.com/noresponsehq/go/v2 is the server-side client for Go. It covers every route in the interactive reference, verifies webhooks and turns your auth provider’s verified claims into auth_context. Requests and responses are typed structs in the models/components package, with the contract’s descriptions as doc comments.
Install
Section titled “Install”go get github.com/noresponsehq/go/v2import ( noresponse "github.com/noresponsehq/go/v2" "github.com/noresponsehq/go/v2/models/components")The root package is noresponse. Request and response types live in github.com/noresponsehq/go/v2/models/components, typed API errors in github.com/noresponsehq/go/v2/models/apierrors and per-call options in github.com/noresponsehq/go/v2/models/operations. The module needs Go 1.24 or later. The source and issue tracker are at github.com/noresponsehq/go. Keep the client on your backend, where your API key lives.
Client
Section titled “Client”client := noresponse.NewClient(os.Getenv("NORESPONSE_API_KEY"), noresponse.WithTimeout(10*time.Second), noresponse.WithRetryConfig(retry.Config{ Strategy: "backoff", Backoff: &retry.BackoffStrategy{InitialInterval: 200, MaxInterval: 2000, Exponent: 2, MaxElapsedTime: 5000}, // milliseconds RetryConnectionErrors: true, }),)NewClient(apiKey, opts...) returns a *noresponse.NoResponse and no error. It reads nothing from the environment, so pass your nrk_ key. noresponse.New(opts...) is the same constructor without a key argument: it reads NORESPONSE_API_KEY unless you pass WithAPIKey or WithSecurity.
| Option | Meaning |
|---|---|
WithAPIKey(key) |
Your nrk_ key. WithSecurity(key) is the same option. |
WithSecuritySource(fn) |
A function that returns the key for each call, for a key you rotate while the process runs. |
WithServerURL(url) |
The API host. The default is https://api.noresponse.org. |
WithTimeout(d) |
A timeout applied to each operation. |
WithRetryConfig(config) |
The retry policy for every call. See Retries. |
WithClient(c) |
Your own HTTP client, any value with a Do(*http.Request) (*http.Response, error) method, for a proxy, a transport or tests. |
The default HTTP client has a 60 second timeout. Nothing on the client changes after construction, so one client is safe to share across goroutines.
Every method takes a context.Context as its first argument. Cancelling it cancels the request and any retry wait.
Methods
Section titled “Methods”| Method | Calls | Result |
|---|---|---|
client.Assessments.Create(ctx, idempotencyKey, request, opts...) |
POST /v1/assessments |
res.Result is a components.Assessment |
client.Assessments.Get(ctx, id, responseMode, opts...) |
GET /v1/assessments/{id} |
res.Result is a components.Assessment |
noresponse.WaitForAssessment(ctx, client, id, responseMode) |
Polls GET /v1/assessments/{id} |
*components.Assessment that is no longer pending |
client.Assessments.Query(ctx, query, opts...) |
POST /v1/assessments/query |
res.Result is a components.AssessmentList; res.Next() fetches the next page |
noresponse.AllAssessments(ctx, client, query) |
POST /v1/assessments/query, page by page |
iter.Seq2[components.Assessment, error] |
client.Assessments.RecordOutcome(ctx, id, idempotencyKey, request, opts...) |
POST /v1/assessments/{id}/outcomes |
res.Result is a components.AssessmentOutcome |
client.Keys.Rotate(ctx, request, opts...) |
POST /v1/keys/rotate |
res.Result is a components.KeyRotation |
noresponse.ConstructEvent(payload, headers, secrets, opts...) |
No request | *noresponse.Event |
noresponse.VerificationResponse(event) |
No request | map[string]string to encode as JSON |
Every response also carries res.Headers, the response headers as a map[string][]string. Wrap it in http.Header(res.Headers) to read one, for example X-Request-ID, Retry-After, Location on a 202 or Idempotent-Replayed. Optional request fields are pointers; noresponse.Pointer(value) makes one, and noresponse.String, noresponse.Bool, noresponse.Int and noresponse.Int64 do the same for one type each.
Assessments.Create
Section titled “Assessments.Create”res, err := client.Assessments.Create(ctx, "signup-usr_8f2k1-01JB3C4D", components.AssessmentRequest{ AccountID: "usr_8f2k1", Identifiers: components.AssessmentRequestIdentifiers{ Name: noresponse.Pointer("Jordan Lee"), IP: noresponse.Pointer("81.2.69.160"), }, PolicyContext: &components.AssessmentRequestPolicyContext{ Flow: noresponse.Pointer(components.FlowSignup), Region: noresponse.Pointer("GB"), }, RequestID: noresponse.Pointer("req_01"), DeadlineMs: noresponse.Pointer(int64(400)), ResponseMode: noresponse.Pointer(components.ResponseModeStandard), ClientContext: &components.AssessmentRequestClientContext{ Timezone: noresponse.Pointer("Europe/London"), Locale: noresponse.Pointer("en-GB"), Platform: noresponse.Pointer(components.PlatformWeb), }, AuthContext: authContext, // from noresponse.FromClerkClaims and friends})if err != nil { return err}assessment := res.ResultThe request mirrors the contract: Identifiers holds Email, Username, Phone, Name and IP, and PolicyContext holds Flow and Region. The limits are on Create an assessment.
The method returns the assessment for both a 200 and a 202. Read assessment.Status: components.StatusComplete and components.StatusPartial carry Risk and Recommendation; components.StatusPending carries neither, so both are nil.
Idempotency keys
Section titled “Idempotency keys”Create and RecordOutcome take the Idempotency-Key as a required string argument. The same key with the same body replays the stored answer and starts no new work; the same key with a different body is a 409. noresponse.NewIdempotencyKey() returns a random UUID. Derive the key from your own signup attempt or case instead, and store it beside the signup, so a retry after a crash sends the same one. The client reuses the key on every automatic retry of one call.
res, err := client.Assessments.Create(ctx, "signup-"+signupAttempt.ID, request)if err == nil && http.Header(res.Headers).Get("Idempotent-Replayed") != "" { fmt.Println("replayed", http.Header(res.Headers).Get("X-Request-ID"))}Assessments.Get and WaitForAssessment
Section titled “Assessments.Get and WaitForAssessment”detailed := components.ResponseModeDetailedlatest, err := client.Assessments.Get(ctx, assessment.AssessmentID, &detailed)if err != nil { return err}
settled, err := noresponse.WaitForAssessment(ctx, client, assessment.AssessmentID, nil)if err != nil { return err}Get takes the response mode as a pointer; pass nil for the default. WaitForAssessment polls Get until Status is not pending, sleeping for Retry-After between polls with a one second floor. When the context has no deadline it gives up after noresponse.DefaultWaitTimeout (30 seconds); give the context a deadline to choose your own. Running out of time returns a *noresponse.WaitTimeoutError that carries the last assessment it saw, and a cancelled context returns the context’s error. Do not hold a signup open on it: apply your fallback first and let the result arrive by webhook or a background goroutine.
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)defer cancel()settled, err := noresponse.WaitForAssessment(waitCtx, client, assessment.AssessmentID, nil)var timedOut *noresponse.WaitTimeoutErrorif errors.As(err, &timedOut) { fmt.Println("still pending", timedOut.Last.AssessmentVersion)}Assessments.Query and AllAssessments
Section titled “Assessments.Query and AllAssessments”res, err := client.Assessments.Query(ctx, components.AssessmentQuery{ AccountID: "usr_8f2k1", Limit: noresponse.Pointer(int64(50)),})if err != nil { return err}page := res.Resultfmt.Println(len(page.Items), page.NextCursor, page.RetentionDays) // []components.Assessment, *string, *int64
if res.Next != nil { next, err := res.Next() // the next page, or nil on the last page if err != nil { return err } _ = next}Query returns one page, newest first. res.Next is nil on the last page and otherwise fetches the following one. The query needs a key with the history scope. See Get and query assessments.
Paging
Section titled “Paging”query := components.AssessmentQuery{AccountID: "usr_8f2k1"}for assessment, err := range noresponse.AllAssessments(ctx, client, query) { if err != nil { return err } fmt.Println(assessment.AssessmentID, assessment.Status) // every unexpired assessment, one request per page}AllAssessments follows the cursor for you, so there is nothing to carry between pages. When a page fails it yields a zero components.Assessment with the error and stops.
Assessments.RecordOutcome
Section titled “Assessments.RecordOutcome”res, err := client.Assessments.RecordOutcome(ctx, assessment.AssessmentID, "outcome-"+caseID, components.AssessmentOutcomeRequest{ Outcome: components.OutcomeConfirmedAbuse, ObservedAt: noresponse.Pointer("2026-10-20T05:00:00Z"), ExternalRef: noresponse.Pointer("case-123"), Notes: noresponse.Pointer("Card testing from this account."),})if err != nil { return err}fmt.Println(res.Result.ID, res.Result.ReceivedAt)Outcome is one of OutcomeConfirmedAbuse, OutcomeAccountTakeover, OutcomeChargeback, OutcomeFalsePositive, OutcomeLegitimate or OutcomeUnknown. ObservedAt is an RFC 3339 string. The idempotency key stays bound for as long as the outcome is kept, so derive it from your case rather than generating a fresh one per call.
Keys.Rotate
Section titled “Keys.Rotate”res, err := client.Keys.Rotate(ctx, components.KeyRotationRequest{OverlapSeconds: noresponse.Pointer(int64(86400))})if err != nil { return err}rotation := res.ResultnewKey := rotation.Key // the new key, shown oncefmt.Println(rotation.PreviousKeyExpiresAt) // when the key that made this call stops workingThe new key has the scopes of the key that made the call. Store it at once and roll it out before PreviousKeyExpiresAt. The client keeps using the key it was built with, so build a new client with the new key. See Authentication.
Errors
Section titled “Errors”Every API failure is a typed error you inspect with errors.As. Each carries the problem document’s Type, Title, Detail, Code, RequestID and InvalidParams.
_, err := client.Assessments.Get(ctx, id, nil)
var notFound *apierrors.NotFoundErrorvar limited *apierrors.RateLimitErrorvar bad *apierrors.BadRequestErrorvar other *apierrors.APIStatusErrorswitch {case errors.As(err, ¬Found): log.Println("no such assessment, request", notFound.RequestID)case errors.As(err, &limited): log.Println("rate limited, request", limited.RequestID) // quote it when you contact uscase errors.As(err, &bad): for _, param := range bad.InvalidParams { log.Println(param.Field, param.Reason) // on a 400 }case errors.As(err, &other): log.Println("HTTP", other.StatusCode)case err != nil: log.Println(err) // a network failure or a cancelled context}// Apply your fallback.| Error | Returned for |
|---|---|
*apierrors.BadRequestError |
400 |
*apierrors.AuthenticationError |
401 |
*apierrors.PermissionDeniedError |
403 |
*apierrors.NotFoundError |
404 |
*apierrors.ConflictError |
409 |
*apierrors.RateLimitError |
429 |
*apierrors.APIStatusError |
Any other status, with StatusCode, Message and Body |
A failed connection, a timeout or a cancelled context returns the underlying error from net/http or context, so use errors.Is(err, context.DeadlineExceeded) to detect a timeout. The codes are listed on Errors and limits.
Retries
Section titled “Retries”The client retries 408, 429, 5xx answers and connection errors with exponential backoff and jitter: 500 ms first, 8 seconds at most between attempts and 30 seconds in all. Every attempt of a create or outcome call sends the same idempotency key, so a retry never repeats work. Change the policy for the whole client with WithRetryConfig, or for one call with operations.WithRetries:
res, err := client.Assessments.Get(ctx, id, nil, operations.WithRetries(retry.Config{Strategy: "none"}))retry is github.com/noresponsehq/go/v2/retry. Durations in retry.BackoffStrategy are milliseconds. operations.WithOperationTimeout(d) bounds one call.
Webhooks
Section titled “Webhooks”Pass the raw request body. Re-encoding parsed JSON breaks the signature.
secrets := map[string]string{os.Getenv("NORESPONSE_WEBHOOK_KEY_ID"): os.Getenv("NORESPONSE_WEBHOOK_SECRET")}
http.HandleFunc("/webhooks/noresponse", func(w http.ResponseWriter, r *http.Request) { payload, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { http.Error(w, "unreadable body", http.StatusBadRequest) return } event, err := noresponse.ConstructEvent(payload, r.Header, secrets, noresponse.WithTolerance(300*time.Second)) if err != nil { http.Error(w, "invalid signature", http.StatusBadRequest) return } switch data := event.Data.(type) { case *components.Assessment: if isNewer(event.ID, data) { apply(data) // the full assessment, as Get returns it } case *noresponse.AssessmentExpired: forget(data.AssessmentID) case *noresponse.EndpointVerification: body, err := noresponse.VerificationResponse(event) if err != nil { http.Error(w, "invalid verification", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(body) return } w.WriteHeader(http.StatusOK)})ConstructEvent(payload, headers, secrets, opts...) takes the raw body as []byte, the request’s http.Header and a map[string]string from webhook key id (whk_ and its characters) to secret. It checks that each NR-Webhook-* header is present exactly once, that the timestamp is within the tolerance (noresponse.DefaultWebhookTolerance, 300 seconds, unless WithTolerance says otherwise), that the key id names a secret you hold, that the signature over timestamp + "." + raw body matches in constant time and that the body’s id equals NR-Webhook-Id. Every failure is a *noresponse.WebhookError that matches noresponse.ErrWebhook with errors.Is; its Code says which check failed.
The returned Event carries ID, Type, CreatedAt and Data. For an assessment.updated event Data is a *components.Assessment, or a *noresponse.AssessmentExpired for the terminal expiry notice; event.Expired() reports the second. For an endpoint.verification event Data is a *noresponse.EndpointVerification, and VerificationResponse(event) returns the map to encode as the answer.
Deliveries arrive at least once and unordered. Deduplicate on event.ID and apply an assessment only when its AssessmentVersion is higher than the one you hold; an expiry is always applied. See Webhooks.
Auth adapters
Section titled “Auth adapters”authContext, err := noresponse.FromClerkClaims(verifiedClaims, map[string]any{ "email_verified": true, "account_created_at": user.CreatedAt.UTC().Format(time.RFC3339),})if err != nil { return err}Each adapter takes the claims your backend has already verified, as a map[string]any, and returns a *components.AuthContext with Verified true and Transport backend. Overrides is a second map[string]any of facts the token does not carry, keyed by the auth_context field names, such as email_verified, phone_verified, account_created_at, session_created_at, impersonated or mfa. Pass nil for none. Timestamps are RFC 3339 strings. Every adapter applies the contract’s bounds, dropping over-long values rather than truncating them, and returns an error matching noresponse.ErrInvalidAuthContext when the issuer or subject is missing. Pass verified claims, never the raw token.
| Function | Provider notes |
|---|---|
FromWorkOSClaims(claims, overrides) |
WorkOS AuthKit access token claims. Reads organisation, roles and permissions. |
FromClerkClaims(claims, overrides) |
Clerk session claims. Pass the session’s creation time as the session_created_at override. |
FromAuth0Claims(claims, overrides) |
Auth0 ID token, userinfo or session claims, not an opaque API access token. |
FromOIDCClaims(provider, claims, overrides) |
Any other OpenID Connect provider. provider is a lowercase slug such as keycloak. |
FromSupabaseClaims(claims, overrides) |
Supabase Auth access token claims. Pass the user record’s contact verification as overrides. |
FromFirebaseClaims(claims, overrides) |
Firebase ID token claims from the Admin SDK’s VerifyIDToken. Custom claims never change the mapping. |
The guide Send your auth session shows each provider end to end.
Every request, response, webhook and error type is exported: components.Assessment, components.Risk, components.AssessmentOutcome, components.KeyRotation and the rest. Enums are typed string constants (components.Status*, Recommendation*, Band*, Confidence*, Flow*, ResponseMode*, Outcome*, ReasonCode*, Lane*), so a misspelt value does not compile.
Upgrading from 1.x
Section titled “Upgrading from 1.x”The 1.x client was hand-written; 2.x is generated from the contract and the call shapes differ.
| 1.x | 2.x |
|---|---|
client, err := noresponse.NewClient(noresponse.WithAPIKey(key)) |
client := noresponse.NewClient(key) |
client.Assessments.Create(ctx, noresponse.CreateAssessmentParams{...}, noresponse.WithIdempotencyKey(k)) |
client.Assessments.Create(ctx, k, components.AssessmentRequest{...}) |
The assessment returned directly, with response headers on Meta |
res.Result for the assessment and res.Headers for the headers |
client.Assessments.Wait(ctx, id) |
noresponse.WaitForAssessment(ctx, client, id, nil) |
client.Assessments.QueryAll(ctx, params) |
noresponse.AllAssessments(ctx, client, components.AssessmentQuery{...}) |
One *noresponse.Error with sentinels such as ErrRateLimited |
Typed errors in apierrors, checked with errors.As |
StatusExpired on an *Assessment |
A *noresponse.AssessmentExpired in event.Data |
FromClerk(claims, *AuthContext) |
FromClerkClaims(claims, map[string]any) |