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

# Go SDK

> Official Go SDK for the Truthlocks platform. Idiomatic Go with full context propagation, typed request/response structs, automatic retries, and comprehensive error handling.

<Info>
  **Public module:** The Go SDK is available as a public Go module. No special
  configuration required.
</Info>

## Installation

```bash theme={null}
go get github.com/truthlocks/sdk-go@latest
```

## Quick Start

```go main.go theme={null}
package main

import (
    "context"
    "fmt"
    "log"

    truthlock "github.com/truthlocks/sdk-go"
)

func main() {
    // 1. Create client
    client := truthlock.NewClient(truthlock.Config{
        BaseURL:  "https://api.truthlocks.com",
        TenantID: "your-tenant-id",
        APIKey:   "tl_live_...",
    })

    ctx := context.Background()

    // 2. Create and trust an issuer
    issuer, err := client.Issuers.Create(ctx, &truthlock.CreateIssuerRequest{
        Name:        "My Organization",
        LegalName:   "My Organization Inc.",
        DisplayName: "My Org",
    })
    if err != nil {
        log.Fatal(err)
    }

    if _, err := client.Issuers.Trust(ctx, issuer.ID); err != nil {
        log.Fatal(err)
    }

    // 3. Register a signing key
    _, err = client.Keys.Register(ctx, issuer.ID, &truthlock.RegisterKeyRequest{
        KID:          "key-2026",
        Alg:          truthlock.AlgEd25519,
        PublicKeyB64: "your-public-key-base64url",
    })
    if err != nil {
        log.Fatal(err)
    }

    // 4. Mint an attestation
    attestation, err := client.Attestations.Mint(ctx, &truthlock.MintRequest{
        IssuerID: issuer.ID,
        KID:      "key-2026",
        Alg:      truthlock.AlgEd25519,
        Schema:   "degree",
        Claims: map[string]interface{}{
            "student_name":    "Jane Doe",
            "degree_type":     "Bachelor of Science",
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Attestation ID: %s\n", attestation.ID)

    // 5. Verify
    result, err := client.Verify.VerifyOnline(ctx, &truthlock.VerifyRequest{
        AttestationID: attestation.ID,
    })
    if err != nil {
        log.Fatal(err)
    }

    if result.Verdict == truthlock.VerdictValid {
        fmt.Println("Attestation verified successfully")
    }
}
```

## Revoke an attestation

Permanently invalidate an attestation. Once revoked, any verification check returns `REVOKED`. This action cannot be undone — if you need to issue an updated credential instead, use [supersede](#supersede-an-attestation).

```go revoke.go theme={null}
package main

import (
    "context"
    "fmt"
    "log"

    truthlock "github.com/truthlocks/sdk-go"
)

func main() {
    client := truthlock.NewClient(truthlock.Config{
        BaseURL:  "https://api.truthlocks.com",
        TenantID: "your-tenant-id",
        APIKey:   "tl_live_...",
    })

    ctx := context.Background()

    revoked, err := client.Attestations.Revoke(ctx,
        "660e8400-e29b-41d4-a716-446655440001",
        &truthlock.RevokeRequest{
            Reason: "Certificate holder no longer employed",
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Status: %s\n", revoked.Status)     // "REVOKED"
    fmt.Printf("Revoked at: %s\n", revoked.RevokedAt)

    // Subsequent verifications reflect the revocation
    result, err := client.Verify.VerifyOnline(ctx, &truthlock.VerifyRequest{
        AttestationID: "660e8400-e29b-41d4-a716-446655440001",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Verdict: %s\n", result.Verdict) // "REVOKED"
}
```

<Warning>
  Revocation is permanent and recorded in the transparency log. You cannot undo it. Use **supersede** if you need to replace a credential with an updated version.
</Warning>

## Supersede an attestation

Replace an existing attestation with an updated version. The original is marked as `SUPERSEDED` and linked to the new one, creating an auditable version chain.

```go supersede.go theme={null}
package main

import (
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"

    truthlock "github.com/truthlocks/sdk-go"
)

func main() {
    client := truthlock.NewClient(truthlock.Config{
        BaseURL:  "https://api.truthlocks.com",
        TenantID: "your-tenant-id",
        APIKey:   "tl_live_...",
    })

    ctx := context.Background()

    // Supersede an attestation with updated claims
    updatedClaims, _ := json.Marshal(map[string]string{
        "student_name": "Jane Doe",
        "degree_type":  "Master of Science", // Updated from Bachelor to Master
    })
    payloadB64 := base64.RawURLEncoding.EncodeToString(updatedClaims)

    result, err := client.Attestations.Supersede(ctx,
        "660e8400-e29b-41d4-a716-446655440001", // Original attestation ID
        &truthlock.SupersedeRequest{
            PayloadB64URL: payloadB64,
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Old attestation status: %s\n", result.Old.Status) // "SUPERSEDED"
    fmt.Printf("New attestation ID: %s\n", result.New.ID)
}
```

<Info>
  Both the original and new attestation remain in the transparency log. Verifiers can trace the full chain using the `superseded_by_attestation_id` field on the original.
</Info>

## Configuration

```go theme={null}
client := truthlock.NewClient(truthlock.Config{
    // Required
    BaseURL:  "https://api.truthlocks.com",  // API base URL
    TenantID: "your-tenant-id",              // From Console > Settings
    APIKey:   "tl_live_...",                 // From Console > API Keys

    // Optional
    Timeout:          30 * time.Second,  // Request timeout (default: 30s)
    MaxRetries:       3,                 // Auto-retry with backoff (default: 3)
    IdempotencyPrefix: "my-app",         // Prefix for auto-generated idempotency keys
    HTTPClient:       &http.Client{},    // Custom HTTP client
})
```

## Idempotency keys

The SDK generates an `Idempotency-Key` header automatically on every write operation (mint, revoke, supersede), making retries safe by default. If a request fails and is retried, the server returns the original response instead of performing the action twice.

When multiple applications share the same tenant, set `IdempotencyPrefix` to namespace the auto-generated keys and prevent collisions:

```go theme={null}
// Service A
clientA := truthlock.NewClient(truthlock.Config{
    BaseURL:           "https://api.truthlocks.com",
    TenantID:          "shared-tenant-id",
    APIKey:            "tl_live_...",
    IdempotencyPrefix: "billing-svc",
})

// Service B
clientB := truthlock.NewClient(truthlock.Config{
    BaseURL:           "https://api.truthlocks.com",
    TenantID:          "shared-tenant-id",
    APIKey:            "tl_live_...",
    IdempotencyPrefix: "onboarding-svc",
})
```

With these prefixes, a key generated by the billing service looks like `billing-svc_<uuid>`, while onboarding produces `onboarding-svc_<uuid>` — so concurrent requests from different services never conflict.

<Info>
  Idempotency keys expire after 24 hours. If you omit `IdempotencyPrefix`, keys are generated without a prefix and are still unique per request.
</Info>

## Retry behavior

When `MaxRetries` is set (default: 3), the SDK automatically retries failed requests using exponential backoff with jitter. You do not need to implement retry logic yourself for transient failures.

### What gets retried

The SDK retries a request when **all** of the following are true:

* The HTTP status code is retryable: `408`, `429`, `500`, `502`, `503`, or `504`
* The retry count has not exceeded `MaxRetries`
* The request context has not been cancelled or timed out

Requests that return `400`, `401`, `403`, `404`, or `409` are **not** retried because these indicate a problem with the request itself.

### Backoff schedule

Retry delays use exponential backoff with jitter to avoid thundering-herd effects:

| Attempt   | Base delay | With jitter (approx.) |
| :-------- | :--------- | :-------------------- |
| 1st retry | 100 ms     | 80–120 ms             |
| 2nd retry | 200 ms     | 160–240 ms            |
| 3rd retry | 400 ms     | 320–480 ms            |

The delay doubles on each attempt and is capped at 2 seconds. Jitter adds up to ±20% randomization.

### Retry-After header

When the API returns a `429` response with a `Retry-After` header, the SDK respects the server-specified delay instead of using the calculated backoff. This ensures you do not retry faster than the rate limit allows.

### Disabling retries

Set `MaxRetries` to `0` to disable automatic retries entirely:

```go theme={null}
client := truthlock.NewClient(truthlock.Config{
    BaseURL:    "https://api.truthlocks.com",
    TenantID:   "your-tenant-id",
    APIKey:     "tl_live_...",
    MaxRetries: 0, // No automatic retries
})
```

<Info>
  The SDK generates idempotency keys automatically for write operations, so retries for mint and revoke calls are safe. See [idempotency keys](#idempotency-keys) to configure a prefix for multi-service environments.
</Info>

## Error handling

API errors are returned as `*truthlock.Error`, which implements the standard `error` interface.

```go Error handling theme={null}
attestation, err := client.Attestations.Mint(ctx, req)
if err != nil {
    var tlErr *truthlock.Error
    if errors.As(err, &tlErr) {
        fmt.Printf("Code:    %s\n", tlErr.Code)    // e.g. "ISSUER_NOT_TRUSTED"
        fmt.Printf("Message: %s\n", tlErr.Message) // Human-readable
        fmt.Printf("Status:  %d\n", tlErr.Status)  // HTTP status code

        // Check for specific error types
        if tlErr.IsCode("RATE_LIMITED") {
            // Already retried MaxRetries times — back off at the application level
        }
        if tlErr.NotFoundError() {
            // Resource does not exist — do not retry
        }
    }
    return err
}
```

## API Methods

All methods accept a `context.Context` as the first argument for cancellation and deadline propagation.

### Issuers

* `client.Issuers.Create(ctx, req)`
* `client.Issuers.Get(ctx, id)`
* `client.Issuers.List(ctx)`
* `client.Issuers.Trust(ctx, id)`
* `client.Issuers.Suspend(ctx, id, reason)`
* `client.Issuers.Revoke(ctx, id, reason)`

### Keys

* `client.Keys.Register(ctx, issuerId, req)`
* `client.Keys.List(ctx, issuerId)`
* `client.Keys.Rotate(ctx, kid, req)`
* `client.Keys.ReportCompromise(ctx, kid)`

### Attestations

* `client.Attestations.Mint(ctx, req)`
* `client.Attestations.Get(ctx, id)`
* `client.Attestations.List(ctx)`
* `client.Attestations.Revoke(ctx, id, req)`
* `client.Attestations.Supersede(ctx, id, req)` — [see supersede API](/api-reference/attestations/supersede)
* `client.Attestations.GetProofBundle(ctx, id)`

### Receipts

* `client.Receipts.Mint(ctx, req)`
* `client.Receipts.Get(ctx, id)`
* `client.Receipts.List(ctx, filter)`
* `client.Receipts.Revoke(ctx, id, req)`
* `client.Receipts.ListTypes(ctx)`
* `client.Receipts.GetType(ctx, name)`
* `client.Receipts.CreateType(ctx, req)`
* `client.Receipts.GetProofBundle(ctx, id)` — [see proof bundle API](/api-reference/receipts/proof-bundle)
* `client.Receipts.Verify(ctx, receiptID)` — [see verify API](/api-reference/receipts/verify)
* `client.Receipts.Search(ctx, query)` — [see search API](/api-reference/receipts/search)
* `client.Receipts.Export(ctx, req)` — [see export API](/api-reference/receipts/export)
* `client.Receipts.GetExport(ctx, exportID)`
* `client.Receipts.Redact(ctx, id)` — [see redact API](/api-reference/receipts/redact)

### Verification

* `client.Verify.VerifyOnline(ctx, req)`

### API Keys

* `client.APIKeys.List(ctx)`
* `client.APIKeys.Create(ctx, req)`
* `client.APIKeys.Revoke(ctx, id)`

### Audit

* `client.Audit.Query(ctx, params)`
* `client.Audit.Export(ctx, req)`

### Governance

* `client.Governance.ListRequests(ctx)`
* `client.Governance.CreateRequest(ctx, req)`
* `client.Governance.ApproveRequest(ctx, id)`
* `client.Governance.ExecuteRequest(ctx, id)`

## Querying audit logs

Retrieve audit events to track API activity, monitor security events, and generate compliance reports. Filter by action, actor, resource, or time range.

```go audit.go theme={null}
package main

import (
	"context"
	"fmt"
	"log"

	truthlock "github.com/truthlocks/sdk-go"
)

func main() {
	client := truthlock.NewClient(truthlock.Config{
		BaseURL:  "https://api.truthlocks.com",
		TenantID: "your-tenant-id",
		APIKey:   "tl_live_...",
	})
	ctx := context.Background()

	// Query recent attestation events
	events, err := client.Audit.Query(ctx, map[string]string{
		"action": "attestation.mint",
		"from":   "2026-01-01T00:00:00Z",
		"to":     "2026-01-31T23:59:59Z",
		"limit":  "100",
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, event := range events {
		fmt.Printf("[%s] %s by %s\n", event.Timestamp, event.Action, event.ActorID)
	}

	// Export audit logs for compliance
	exportJob, err := client.Audit.Export(ctx, &truthlock.AuditExportRequest{
		StartDate: "2026-01-01",
		EndDate:   "2026-01-31",
		Format:    "csv",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Export started: %s (status: %s)\n", exportJob.ID, exportJob.Status)
}
```

See the [audit API reference](/api-reference/audit/query) for the full list of filter parameters.

## Governance workflows

Manage formal issuer actions — suspend, revoke, reinstate, and change trust tier — through a multi-party approval workflow. Create a request, collect approvals from authorized reviewers, then execute.

```go governance.go theme={null}
package main

import (
	"context"
	"fmt"
	"log"

	truthlock "github.com/truthlocks/sdk-go"
)

func main() {
	client := truthlock.NewClient(truthlock.Config{
		BaseURL:  "https://api.truthlocks.com",
		TenantID: "your-tenant-id",
		APIKey:   "tl_live_...",
	})
	ctx := context.Background()

	// Create a governance request to suspend an issuer
	req, err := client.Governance.CreateRequest(ctx, &truthlock.GovernanceRequest{
		Action:   "suspend",
		IssuerID: "issuer-uuid",
		Reason:   "Compliance review pending",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Request created: %s (status: %s)\n", req.ID, req.Status)

	// Another authorized user approves the request
	if _, err := client.Governance.ApproveRequest(ctx, req.ID); err != nil {
		log.Fatal(err)
	}

	// Execute the approved request
	result, err := client.Governance.ExecuteRequest(ctx, req.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Executed: %s — issuer is now %s\n", result.ID, result.IssuerStatus)

	// List all governance requests
	requests, err := client.Governance.ListRequests(ctx)
	if err != nil {
		log.Fatal(err)
	}
	for _, r := range requests {
		fmt.Printf("[%s] %s %s — %s\n", r.Status, r.Action, r.IssuerID, r.Reason)
	}
}
```

See the [governance API reference](/api-reference/governance/list-requests) for the full request and response schemas.

## Receipt operations

Mint, verify, search, export, and redact cryptographically signed receipts. See the [receipts guide](/guides/receipts) for an overview of receipt types and the full lifecycle.

```go receipts.go theme={null}
package main

import (
	"context"
	"fmt"
	"log"

	truthlock "github.com/truthlocks/sdk-go"
)

func main() {
	client := truthlock.NewClient(truthlock.Config{
		BaseURL:  "https://api.truthlocks.com",
		TenantID: "your-tenant-id",
		APIKey:   "tl_live_...",
	})
	ctx := context.Background()

	// Mint a payment receipt
	receipt, err := client.Receipts.Mint(ctx, &truthlock.MintReceiptRequest{
		IssuerID:    "your-issuer-id",
		KID:         "ed-key-2026",
		Alg:         "Ed25519",
		ReceiptType: "payment_receipt",
		Subject:     "customer@example.com",
		Payload: map[string]interface{}{
			"amount":             5000,
			"currency":           "USD",
			"provider":           "stripe",
			"provider_reference": "ch_3Px...",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Verify a receipt
	result, err := client.Receipts.Verify(ctx, receipt.ReceiptID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Verdict: %s\n", result.Verdict) // "VALID"

	// Download a proof bundle for offline verification
	bundle, err := client.Receipts.GetProofBundle(ctx, receipt.ReceiptID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Bundle version: %s\n", bundle["bundle_version"])

	// Search receipts
	results, err := client.Receipts.Search(ctx, &truthlock.SearchReceiptsQuery{
		Q:           "stripe",
		ReceiptType: "payment_receipt",
		FromDate:    "2026-01-01T00:00:00Z",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Found %d receipts\n", len(results.Items))

	// Export receipts as JSON
	exportJob, err := client.Receipts.Export(ctx, &truthlock.CreateExportRequest{
		Format:  "json",
		Filters: map[string]interface{}{"receipt_type": "payment_receipt"},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Poll for completion
	completed, err := client.Receipts.GetExport(ctx, exportJob.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Export status: %s\n", completed.Status)

	// Redact PII from a receipt (permanent, for GDPR erasure requests)
	_, err = client.Receipts.Redact(ctx, receipt.ReceiptID)
	if err != nil {
		log.Fatal(err)
	}
}
```

See the [receipts API reference](/api-reference/receipts/mint) for the full request and response schemas.
