Go SDK

Available v2.0.0 Go 1.21+ Zero Dependencies

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
CallbackURLstringYesWebhook URL for verification result
MinAgeintNoAge threshold: 12, 15, 18, 21, or 25
SuccessURLstringNoRedirect URL on success
FailedURLstringNoRedirect URL on failure
UserIDstringNoYour internal user ID (passed through to webhook)
ThemestringNo"light" or "dark"
LocalestringNoWidget language: "en", "de", "fr", etc.
MetadatastringNoOpaque string (up to 500 chars) passed to webhook
Expected*ExpectedIdentityNoIdentity data you already hold, checked against the document. See Data match.
MismatchPolicystringNoMismatchPolicyReport (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
TypestringEvent 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.)
Datamap[string]anyFor every session.*/review.* event, the fields of the SessionResult your secret key gets from GetResult, as a raw map. For test, just {"message": "..."}.
IDstringUnique event identifier, e.g. "evt_a1b2c3d4e5f6" (also the value of X-Xident-Delivery) -- use it for de-duplication.
Createdint64Unix 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_DATA422The 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_EXPIRED422It 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)[]BlacklistEntryPage through active entries. Pass nil for the server defaults (page 1, 20 per page). Pagination lands on resp.Pagination.
Blacklist.AddBySession(ctx, params)*BlacklistAddResultBlacklist the person from one of your terminal document sessions. Async.
Blacklist.AddByImage(ctx, params)*BlacklistAddResultBlacklist the face in a base64 image. Async.
Blacklist.Remove(ctx, id)*BlacklistRemoveResultDeactivate an entry (un-ban).

BlacklistAddSessionParams / BlacklistAddImageParams

Field Type Required Description
SessionTokenstringYes (session)Token of one of your terminal document verification sessions (max 100 chars). A session still in progress is rejected with HTTP 409.
ImagestringYes (image)Base64-encoded face image (max ~10MB of base64, roughly 7.5MB decoded).
ReasonstringYesWhy the person is being blacklisted (max 500 chars).

BlacklistEntry Fields

Field Type Description
IDint64Entry identifier. Pass it to Remove.
ReasonstringThe reason you supplied when adding the entry.
SourcestringHow the entry was created: "session" or "image".
SessionID*int64Session the face was lifted from, or nil for image entries.
CreatedAtstringRFC 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)*Face2FAChallengeStore or replace the user's face enrolment. Async, free of charge.
Face2FA.Verify(ctx, params)*Face2FAChallengeCompare a face against the enrolled one. Async.
Face2FA.GetStatus(ctx, challengeID)*Face2FAChallengeStatusPoll a challenge for the pass/fail outcome.
Face2FA.GetUser(ctx, userID)*Face2FAEnrollmentWhether the user has a face enrolled, and when.
Face2FA.DeleteUser(ctx, userID)*Face2FADeleteResultGDPR hard delete of the enrolment. Idempotent.

Face2FAParams Fields

Field Type Required Description
UserIDstringYesYour own user identifier (max 255 chars). Opaque to Xident -- it only has to be stable so a later Verify finds the registered face.
ImagestringYesBase64-encoded face image (max ~10MB of base64, roughly 7.5MB decoded).

Face2FAChallengeStatus Fields

Field Type Description
ChallengeIDstringThe challenge being polled.
Kindstring"enroll" or "verify".
StatusFace2FAStatusLifecycle state. Call .IsTerminal() on it.
Passed*boolThe verdict: true on completed, false on failed or expired, nil while still processing.
FailureReason*Face2FAFailureReasonWhy a non-passing challenge did not pass.
ExpiresAtstringRFC 3339 timestamp after which the challenge expires.
CompletedAt*stringRFC 3339 timestamp of the terminal state, or nil.

Challenge Statuses

Status Constant Terminal Meaning
processingFace2FAStatusProcessingNoAccepted, not finished. Keep polling.
completedFace2FAStatusCompletedYesPASSED -- the face was enrolled, or it matched.
failedFace2FAStatusFailedYesDid not pass. Read FailureReason.
expiredFace2FAStatusExpiredYesNot processed within its lifetime. Submit a new challenge.

Failure Reasons

Reason Constant Meaning
invalid_imageFace2FAFailInvalidImageThe image could not be decoded.
no_face_detectedFace2FAFailNoFaceDetectedNo face was found in the image.
not_enrolledFace2FAFailNotEnrolledVerify was called for a user with no enrolment.
face_mismatchFace2FAFailFaceMismatchThe face did not match the enrolled one.
blacklist_matchFace2FAFailBlacklistMatchThe face matched an entry on your blacklist.
expiredFace2FAFailExpiredThe challenge expired before processing finished.
internal_errorFace2FAFailInternalErrorProcessing 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, &notFound):
        // 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
*AuthenticationError401, 403Invalid or missing API key
*ValidationError400, 4xxBad request parameters
*NotFoundError404Token or resource not found
*RateLimitError429Rate limit exceeded (.RetryAfter seconds)
*ServerError5xxServer 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
TokenstringThe result token (xtk_…)
StatusSessionStatusSee 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")
VerifiedboolThe one field to branch on for access control -- true only when Status == "success", computed server-side
ReasonstringWhy a non-passing session ended the way it did; empty on success
VerificationModestringWhich 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.
ExternalUserIDstringYour user ID (from InitParams.UserID), empty if none was supplied
ChecksChecksPer-method performed/passed detail -- see below
CreatedAtstringRFC 3339 timestamp
CompletedAtstringRFC 3339 timestamp, empty if not yet completed
ExpiresAtstringRFC 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()boolTrue if status is "success" (the user passed) -- same value as the Verified field
IsFailed()boolTrue if status is "failed"
IsPending()boolTrue if "pending" or "in_progress"
IsTerminal()boolTrue if success, failed, or canceled
AgeBracket()*intChecks.Age.Gate when the age check passed, otherwise nil
Method()stringReturns VerificationMode ("full" · "age_check" · "xident_id" · "eu_wallet")

Environment Variables

Variable Description
XIDENT_SECRET_KEYYour secret API key (sk_live_)
XIDENT_WEBHOOK_SECRETWebhook signing secret (whsec_)

Related