> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oathnet.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK

> Official Go client library for OathNet API

## Installation

```bash theme={null}
go get github.com/oathnet/oathnet-go
```

**Requirements:** Go 1.21+

**Source:** [oathnet/oathnet-go](https://github.com/oathnet/oathnet-go)

## Quick Start

```go theme={null}
package main

import (
    "fmt"
    "log"
    "github.com/oathnet/oathnet-go/pkg/oathnet"
)

func main() {
    // Initialize client with API key (required)
    client, err := oathnet.NewClient("your-api-key")
    if err != nil {
        log.Fatal(err)
    }

    // Search breaches
    result, err := client.Search.Breach("user@example.com", nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Found %d results\n", result.Data.ResultsFound)
    for _, record := range result.Data.Results {
        fmt.Printf("%s: %s\n", record.Email, record.Password)
    }
}
```

## Authentication

```go theme={null}
import (
    "time"
    "github.com/oathnet/oathnet-go/pkg/oathnet"
)

// Explicit API key (required)
client, err := oathnet.NewClient("your-api-key")

// With options
client, err := oathnet.NewClient("your-api-key",
    oathnet.WithBaseURL("https://oathnet.org/api"),
    oathnet.WithTimeout(30 * time.Second),
)
```

## Search Service

### Breach Search

```go theme={null}
// Basic search
result, err := client.Search.Breach("user@example.com", nil)
if err != nil {
    log.Fatal(err)
}

// With options
opts := &oathnet.SearchOptions{
    DBNames: "linkedin_2012,adobe_2013",
}
result, err := client.Search.Breach("user@example.com", opts)

// Access results
fmt.Printf("Total: %d\n", result.Data.ResultsFound)
fmt.Printf("Shown: %d\n", result.Data.ResultsShown)

for _, record := range result.Data.Results {
    fmt.Printf("Email: %s\n", record.Email)
    fmt.Printf("Password: %s\n", record.Password)
    fmt.Printf("Database: %s\n", record.DBName)
}

// Pagination
if result.Data.Cursor != "" {
    nextOpts := &oathnet.SearchOptions{
        Cursor: result.Data.Cursor,
    }
    nextPage, _ := client.Search.Breach("user@example.com", nextOpts)
}
```

### Initialize Session

Search sessions group related lookups for the same query. Set `SearchType`
when you already know what the query is; otherwise OathNet will detect it.
Reuse the returned `Session.ID` as `SearchID` on follow-up searches.

```go theme={null}
// Create a search session for quota optimization.
result, err := client.Search.InitSession("user@example.com", &oathnet.SearchSessionOptions{
    SearchType: "email",
})
if err != nil {
    log.Fatal(err)
}

sessionID := result.Data.Session.ID
fmt.Printf("Session ID: %s\n", sessionID)
fmt.Printf("Status: %s\n", result.Data.Session.Status)
fmt.Printf("Search Type: %s\n", result.Data.Session.SearchType)
fmt.Printf("Expires: %s\n", result.Data.Session.ExpiresAt)

if result.Data.Summary != nil {
    fmt.Printf("Available services: %d\n", result.Data.Summary.AvailableServices)
}

// Reuse the session ID on current V2 search calls.
breachResults, err := client.Breach.Search("user@example.com", &oathnet.BreachV2SearchOptions{
    SearchID: sessionID,
    PageSize: 25,
})
if err != nil {
    log.Fatal(err)
}
if breachResults.Data != nil {
    fmt.Printf("Breach items: %d\n", len(breachResults.Data.Items))
}
```

## AI Filters

Translate natural-language prompts into reusable V2 structured filters.

```go theme={null}
aiFilter, err := client.AI.Create(oathnet.V2AIFilterRequest{
    Index: oathnet.AIFilterIndexBreach,
    Query: "US gmail users with LinkedIn records after 2020",
})
if err != nil {
    log.Fatal(err)
}

fmt.Println(aiFilter.FilterID)
fmt.Printf("%+v\n", aiFilter.Filter)

context, err := client.AI.GetContext(aiFilter.FilterID)
if err != nil {
    log.Fatal(err)
}

fmt.Println(context.IndexType)
fmt.Println(context.Query)
```

Use `aiFilter.FilterID` with V2 search, export, bulk-search, or scanner
`QueryConfig` flows when you want OathNet to reuse the generated filter
context. Set `FilterID` on `V2AIFilterRequest` to refine an existing context.

## V2 Breach Search

Use V2 breach search when you need fielded filters, structured filters, or
autocomplete-driven workflows. GET is best for simple filters that fit in query
parameters. POST is best when the filter tree comes from AI filters, saved UI
state, or a complex manual builder.

```go theme={null}
// Simple pivots stay readable and shareable
result, err := client.Breach.Search("user@example.com", &oathnet.BreachV2SearchOptions{
    DBNames:  []string{"linkedin_2012"},
    Fields:   []string{"email", "password", "dbname"},
    PageSize: 25,
})

// The same method accepts structured filters
filtered, err := client.Breach.Search("", &oathnet.BreachV2SearchOptions{
    Filter: map[string]interface{}{
        "and": []map[string]interface{}{
            {"field": "email_domain", "operator": "eq", "value": "example.com"},
            {"field": "country", "operator": "eq", "value": "US"},
        },
    },
    Fields: []string{"email", "username", "dbname"},
})

// Use autocomplete before building a narrow query or scanner QueryConfig
values, err := client.Breach.Autocomplete(&oathnet.BreachV2AutocompleteOptions{
    Field:       "email_domain",
    Query:       "example",
    Limit:       10,
    IncludeInfo: true,
})
dbnames, err := client.Breach.AutocompleteDBNames(&oathnet.BreachV2AutocompleteDBNamesOptions{
    Query: "link",
    Limit: 10,
})
fieldCoverage, err := client.Breach.AutocompleteFields(&oathnet.BreachV2AutocompleteFieldsOptions{
    Field: "discord_id",
    Limit: 10,
})
```

## V2 Stealer Search

```go theme={null}
// Basic search
result, err := client.Stealer.Search("user@example.com", nil)

// Advanced filtering
opts := &oathnet.StealerSearchOptions{
    Domains:   []string{"google.com", "facebook.com"},
    HasLogID:  true,
    PageSize:  50,
}
result, err := client.Stealer.Search("user@example.com", opts)

// Access items
for _, item := range result.Data.Items {
    fmt.Printf("URL: %s\n", item.URL)
    fmt.Printf("Username: %s\n", item.Username)
    fmt.Printf("Password: %s\n", item.Password)
    fmt.Printf("Log ID: %s\n", item.LogID)
}

// Pagination
if result.Data.NextCursor != "" {
    nextOpts := &oathnet.StealerSearchOptions{
        Cursor: result.Data.NextCursor,
    }
    nextPage, _ := client.Stealer.Search("user@example.com", nextOpts)
}

// Subdomain extraction with optional live checks and session reuse
alive := true
subResult, err := client.Stealer.Subdomain("example.com", "", &oathnet.SubdomainOptions{
    Query: "mail",
    Alive: &alive,
    SearchID: "sess_0123456789abcdef",
})
fmt.Printf("Found %d subdomains\n", subResult.Data.Count)
for _, sub := range subResult.Data.Subdomains {
    fmt.Printf("  %v\n", sub)
}
```

## Investigation And Phonebook

Use investigation when one query should fan out across credentials, victims,
files, properties, and related credentials. Use phonebook when you want domain
host and email intelligence before deciding which pivots to run next.

```go theme={null}
// Structured filters use the same search method
filtered, err := client.Stealer.Search("", &oathnet.StealerSearchOptions{
    Filter: map[string]interface{}{
        "field": "domain", "operator": "eq", "value": "example.com",
    },
    Fields: []string{"log_id", "url_str", "username"},
    View:   "enriched",
})

investigation, err := client.Stealer.InvestigationSearch("example.com", &oathnet.InvestigationSearchOptions{
    Scope:    "all",
    Include:  []string{"credentials", "victims", "files"},
    PageSize: 25,
    View:     "enriched",
})

investigationFromBody, err := client.Stealer.InvestigationSearch("example.com", &oathnet.InvestigationSearchOptions{
    Scope:   "all",
    Include: []string{"credentials"},
    Filters: oathnet.V2InvestigationSectionFilters{
        "credentials": map[string]interface{}{"domain": "example.com"},
    },
})

if investigation.Data != nil &&
    investigation.Data.Sections != nil &&
    investigation.Data.Sections.Credentials != nil {
    for _, item := range investigation.Data.Sections.Credentials.Items {
        fmt.Println(item.LogID, item.Username)
    }
}

phonebook, err := client.Stealer.Phonebook("example.com", &oathnet.PhonebookOptions{
    Alive: true,
})
fmt.Println(len(phonebook.Data.Subdomains))
```

## Victims

Victim search returns stealer-log profiles. Use the `LogID` from a selected
victim to inspect its file tree, fetch one raw file, or download the whole
archive. Passing the `SearchID` from `InitSession` keeps these follow-up
requests tied to the same search workflow.

```go theme={null}
// Search victims
result, err := client.Victims.Search("user@example.com", nil)

for _, victim := range result.Data.Items {
    fmt.Printf("Log ID: %s\n", victim.LogID)
    fmt.Printf("Users: %v\n", victim.DeviceUsers)
    fmt.Printf("Documents: %d\n", victim.TotalDocs)
}

// Use the same method when the pivot comes from an AI/manual structured filter
victims, err := client.Victims.Search("", &oathnet.VictimsSearchOptions{
    Filter: map[string]interface{}{
        "field": "service", "operator": "eq", "value": "discord",
    },
    View: "enriched",
})

// Search victim properties globally, then inspect one selected log
excludeCookieEvidence := true
properties, err := client.Victims.SearchVictimProperties(&oathnet.VictimPropertiesSearchOptions{
    Query:                 "example.com",
    Service:               "discord",
    Confidence:            []string{"high"},
    ExcludeCookieEvidence: &excludeCookieEvidence,
})
propertiesFromBody, err := client.Victims.SearchVictimProperties(&oathnet.VictimPropertiesSearchOptions{
    Query:      "example.com",
    Service:    "discord",
    Confidence: []string{"high", "medium"},
})
fmt.Println(len(properties.Data.Items), len(propertiesFromBody.Data.Items))

details, err := client.Victims.GetProperties("log_id_here", &oathnet.VictimPropertiesSearchOptions{
    Service: "discord",
})
fmt.Println(len(details.Data.Items))

summary, err := client.Victims.GetSummary("log_id_here", nil)
cookies, err := client.Victims.GetCookies("log_id_here", &oathnet.VictimCookieInventoryOptions{
    Domain: "example.com",
    Status: "active",
})

// Raw cookie-domain inspection returns copyable text
cookieText, err := client.Victims.InspectCookieDomain("log_id_here", &oathnet.VictimCookieDomainOptions{
    Domain: "example.com",
})
fmt.Printf("Cookie export characters: %d\n", len(cookieText.CopyText))

// Get the manifest (file tree) for the selected log
searchID := "sess_0123456789abcdef"
logID := "log_id_here"
manifest, err := client.Victims.GetManifest(logID, &oathnet.VictimRawOptions{
    SearchID: searchID,
})
fmt.Printf("Root: %s\n", manifest.VictimTree.Name)

for _, child := range manifest.VictimTree.Children {
    fmt.Printf("  %s (%s)\n", child.Name, child.Type)
}

// Get one raw file from the manifest as bytes
file, err := client.Victims.GetFile(
    logID,
    "Browser Passwords/file 1.txt",
    &oathnet.VictimRawOptions{SearchID: searchID},
)
fmt.Printf("Downloaded file bytes: %d\n", len(file))

// Download the full victim archive as ZIP bytes
archive, err := client.Victims.DownloadArchiveBytes(logID, &oathnet.VictimRawOptions{
    SearchID: searchID,
})
if err == nil {
    os.WriteFile("./victim_archive.zip", archive, 0644)
}
```

## File Search

File search runs an asynchronous scan across selected stealer-log files. Use
metadata search first when you only need file names and IDs. Create a file-search
job when you need to scan file contents with a literal, regex, or wildcard
expression.

```go theme={null}
// Search victim file metadata before fetching any file bytes
files, err := client.Victims.SearchFilesMetadata(&oathnet.FileMetadataSearchOptions{
    Query:    "cookies",
    Kind:     "cookies",
    PageSize: 25,
})
for _, file := range files.Data.Items {
    fmt.Println(file.LogID, file.FileID, file.Path)
}

// Create a file search job
result, err := client.FileSearch.Create("password", &oathnet.FileSearchCreateOptions{
    LogIDs:     []string{"log_id_1", "log_id_2"},
    SearchMode: "literal",  // literal, regex, or wildcard
})
jobID := result.Data.JobID
fmt.Printf("Job ID: %s\n", jobID)

// Check job status
status, err := client.FileSearch.GetStatus(jobID)
fmt.Printf("Status: %s\n", status.Data.Status)
if status.Data.Summary != nil {
    fmt.Printf("Files scanned: %d\n", status.Data.Summary.FilesScanned)
    fmt.Printf("Matches: %d\n", status.Data.Summary.Matches)
}

// Wait for completion and get results
searchResult, err := client.FileSearch.Search("password", &oathnet.FileSearchCreateOptions{
    LogIDs:     []string{"log_id_1", "log_id_2"},
    SearchMode: "literal",
}, 300*time.Second)

for _, match := range searchResult.Data.Matches {
    fmt.Printf("File: %s\n", match.FileName)
    fmt.Printf("Log ID: %s\n", match.LogID)
    fmt.Printf("Match: %s\n", match.MatchText)
}
```

## Exports

Exports are asynchronous jobs for large result sets. Create a job with the
service and structured `QueryConfig` you want, poll the job status, then
download the completed file. Use `docs` for credential-style stealer exports,
`victims` for victim/profile exports, and `breach` for breach-record exports.

```go theme={null}
// List recent export jobs before creating a duplicate
exports, err := client.Exports.ListExportsV2(1, 20)
for _, job := range exports.Results {
    fmt.Println(job.JobID, job.Status)
}

// Create an export job from a structured breach query
result, err := client.Exports.CreateExportV2("breach", &oathnet.ExportCreateOptions{
    Service: "breach",
    Limit:   100,
    Format:  "jsonl", // json, jsonl, csv, txt, or html
    Fields:  []string{"email", "password", "dbname"},
    QueryConfig: map[string]interface{}{
        "filter": map[string]interface{}{
            "field":    "email_domain",
            "operator": "eq",
            "value":    "example.com",
        },
    },
})
jobID := result.Data.JobID
fmt.Printf("Export Job ID: %s\n", jobID)

// Check export status
status, err := client.Exports.GetExportV2(jobID)
fmt.Printf("Status: %s\n", status.Data.Status)
if status.Data.Progress != nil {
    fmt.Printf("Progress: %.1f%%\n", status.Data.Progress.Percent)
}

// Wait for completion
completed, err := client.Exports.WaitForCompletion(jobID, 2*time.Second, 600*time.Second)
if completed.Data.Status == "completed" {
    // Download the export file
    exportBytes, err := client.Exports.DownloadExportV2(jobID)
    if err == nil {
        os.WriteFile("./export.jsonl", exportBytes, 0644)
    }
}
```

## Bulk Search

Bulk search is for many input terms or a saved structured filter that should run
as one background job. Create the job, poll it, and download the result file
when it completes.

```go theme={null}
job, err := client.Bulk.Create(
    []string{"alice@example.com", "bob@example.com"},
    "breach",
    &oathnet.BulkCreateOptions{
        Format: "jsonl",
        QueryConfig: oathnet.BulkSearchQueryConfig{
            "filter": map[string]interface{}{
                "field": "email_domain", "operator": "eq", "value": "example.com",
            },
        },
        Fields: []string{"email", "password", "dbname"},
    },
)
jobID := job.Data.JobID

status, err := client.Bulk.GetStatus(jobID)

jobs, err := client.Bulk.List(1, 20)
for _, item := range jobs.Results {
    fmt.Println(item.JobID, item.Status)
}

completed, err := client.Bulk.WaitForCompletion(jobID, 5*time.Second, 10*time.Minute)
if completed.Data.Status == "completed" {
    err = client.Bulk.Download(jobID, "./bulk-results.jsonl")
}
```

## Scanners

Scanners monitor newly indexed breach or stealer data and send notifications.
Use regular search endpoints for historical investigation; scanners only watch
new data after their baseline.

```go theme={null}
// Check scanner quota before creating a new monitor
quota, err := client.Scanners.GetQuota()
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Remaining scanners: %d\n", quota.Remaining)

queryConfig := oathnet.ScannerQueryConfig{
    "filter": map[string]interface{}{
        "and": []map[string]interface{}{
            {"field": "email_domain", "operator": "eq", "value": "example.com"},
            {"field": "dbname", "operator": "in", "value": []string{"twitter.com", "linkedin.com"}},
        },
    },
}

// Validate webhook or Discord delivery before saving the scanner.
// A 200 response can still have Success=false when the remote endpoint rejects
// the test delivery, so inspect the returned object.
test, err := client.Scanners.TestDraftDelivery(oathnet.ScannerDraftTestRequest{
    ScannerType:         "breach",
    QueryConfig:         queryConfig,
    NotificationType:    "webhook",
    WebhookURL:          "https://alerts.example.com/oathnet",
    WebhookSecurityMode: "signed_json",
})
if err != nil {
    log.Fatal(err)
}
if !test.Success {
    fmt.Printf("Delivery test failed: %s\n", test.Message)
}

notifyOnZero := false
scanner, err := client.Scanners.Create(oathnet.ScannerCreateRequest{
    Name:                "Example breach monitor",
    ScannerType:         "breach",
    QueryConfig:         queryConfig,
    NotificationType:    "webhook",
    WebhookURL:          "https://alerts.example.com/oathnet",
    WebhookSecurityMode: "signed_json",
    NotifyOnZeroResults: &notifyOnZero,
})
if err != nil {
    log.Fatal(err)
}

security, err := client.Scanners.GetWebhookSecurity(scanner.UID)
if err != nil {
    log.Fatal(err)
}
if security.Data != nil {
    fmt.Println(security.Data.VerificationMethod)
}

scanner, err = client.Scanners.Update(scanner.UID, oathnet.ScannerUpdateRequest{
    Name: "Example breach monitor v2",
})
if err != nil {
    log.Fatal(err)
}

_, err = client.Scanners.Trigger(scanner.UID)
if err != nil {
    log.Fatal(err)
}

runs, err := client.Scanners.ListRuns(scanner.UID, &oathnet.ScannerRunsOptions{
    Limit: 10,
})
if err != nil {
    log.Fatal(err)
}
for _, run := range runs {
    fmt.Printf("%s: %s (%d results)\n", run.UID, run.Status, run.ResultsCount)
}

_, _ = client.Scanners.Pause(scanner.UID)
_, _ = client.Scanners.Resume(scanner.UID)
```

Use `client.Scanners.Delete(scannerUID)` when you want to permanently remove a
scanner and its run history.

Webhook receivers should verify `signed_json` or `signed_encrypted` deliveries
against the exact raw request body before parsing JSON. See [Scanners](/guides/scanners#verifying-webhook-signatures)
for the HMAC and encryption contract.

## OSINT Lookups

```go theme={null}
// IP Info
ipInfo, err := client.OSINT.IPInfo("8.8.8.8")
fmt.Printf("Location: %s, %s\n", ipInfo.Data.City, ipInfo.Data.Country)
fmt.Printf("ISP: %s\n", ipInfo.Data.ISP)
fmt.Printf("Proxy: %v\n", ipInfo.Data.Proxy)

// Steam
steam, err := client.OSINT.Steam("76561198012345678")
fmt.Printf("Username: %s\n", steam.Data.Username)
fmt.Printf("Avatar: %s\n", steam.Data.Avatar)

// Xbox
xbox, err := client.OSINT.Xbox("GamerTag123")
fmt.Printf("Username: %s\n", xbox.Data.Username)
fmt.Printf("Avatar: %s\n", xbox.Data.Avatar)

// Discord User Info
discord, err := client.OSINT.DiscordUserinfo("123456789012345678")
fmt.Printf("Username: %s\n", discord.Data.Username)
fmt.Printf("Global Name: %s\n", discord.Data.GlobalName)
fmt.Printf("Created: %s\n", discord.Data.CreationDate)

// Discord Username History
history, err := client.OSINT.DiscordUsernameHistory("123456789012345678")
for _, entry := range history.Data.History {
    if len(entry.Name) > 0 && len(entry.Time) > 0 {
        fmt.Printf("%s at %s\n", entry.Name[0], entry.Time[0])
    }
}

// Roblox User Info
roblox, err := client.OSINT.RobloxUserinfo(oathnet.RobloxUserinfoOptions{
    UserID: "123456789",
})
// Or by username:
roblox2, _ := client.OSINT.RobloxUserinfo(oathnet.RobloxUserinfoOptions{
    Username: "PlayerName",
})
fmt.Printf("Username: %s\n", roblox.Data.Username)
fmt.Printf("Display Name: %s\n", roblox.Data.DisplayName)

// Holehe - Email account detection
holehe, err := client.OSINT.Holehe("user@example.com")
fmt.Printf("Found on %d services:\n", len(holehe.Data.Domains))
for _, domain := range holehe.Data.Domains {
    fmt.Printf("  %s\n", domain)
}

// GHunt - Google account lookup
ghunt, err := client.OSINT.GHunt("user@gmail.com")
if ghunt.Data != nil && ghunt.Data.Data != nil && ghunt.Data.Data.Profile != nil {
    fmt.Printf("Name: %s\n", ghunt.Data.Data.Profile.Name)
}

// Subdomain extraction
alive := true
subdomains, err := client.OSINT.ExtractSubdomain("example.com", &alive)
for _, sub := range subdomains.Data.Subdomains {
    fmt.Println(sub)
}

// Minecraft username history
mc, err := client.OSINT.MinecraftHistory("PlayerName")
for _, entry := range mc.Data.History {
    fmt.Printf("%s - %s\n", entry.Username, entry.ChangedAt)
}
```

## Utility Service

```go theme={null}
// Database name autocomplete
dbnames, err := client.Utility.DBNameAutocomplete("link")
for _, name := range dbnames {
    fmt.Println(name)  // linkedin_2012, linkedin_2021, etc.
}
```

## Error Handling

```go theme={null}
import "github.com/oathnet/oathnet-go/pkg/oathnet"

result, err := client.Search.Breach("user@example.com", nil)
if err != nil {
    switch e := err.(type) {
    case *oathnet.AuthenticationError:
        fmt.Println("Invalid API key")
    case *oathnet.QuotaExceededError:
        fmt.Println("Daily quota exceeded")
    case *oathnet.RateLimitError:
        fmt.Printf("Rate limited. Retry after %d seconds\n", e.RetryAfter)
    case *oathnet.NotFoundError:
        fmt.Println("Resource not found")
    case *oathnet.ValidationError:
        fmt.Printf("Invalid input: %s\n", e.Message)
    case *oathnet.OathNetError:
        fmt.Printf("API error: %s\n", e.Message)
    default:
        fmt.Printf("Unknown error: %v\n", err)
    }
    return
}
```

## Timeout Configuration

```go theme={null}
client, err := oathnet.NewClient("your-api-key",
    oathnet.WithTimeout(10 * time.Second),
)
result, err := client.Search.Breach("user@example.com", nil)
```

## Configuration Options

```go theme={null}
import (
    "time"
    "github.com/oathnet/oathnet-go/pkg/oathnet"
)

client, err := oathnet.NewClient("your-api-key",
    oathnet.WithBaseURL("https://oathnet.org/api"),
    oathnet.WithTimeout(60 * time.Second),
)
```

<CardGroup cols={2}>
  <Card title="CLI Tool" icon="terminal" href="/sdks/cli">
    Next: CLI documentation
  </Card>

  <Card title="JavaScript SDK" icon="js" href="/sdks/javascript">
    Previous: JavaScript SDK
  </Card>
</CardGroup>
