Go SDK

Available v1.0.0 Go 1.21+ Zero Dependencies

The Go SDK (github.com/xident-io/go-sdk) 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_ / sk_test_) 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/xident-go

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    xident "github.com/xident-io/go-sdk"
)

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())
    }
}

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/1.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
    LivenessDifficulty: "hard",                // "easy", "normal", "hard"
    Purpose:            "age_verification",
})
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
LivenessDifficultystringNo"easy", "normal", or "hard"
PurposestringNoVerification purpose (shown to user)

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.

session, resp, err := client.Verification.GetResult(ctx, "xit_abc123")
if err != nil {
    log.Fatal(err)
}

fmt.Println("Status:", session.Status)           // "completed", "failed", "pending", etc.
fmt.Println("Verified:", session.IsVerified())    // true if completed
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: 12, 15, 18, 21, 25, or nil
fmt.Println("Method:", session.Method())           // "ml_fast", "ocr", "self_declaration"

Webhook Verification

Xident sends webhooks to your CallbackURL when verification sessions complete, fail, or expire. The X-Xident-Signature header uses HMAC-SHA256 with the format t=TIMESTAMP,v1=HMAC_HEX (Stripe-style).

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.completed":
        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.completed", "session.failed"
Datamap[string]anyEvent payload (session details)
IDstringUnique event identifier (for deduplication)
Createdint64Unix timestamp of event creation

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"
)

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"
)

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"
)

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

Field Type Description
IDstringUnique session identifier
StatusSessionStatus"pending", "in_progress", "completed", "failed", "canceled", "claimed"
LivenessResultjson.RawMessageLiveness check outcome
AgeResultjson.RawMessageAge verification outcome
OCRResultjson.RawMessageDocument OCR outcome
FaceMatchResultjson.RawMessageFace matching outcome
CountryCode*stringISO 3166-1 alpha-2 country code
MinAge*intMinimum age threshold for this session
ExternalUserID*stringYour user ID (from InitParams.UserID)
RequiredMethods[]stringVerification methods required
RemainingAttempts*intAttempts remaining before failure
CreatedAtstringRFC 3339 timestamp
CompletedAt*stringRFC 3339 timestamp (nil if in progress)
ExpiresAt*stringRFC 3339 timestamp

Helper Methods

Method Returns Description
IsVerified()boolTrue if status is "completed"
IsFailed()boolTrue if status is "failed"
IsPending()boolTrue if "pending" or "in_progress"
IsTerminal()boolTrue if completed, failed, canceled, or claimed
AgeBracket()*intVerified age bracket (12/15/18/21/25) or nil
Method()stringVerification method used or ""

Environment Variables

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

Related