# Go SDK

URL: https://email-validation.org/docs/sdks/go/
Updated: 2026-10-23

> Documentation index: https://email-validation.org/llms.txt

The official Go SDK uses only the standard library (`net/http`, `encoding/json`, `crypto/hmac`) and has no
dependencies. Every method takes a `context.Context` first, results are typed structs and errors work with
`errors.Is` and `errors.As`. It needs Go 1.22 or later.

**Not published yet.** The Go module `email-validation.org/go/emailvalidation` is not on the Go module proxy yet, so the
install step below does not work today. The SDK is complete and tested; it will be published once the
registry account and the licence are in place. Until then, use the [code samples](https://email-validation.org/docs/code-samples/), which
call the API directly.

## Install

```bash
go get email-validation.org/go/emailvalidation
```

```go
import "email-validation.org/go/emailvalidation"
```

## Validate an address

```go
package main

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

	"email-validation.org/go/emailvalidation"
)

func main() {
	client, err := emailvalidation.New(os.Getenv("EV_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}
	result, err := client.Validate(context.Background(), "accepted@sandbox.email-validation.org", nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Status, result.Reason, result.Action) // valid accepted send
}
```

Create the client once and share it: it is safe for concurrent use. Options go in a `*ValidateOptions` (`nil` sends
none): `IPAddress`, `TimeoutSeconds` (seconds the check may take, 5–15) and `Fresh`.
`Fresh: emailvalidation.Bool(true)` checks the address again even when it was checked in the last
12 months (see [repeat checks](https://email-validation.org/docs/api/validate/#repeat-checks)). Nullable fields are pointers
(`nil` is JSON `null`), and a status, reason or action this SDK version does not know yet is kept as its raw string.

## Concurrency and cancellation

Calls are synchronous; run several at once with goroutines (keep within your [rate limit](https://email-validation.org/docs/rate-limits/); for
long lists use a bulk job). Cancelling the context, or its deadline passing, stops a call at once, during a request
or a retry wait, with a `ConnectionError` of code `aborted`; `errors.Is(err, context.Canceled)` also holds.

## Handle errors

```go
_, err := client.Validate(ctx, address, nil)
var apiErr *emailvalidation.Error
switch {
case errors.Is(err, emailvalidation.ErrInsufficientCredits):
	// buy credits; nothing was charged
case errors.Is(err, emailvalidation.ErrRateLimited):
	if errors.As(err, &apiErr) && apiErr.RetryAfter != nil {
		log.Printf("retry in %d s", *apiErr.RetryAfter)
	}
case errors.As(err, &apiErr):
	log.Println(apiErr.Kind, apiErr.Status, apiErr.Code, apiErr.RequestID)
}
```

Every API call returns an `*emailvalidation.Error` with `Kind`, `Status` (0 when no answer arrived), `Code`,
`Message`, `RequestID` and `RetryAfter`. Sentinels for `errors.Is`: `ErrInvalidRequest`, `ErrAuthentication`,
`ErrInsufficientCredits`, `ErrPermissionDenied`, `ErrNotFound`, `ErrConflict`, `ErrRateLimited`,
`ErrServiceUnavailable`, `ErrServer`, `ErrConnection` and `ErrUnexpectedResponse`. Wrong arguments return an error
wrapping `ErrInvalidArgument` before any request is sent.

## Retries and timeouts

```go
client, err := emailvalidation.New(os.Getenv("EV_API_KEY"),
	emailvalidation.WithTimeout(30*time.Second),      // per attempt
	emailvalidation.WithMaxRetries(2),                // after 429 / 503 answers
	emailvalidation.WithMaxRetryWait(10*time.Second), // longer Retry-After values are returned to you
)
```

`429` and `503` answers are retried with backoff, honouring `Retry-After`. A daily key cap is never retried, and a
validation is never retried after a network failure, because it may already have been charged.

## Credits, usage and status

```go
credits, err := client.Credits(ctx) // Balance, Debt, Pending, Lots
usage, err := client.Usage(ctx, &emailvalidation.UsageParams{
	From: "2026-10-19", To: "2026-11-30", GroupBy: emailvalidation.GroupByDay,
})
status, err := client.Status(ctx)
```

## Bulk jobs

```go
job, err := client.Bulk.Create(ctx, &emailvalidation.BulkCreateParams{
	Emails: []string{"accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"},
})

// or from a CSV file (up to 100 MB)
f, err := os.Open("customers.csv")
job, err = client.Bulk.Create(ctx, &emailvalidation.BulkCreateParams{
	File: f, ColumnName: "email", HasHeader: emailvalidation.Bool(true),
})

job, err = client.Bulk.Wait(ctx, job.ID, &emailvalidation.WaitOptions{
	OnProgress: func(j *emailvalidation.BulkJob) { log.Println(j.Status, j.Processed) },
})
if job.Status == emailvalidation.BulkJobStatusCompleted {
	csv, err := client.Bulk.Results(ctx, job.ID, &emailvalidation.ResultsOptions{
		Filter: []emailvalidation.Status{emailvalidation.StatusValid, emailvalidation.StatusRisky},
	})
	// …
} else if job.Status == emailvalidation.BulkJobStatusReady {
	// it could not start: job.ErrorCode is e.g. insufficient_credits (buy credits, then client.Bulk.Start)
}
```

`Wait` returns when the job is `completed`, `failed`, `cancelled` or `expired`, or when it is still `ready` with an
error code: it could not start and does not move on by itself. Set `Timeout` in `WaitOptions` to give up earlier.
Also: `Bulk.Get`, `Bulk.Start` for jobs created with `Start: emailvalidation.Bool(false)` (call it before `Wait`),
`Bulk.Cancel`, and `Bulk.ResultsStream`, which returns an `io.ReadCloser` for large files.

## Verify webhooks

```go
http.HandleFunc("/webhooks/email-validation", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body) // the raw body, before any JSON parsing
	if err != nil {
		http.Error(w, "bad body", http.StatusBadRequest)
		return
	}
	event, err := emailvalidation.VerifyWebhook(body, r.Header, os.Getenv("EV_WEBHOOK_SECRET"), nil)
	if err != nil {
		http.Error(w, "invalid signature", http.StatusBadRequest)
		return
	}
	if event.Type == emailvalidation.WebhookBulkCompleted {
		data, _ := event.BulkData()
		log.Println("job done", data.JobID)
	}
	w.WriteHeader(http.StatusNoContent)
})
```

`event.Type` can also be `webhook.test`, sent when you press **Send test** in the dashboard: answer it with a `2xx`
like any other event. `SignWebhook` creates a valid signature for local tests.

## Constants

`Statuses`, `Reasons`, `ReasonsByStatus`, `ReasonInfo`, `Actions`, `ErrorCodes`, `WebhookEvents`, typed constants
such as `StatusValid` and `ReasonAccepted`, and `SandboxAddress(reason)` mirror the API's vocabulary. See
[statuses and reasons](https://email-validation.org/docs/statuses/) and [test addresses](https://email-validation.org/docs/test-addresses/).

## Bring your own HTTP client

`WithHTTPClient(*http.Client)` uses your client (proxies, custom TLS); redirects are still never followed. To replace
the HTTP stack, implement the `Transport` interface (`Do(ctx, *Request) (*Response, error)`) and pass it with
`WithTransport`, for example in tests.
