Go SDK
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, which call the API directly.
Install
go get email-validation.org/go/emailvalidationimport "email-validation.org/go/emailvalidation"Validate an address
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). 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; 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
_, 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
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
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
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
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 and 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.