Go SDK
The Go SDK (github.com/xident-io/go-sdk/v3) is an idiomatic Go client for the Xident age verification API. It uses the functional options pattern for configuration, returns (result, *Response, error) triples following the go-github convention, and accepts context.Context on every method. The client is safe for concurrent use across goroutines.
Secret key required: The Go SDK is a server-side SDK. Use your secret key (sk_live_) from the dashboard. The key is sent via the X-API-Key header. Never expose it in client-side code.
Installation
go get github.com/xident-io/go-sdk/v3
Quick Start
package main
import (
"context"
"fmt"
"log"
"os"
xident "github.com/xident-io/go-sdk/v3"
)
func main() {
client := xident.NewClient(os.Getenv("XIDENT_SECRET_KEY"))
// 1. Create a verification session
init, _, err := client.Verification.Init(context.Background(), &xident.InitParams{
CallbackURL: "https://yoursite.com/webhook",
MinAge: 18,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Redirect user to:", init.VerifyURL)
// 2. After user completes verification, check result server-side
session, _, err := client.Verification.GetResult(context.Background(), init.Token)
if err != nil {
log.Fatal(err)
}
if session.IsVerified() {
fmt.Printf("Verified! Age bracket: %d\n", *session.AgeBracket())
}
}
API version
This SDK sends the dated API version it was built against on every request, so its result types always match the payload it receives. That means upgrading the SDK's major version is how you adopt a new API version — a patch or minor release never changes the shape you get.
The constant is xident.PinnedAPIVersion. To trial a newer version
before changing your website's pin in the dashboard:
client := xident.NewClient(apiKey, xident.WithAPIVersion("2026-08-13"))
Every response echoes the version it was served under, in the
X-API-Version header and in meta.api_version. See
API versioning.
Configuration
The client is configured using functional options passed to NewClient. All options are optional -- defaults are production-ready.
client := xident.NewClient("sk_live_xxx",
xident.WithBaseURL("https://staging-api.xident.io"),
xident.WithTimeout(15 * time.Second),
xident.WithMaxRetries(5),
xident.WithHTTPClient(&http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
},
}),
)
| Option | Default | Description |
|---|---|---|
WithBaseURL(url) | https://api.xident.io | API base URL. Override for staging or self-hosted. |
WithTimeout(d) | 30s | HTTP request timeout. Ignored if WithHTTPClient is used. |
WithMaxRetries(n) | 3 | Max retries on 5xx errors. Exponential backoff with jitter. Set to 0 to disable. |
WithHTTPClient(hc) | Default http.Client | Custom *http.Client for full control over transport, TLS, proxies. |
WithUserAgent(ua) | Xident-Go/2.0.0 | Override the User-Agent header. |
Verification
Init (Create Session)
Create a verification session. Returns a short-lived token (10-minute TTL) and a URL to redirect the user to.
result, resp, err := client.Verification.Init(ctx, &xident.InitParams{
// Required
CallbackURL: "https://yoursite.com/webhook",
// Optional
MinAge: 18, // 12, 15, 18, 21, or 25
SuccessURL: "https://yoursite.com/success",
FailedURL: "https://yoursite.com/failed",
UserID: "user_123", // your internal user ID
Theme: "dark", // "light" or "dark"
Locale: "de", // "en", "de", "fr", etc.
Metadata: "order_456", // up to 500 chars, passed to webhook
})
if err != nil {
log.Fatal(err)
}
// result.Token -> "xit_abc123..." (10-minute TTL)
// result.VerifyURL -> "https://verify.xident.io/v/xit_abc123"
// resp.RequestID -> correlation ID for support tickets
InitParams Fields
| Field | Type | Required | Description |
|---|---|---|---|
CallbackURL | string | Yes | Webhook URL for verification result |
MinAge | int | No | Age threshold: 12, 15, 18, 21, or 25 |
SuccessURL | string | No | Redirect URL on success |
FailedURL | string | No | Redirect URL on failure |
UserID | string | No | Your internal user ID (passed through to webhook) |
Theme | string | No | "light" or "dark" |
Locale | string | No | Widget language: "en", "de", "fr", etc. |
Metadata | string | No | Opaque string (up to 500 chars) passed to webhook |
Expected | *ExpectedIdentity | No | Identity data you already hold, checked against the document. See Data match. |
MismatchPolicy | string | No | MismatchPolicyReport (default) or MismatchPolicyReview. Only with Expected. |
GetResult (Check Session)
Retrieve the verification result for a token. Call this after the user returns from the verification widget or after receiving a webhook. Never trust URL parameters alone -- always re-verify server-side.
// token is the RESULT token (xtk_...) from the callback -- not the init token.
session, resp, err := client.Verification.GetResult(ctx, "xtk_golden0001")
if err != nil {
log.Fatal(err)
}
fmt.Println("Status:", session.Status) // "success", "failed", "pending", etc.
fmt.Println("Verified:", session.IsVerified()) // true if the user PASSED
fmt.Println("Failed:", session.IsFailed()) // true if failed
fmt.Println("Pending:", session.IsPending()) // true if pending or in_progress
fmt.Println("Terminal:", session.IsTerminal()) // true if no more changes possible
fmt.Println("Age bracket:", session.AgeBracket()) // *int: the age gate (12/15/18/21/25) if the age check passed, else nil
fmt.Println("Method:", session.Method()) // "full" | "age_check" | "xident_id" | "eu_wallet"
Data match
Send the identity data you already hold about the user at init and the result reports, per field, whether the presented document agrees: match, mismatch or not_on_document. Checks.DataMatch is nil when the check was not performed, so gate on dm != nil && dm.Passed. With MismatchPolicyReview any mismatch sends the session to your review queue with reason data_mismatch. Background in Concepts → Data match.
// Send what you already know about the user; the document confirms it.
// The values travel server to server and never reach the browser.
result, _, err := client.Verification.Init(ctx, &xident.InitParams{
CallbackURL: "https://yoursite.com/verify/callback",
Purpose: "id_verification", // a data match needs a document
Expected: &xident.ExpectedIdentity{
FirstName: "Jane",
LastName: "Smith",
DateOfBirth: "1990-05-14", // YYYY-MM-DD
Nationality: "GB", // ISO 3166-1 alpha-2
},
MismatchPolicy: xident.MismatchPolicyReview, // or MismatchPolicyReport (default)
})
// Later, on the result: one verdict per field you asked about.
dm := session.Checks.DataMatch // nil unless the check was performed
if dm != nil && dm.Passed {
// every field matched the document
}
// dm.Fields.DateOfBirth is "match", "mismatch" or "not_on_document"
Webhook Verification
Xident sends webhooks to your registered endpoint when a session passes, fails, or is canceled, and when a tenant review is created or resolved. See Core Concepts → Webhooks for the full event catalog, the envelope shape, and the signature scheme -- summarized here for the Go client. The X-Xident-Signature header uses HMAC-SHA256 with the format t=TIMESTAMP,v1=HMAC_HEX over "{timestamp}.{raw body}".
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", 400)
return
}
signature := r.Header.Get("X-Xident-Signature")
event, err := client.Webhooks.ConstructEvent(body, signature, webhookSecret)
if err != nil {
log.Printf("Webhook verification failed: %v", err)
http.Error(w, "Invalid signature", 400)
return
}
switch event.Type {
case "session.success":
log.Printf("Verification completed: %v", event.Data)
// Grant access
case "session.failed":
log.Printf("Verification failed: %v", event.Data)
// Handle failure
}
w.WriteHeader(http.StatusOK)
}
// Verify signature only (without parsing)
ok, err := client.Webhooks.VerifySignature(payload, signature, secret)
// Custom tolerance (default: 5 minutes)
event, err := client.Webhooks.ConstructEvent(payload, sig, secret, 10*time.Minute)
// Disable replay protection (NOT recommended in production)
event, err := client.Webhooks.ConstructEvent(payload, sig, secret, 0)
WebhookEvent Fields
| Field | Type | Description |
|---|---|---|
Type | string | Event type: "session.success", "session.failed", "session.canceled", "review.created", "review.approved", "review.rejected", "test". ("session.expired" is a subscribable value but not yet emitted by the server; "session.completed" is a deprecated alias still sent to endpoints that subscribed under the old name.) |
Data | map[string]any | For every session.*/review.* event, the fields of the SessionResult your secret key gets from GetResult, as a raw map. For test, just {"message": "..."}. |
ID | string | Unique event identifier, e.g. "evt_a1b2c3d4e5f6" (also the value of X-Xident-Delivery) -- use it for de-duplication. |
Created | int64 | Unix timestamp of event creation |
Blacklist
The blacklist blocks a known bad actor by face. You add someone in one of two ways: from one of your own completed document verification sessions, or from an image you supply. The face embedding is derived server-side -- a raw embedding is never accepted from you and is never returned in any response. Entries carry bookkeeping fields only.
Both add calls are asynchronous. They return Status: "processing" and the entry appears in List once the server has finished deriving the embedding.
When a blacklisted face turns up in a later document verification, that session fails with reason blacklist_match. The same check runs inside face 2FA.
Add from a Session
Blacklisting from a session works for 12 months after that session. The face embedding is retained for 12 months and then permanently deleted.
// Blacklist the person from one of your own completed DOCUMENT verification
// sessions. The server lifts the face from that session -- you never handle
// the image, and a raw embedding is never accepted or returned.
result, _, err := client.Blacklist.AddBySession(ctx, &xident.BlacklistAddSessionParams{
SessionToken: "xtk_abc123",
Reason: "chargeback fraud, order 4471",
})
if err != nil {
var valErr *xident.ValidationError
if errors.As(err, &valErr) {
switch valErr.Code {
case "SESSION_HAS_NO_FACE_DATA":
// Browser-only check (Path A). No document or selfie was ever
// captured, so there is no face to blacklist. Retrying never
// helps -- use AddByImage if you have a picture of the person.
log.Println("session has no face data")
case "SESSION_FACE_DATA_EXPIRED":
// It was a document verification, but its face data has passed
// the 12-month retention window and has been deleted.
log.Println("face data expired -- report bad actors sooner")
default:
log.Printf("blacklist rejected: %s (%s)", valErr.Message, valErr.Code)
}
return
}
log.Fatal(err)
}
// Adding is asynchronous: the entry appears in List once the server has
// derived the embedding.
fmt.Println(result.Status) // "processing"
Session Failures Worth Handling
Both arrive as *xident.ValidationError with HTTP 422. Read .Code to tell them apart.
| Code | HTTP | What it means |
|---|---|---|
SESSION_HAS_NO_FACE_DATA | 422 | The session was a browser-only check (Path A). No document or selfie was ever captured, so there is no face to blacklist. Retrying will never help. |
SESSION_FACE_DATA_EXPIRED | 422 | It was a document verification, but its face data has passed the 12-month retention window and been deleted. |
Add from an Image
import "encoding/base64"
img, err := os.ReadFile("suspect.jpg")
if err != nil {
log.Fatal(err)
}
result, _, err := client.Blacklist.AddByImage(ctx, &xident.BlacklistAddImageParams{
Image: base64.StdEncoding.EncodeToString(img), // max ~10MB of base64
Reason: "banned from all venues",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status) // "processing"
List and Remove
entries, resp, err := client.Blacklist.List(ctx, &xident.BlacklistListOptions{
Page: 1,
PerPage: 50, // max 100
})
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
// No embedding is present -- entries carry bookkeeping fields only.
fmt.Println(e.ID, e.Reason, e.Source, e.CreatedAt)
}
if resp.Pagination != nil {
fmt.Println("total entries:", resp.Pagination.Total)
}
// Un-ban: deactivate an entry by its ID.
if _, _, err := client.Blacklist.Remove(ctx, entries[0].ID); err != nil {
log.Fatal(err)
}
Methods
| Method | Returns | Description |
|---|---|---|
Blacklist.List(ctx, opts) | []BlacklistEntry | Page through active entries. Pass nil for the server defaults (page 1, 20 per page). Pagination lands on resp.Pagination. |
Blacklist.AddBySession(ctx, params) | *BlacklistAddResult | Blacklist the person from one of your terminal document sessions. Async. |
Blacklist.AddByImage(ctx, params) | *BlacklistAddResult | Blacklist the face in a base64 image. Async. |
Blacklist.Remove(ctx, id) | *BlacklistRemoveResult | Deactivate an entry (un-ban). |
BlacklistAddSessionParams / BlacklistAddImageParams
| Field | Type | Required | Description |
|---|---|---|---|
SessionToken | string | Yes (session) | Token of one of your terminal document verification sessions (max 100 chars). A session still in progress is rejected with HTTP 409. |
Image | string | Yes (image) | Base64-encoded face image (max ~10MB of base64, roughly 7.5MB decoded). |
Reason | string | Yes | Why the person is being blacklisted (max 500 chars). |
BlacklistEntry Fields
| Field | Type | Description |
|---|---|---|
ID | int64 | Entry identifier. Pass it to Remove. |
Reason | string | The reason you supplied when adding the entry. |
Source | string | How the entry was created: "session" or "image". |
SessionID | *int64 | Session the face was lifted from, or nil for image entries. |
CreatedAt | string | RFC 3339 timestamp. |
Face 2FA
Face 2FA is a server-side biometric second factor for your own users. Register enrols a user's face; Verify checks a face against that enrolment (a 1:1 comparison). Both are asynchronous: they return a challenge that you poll with GetStatus. The API returns pass/fail only -- never a confidence score, never biometric data.
Billing: face 2FA bills at the Verification rate, because it is a server-side biometric match. You are charged one event per decided verify. Registration is free, and so is an attempt that never reaches a verdict.
Polling a Challenge
// Both Register and Verify return a challenge in "processing" state. Poll
// GetStatus until the status is terminal, then read Passed.
func waitForChallenge(ctx context.Context, client *xident.Client, id string) (*xident.Face2FAChallengeStatus, error) {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
deadline := time.After(30 * time.Second)
for {
status, _, err := client.Face2FA.GetStatus(ctx, id)
if err != nil {
return nil, err
}
if status.Status.IsTerminal() {
return status, nil
}
select {
case <-ticker.C:
case <-deadline:
return nil, errors.New("face 2FA challenge did not finish in time")
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
Register (Enrol a Face)
// Enrol a face for one of your users. UserID is your own opaque identifier
// -- Xident never interprets it. Registration is free of charge.
challenge, _, err := client.Face2FA.Register(ctx, &xident.Face2FAParams{
UserID: "usr_123",
Image: base64Selfie,
})
if err != nil {
log.Fatal(err)
}
status, err := waitForChallenge(ctx, client, challenge.ChallengeID)
if err != nil {
log.Fatal(err)
}
if status.Passed != nil && *status.Passed {
fmt.Println("face enrolled")
}
Verify (Check a Face)
challenge, _, err := client.Face2FA.Verify(ctx, &xident.Face2FAParams{
UserID: "usr_123",
Image: base64Selfie,
})
if err != nil {
log.Fatal(err)
}
status, err := waitForChallenge(ctx, client, challenge.ChallengeID)
if err != nil {
log.Fatal(err)
}
switch {
case status.Passed != nil && *status.Passed:
// Second factor passed -- complete the login.
case status.FailureReason != nil:
switch *status.FailureReason {
case xident.Face2FAFailNotEnrolled:
// No face on file -- send the user through Register first.
case xident.Face2FAFailFaceMismatch:
// Different person, or a poor capture. Let them try again.
case xident.Face2FAFailBlacklistMatch:
// The face is on your blacklist. Do not let them in.
case xident.Face2FAFailNoFaceDetected, xident.Face2FAFailInvalidImage:
// Capture problem -- ask for a new photo.
default:
// Treat the set as open: new reasons may be added.
log.Println("face 2FA failed:", *status.FailureReason)
}
}
Enrolment State and Deletion
// Does this user have a face on file? Use it to decide whether to offer
// face 2FA at login.
enrollment, _, err := client.Face2FA.GetUser(ctx, "usr_123")
if err != nil {
log.Fatal(err)
}
if enrollment.Enrolled {
fmt.Println("enrolled at", *enrollment.EnrolledAt)
}
// GDPR hard delete: the stored face is removed, not flagged. Idempotent --
// it succeeds whether or not an enrollment existed.
if _, _, err := client.Face2FA.DeleteUser(ctx, "usr_123"); err != nil {
log.Fatal(err)
}
Methods
| Method | Returns | Description |
|---|---|---|
Face2FA.Register(ctx, params) | *Face2FAChallenge | Store or replace the user's face enrolment. Async, free of charge. |
Face2FA.Verify(ctx, params) | *Face2FAChallenge | Compare a face against the enrolled one. Async. |
Face2FA.GetStatus(ctx, challengeID) | *Face2FAChallengeStatus | Poll a challenge for the pass/fail outcome. |
Face2FA.GetUser(ctx, userID) | *Face2FAEnrollment | Whether the user has a face enrolled, and when. |
Face2FA.DeleteUser(ctx, userID) | *Face2FADeleteResult | GDPR hard delete of the enrolment. Idempotent. |
Face2FAParams Fields
| Field | Type | Required | Description |
|---|---|---|---|
UserID | string | Yes | Your own user identifier (max 255 chars). Opaque to Xident -- it only has to be stable so a later Verify finds the registered face. |
Image | string | Yes | Base64-encoded face image (max ~10MB of base64, roughly 7.5MB decoded). |
Face2FAChallengeStatus Fields
| Field | Type | Description |
|---|---|---|
ChallengeID | string | The challenge being polled. |
Kind | string | "enroll" or "verify". |
Status | Face2FAStatus | Lifecycle state. Call .IsTerminal() on it. |
Passed | *bool | The verdict: true on completed, false on failed or expired, nil while still processing. |
FailureReason | *Face2FAFailureReason | Why a non-passing challenge did not pass. |
ExpiresAt | string | RFC 3339 timestamp after which the challenge expires. |
CompletedAt | *string | RFC 3339 timestamp of the terminal state, or nil. |
Challenge Statuses
| Status | Constant | Terminal | Meaning |
|---|---|---|---|
processing | Face2FAStatusProcessing | No | Accepted, not finished. Keep polling. |
completed | Face2FAStatusCompleted | Yes | PASSED -- the face was enrolled, or it matched. |
failed | Face2FAStatusFailed | Yes | Did not pass. Read FailureReason. |
expired | Face2FAStatusExpired | Yes | Not processed within its lifetime. Submit a new challenge. |
Failure Reasons
| Reason | Constant | Meaning |
|---|---|---|
invalid_image | Face2FAFailInvalidImage | The image could not be decoded. |
no_face_detected | Face2FAFailNoFaceDetected | No face was found in the image. |
not_enrolled | Face2FAFailNotEnrolled | Verify was called for a user with no enrolment. |
face_mismatch | Face2FAFailFaceMismatch | The face did not match the enrolled one. |
blacklist_match | Face2FAFailBlacklistMatch | The face matched an entry on your blacklist. |
expired | Face2FAFailExpired | The challenge expired before processing finished. |
internal_error | Face2FAFailInternalError | Processing failed on the Xident side. Safe to retry with a new challenge. |
Treat the reason set as open -- new values may be added, so always keep a default branch.
Error Handling
The SDK returns typed errors that map to HTTP status codes. Use errors.As() to match specific error types.
import "errors"
result, _, err := client.Verification.Init(ctx, params)
if err != nil {
// Match specific error types with errors.As
var authErr *xident.AuthenticationError
var valErr *xident.ValidationError
var notFound *xident.NotFoundError
var rateErr *xident.RateLimitError
var srvErr *xident.ServerError
switch {
case errors.As(err, &authErr):
// HTTP 401/403 -- invalid or missing API key
log.Printf("Auth failed: %s (code: %s, request_id: %s)",
authErr.Message, authErr.Code, authErr.RequestID)
case errors.As(err, &valErr):
// HTTP 400 -- bad request parameters
log.Printf("Validation: %s", valErr.Message)
case errors.As(err, ¬Found):
// HTTP 404 -- token or resource not found
log.Printf("Not found: %s", notFound.Message)
case errors.As(err, &rateErr):
// HTTP 429 -- rate limited
log.Printf("Rate limited, retry after %d seconds", rateErr.RetryAfter)
case errors.As(err, &srvErr):
// HTTP 5xx -- server error (SDK auto-retries these)
log.Printf("Server error: %s", srvErr.Message)
default:
// Network error, context canceled, etc.
log.Printf("Error: %v", err)
}
}
Error Hierarchy
| Error Type | HTTP Status | Description |
|---|---|---|
*AuthenticationError | 401, 403 | Invalid or missing API key |
*ValidationError | 400, 4xx | Bad request parameters |
*NotFoundError | 404 | Token or resource not found |
*RateLimitError | 429 | Rate limit exceeded (.RetryAfter seconds) |
*ServerError | 5xx | Server error (auto-retried by SDK) |
All error types embed ErrorResponse which provides .Code, .Message, .RequestID, and .Response (the raw *http.Response).
Framework Examples
Gin
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
xident "github.com/xident-io/go-sdk/v3"
)
func main() {
client := xident.NewClient(os.Getenv("XIDENT_SECRET_KEY"),
xident.WithTimeout(15*time.Second),
)
webhookSecret := os.Getenv("XIDENT_WEBHOOK_SECRET")
r := gin.Default()
// Start verification
r.POST("/verify", func(c *gin.Context) {
result, _, err := client.Verification.Init(c.Request.Context(), &xident.InitParams{
CallbackURL: "https://example.com/webhook",
MinAge: 18,
SuccessURL: "https://example.com/success",
FailedURL: "https://example.com/failed",
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": result.Token,
"verify_url": result.VerifyURL,
})
})
// Webhook handler
r.POST("/webhook", func(c *gin.Context) {
body, _ := io.ReadAll(c.Request.Body)
event, err := client.Webhooks.ConstructEvent(
body, c.GetHeader("X-Xident-Signature"), webhookSecret,
)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid signature"})
return
}
log.Printf("Event: %s %v", event.Type, event.Data)
c.Status(http.StatusOK)
})
// Check verification result
r.GET("/result/:token", func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
defer cancel()
session, _, err := client.Verification.GetResult(ctx, c.Param("token"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"verified": session.IsVerified(),
"age_bracket": session.AgeBracket(),
"method": session.Method(),
})
})
r.Run(":8080")
}
Echo
package main
import (
"context"
"io"
"log"
"net/http"
"os"
"time"
"github.com/labstack/echo/v4"
xident "github.com/xident-io/go-sdk/v3"
)
func main() {
client := xident.NewClient(os.Getenv("XIDENT_SECRET_KEY"))
webhookSecret := os.Getenv("XIDENT_WEBHOOK_SECRET")
e := echo.New()
e.POST("/verify", func(c echo.Context) error {
result, _, err := client.Verification.Init(c.Request().Context(), &xident.InitParams{
CallbackURL: "https://example.com/webhook",
MinAge: 18,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{
"token": result.Token,
"verify_url": result.VerifyURL,
})
})
e.POST("/webhook", func(c echo.Context) error {
body, _ := io.ReadAll(c.Request().Body)
event, err := client.Webhooks.ConstructEvent(
body, c.Request().Header.Get("X-Xident-Signature"), webhookSecret,
)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid signature"})
}
log.Printf("Event: %s", event.Type)
return c.NoContent(http.StatusOK)
})
e.GET("/result/:token", func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 10*time.Second)
defer cancel()
session, _, err := client.Verification.GetResult(ctx, c.Param("token"))
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]any{
"verified": session.IsVerified(),
"age_bracket": session.AgeBracket(),
"method": session.Method(),
})
})
e.Logger.Fatal(e.Start(":8080"))
}
Fiber
package main
import (
"context"
"log"
"os"
"time"
"github.com/gofiber/fiber/v2"
xident "github.com/xident-io/go-sdk/v3"
)
func main() {
client := xident.NewClient(os.Getenv("XIDENT_SECRET_KEY"))
webhookSecret := os.Getenv("XIDENT_WEBHOOK_SECRET")
app := fiber.New()
app.Post("/verify", func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
result, _, err := client.Verification.Init(ctx, &xident.InitParams{
CallbackURL: "https://example.com/webhook",
MinAge: 18,
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"token": result.Token,
"verify_url": result.VerifyURL,
})
})
app.Post("/webhook", func(c *fiber.Ctx) error {
event, err := client.Webhooks.ConstructEvent(
c.Body(), c.Get("X-Xident-Signature"), webhookSecret,
)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid signature"})
}
log.Printf("Event: %s", event.Type)
return c.SendStatus(fiber.StatusOK)
})
app.Get("/result/:token", func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session, _, err := client.Verification.GetResult(ctx, c.Params("token"))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"verified": session.IsVerified(),
"age_bracket": session.AgeBracket(),
"method": session.Method(),
})
})
log.Fatal(app.Listen(":8080"))
}
Response Types
SessionResult
This is the SDK's Go representation of the v1 tenant result -- the same shape returned by GET /verify/v1/result/{token} and delivered byte-identical as every webhook's data. It replaces the pre-2.0.0 blob fields (LivenessResult, AgeResult, OCRResult, FaceMatchResult, CountryCode, MinAge, RequiredMethods, RemainingAttempts), which no longer exist on this type.
| Field | Type | Description |
|---|---|---|
Token | string | The result token (xtk_…) |
Status | SessionStatus | See Core Concepts → Result Status Values for the full vocabulary and meanings ("claimed" is an internal-only value and never appears here — Xident ID account linking is projected to "success") |
Verified | bool | The one field to branch on for access control -- true only when Status == "success", computed server-side |
Reason | string | Why a non-passing session ended the way it did; empty on success |
VerificationMode | string | Which PATH produced the verdict: full (document path — OCR and/or document-to-selfie face match), age_check (browser-only — liveness and/or age bracket, no document), xident_id (returning user reused a bracket on their Xident account) or eu_wallet. Treat the set as open. Not the request-time auto/document/facial override — that steers which methods run; this reports which path it turned out to be. |
ExternalUserID | string | Your user ID (from InitParams.UserID), empty if none was supplied |
Checks | Checks | Per-method performed/passed detail -- see below |
CreatedAt | string | RFC 3339 timestamp |
CompletedAt | string | RFC 3339 timestamp, empty if not yet completed |
ExpiresAt | string | RFC 3339 timestamp, empty if not set |
Checks
| Field | Type | Description |
|---|---|---|
Liveness | {Performed, Passed bool} | Whether a liveness check ran and passed |
Age | {Performed, Passed bool; Gate int} | Gate is the threshold that was checked (e.g. 18), never the subject's actual age |
Document | {Performed, Passed bool; DocumentType, Country string} | Set only when a document was checked |
FaceMatch | {Performed, Passed bool} | Whether the selfie was matched against the document photo |
Helper Methods
| Method | Returns | Description |
|---|---|---|
IsVerified() | bool | True if status is "success" (the user passed) -- same value as the Verified field |
IsFailed() | bool | True if status is "failed" |
IsPending() | bool | True if "pending" or "in_progress" |
IsTerminal() | bool | True if success, failed, or canceled |
AgeBracket() | *int | Checks.Age.Gate when the age check passed, otherwise nil |
Method() | string | Returns VerificationMode ("full" · "age_check" · "xident_id" · "eu_wallet") |
Environment Variables
| Variable | Description |
|---|---|
XIDENT_SECRET_KEY | Your secret API key (sk_live_) |
XIDENT_WEBHOOK_SECRET | Webhook signing secret (whsec_) |
Related
- Node.js SDK -- Server-side Node.js alternative
- All SDKs -- Overview of all Xident SDKs
- API Reference -- Full REST API documentation