Go’s standard library already has most of what you need to call a REST API.
That makes the HTTP part of email verification small: build a request, add your Bouncer API key, decode JSON and map the returned status to your application’s policy.
Bouncer’s Email Verification API supports real-time and batch workflows. This guide focuses on a real-time Go integration and shows where to switch to batch processing.
Syntax validation is not mailbox verification
Go can check whether a string resembles an email address.
That still does not tell you whether the domain can receive mail, whether the mailbox appears reachable or whether the address carries flags such as disposable or catch-all.
A verification API handles those checks outside your application and returns structured data you can use.
If you want the broader verification model before writing code, see how Bouncer email verification works.
Set up a Go client
package bouncer
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type Client struct {
APIKey string
HTTPClient *http.Client
}
Create the client once and reuse it:
client := &bouncer.Client{
APIKey: os.Getenv("BOUNCER_API_KEY"),
HTTPClient: &http.Client{
Timeout: 12 * time.Second,
},
}
Keep the key outside your source code and never place it in a public client application.
The current Bouncer API setup is shown here:

For production architecture, the API best-practices guide is a useful companion to this tutorial.
Model the response with Go structs
type DomainInfo struct {
Name string `json:"name"`
AcceptAll string `json:"acceptAll"`
Disposable string `json:"disposable"`
Free string `json:"free"`
}
type AccountInfo struct {
Role string `json:"role"`
Disabled string `json:"disabled"`
FullMailbox string `json:"fullMailbox"`
}
type VerificationResult struct {
Email string `json:"email"`
Name string `json:"name"`
Status string `json:"status"`
Reason string `json:"reason"`
Domain DomainInfo `json:"domain"`
Account AccountInfo `json:"account"`
}
You do not need to persist every returned field.
Keep the fields that drive your product logic, reporting or later re-verification.
Bouncer’s richer output can include status, reason, provider information and flags around account or domain characteristics.

Send a real-time verification request
func (c *Client) Verify(
ctx context.Context,
email string,
) (*VerificationResult, error) {
endpoint, err := url.Parse(
"https://api.usebouncer.com/v1/email/verify",
)
if err != nil {
return nil, err
}
query := endpoint.Query()
query.Set("email", email)
query.Set("timeout", "10")
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
endpoint.String(),
nil,
)
if err != nil {
return nil, err
}
req.Header.Set("x-api-key", c.APIKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 ||
resp.StatusCode >= 300 {
return nil, fmt.Errorf(
"bouncer returned HTTP %d",
resp.StatusCode,
)
}
var results []VerificationResult
if err := json.NewDecoder(resp.Body).
Decode(&results); err != nil {
return nil, err
}
if len(results) == 0 {
return nil, fmt.Errorf(
"empty verification response",
)
}
return &results[0], nil
}
The method accepts a context.Context, so the caller stays in control of cancellation and deadlines.
Add a context timeout
ctx, cancel := context.WithTimeout(
context.Background(),
12*time.Second,
)
defer cancel()
result, err := client.Verify(
ctx,
"john@example.com",
)
The application deadline and Bouncer’s verification timeout solve related but separate problems.
Your context controls how long your application is willing to wait. The verification timeout controls the verification request.
Map verification results to application decisions
type Decision string
const (
Allow Decision = "allow"
Reject Decision = "reject"
Review Decision = "review"
RetryLater Decision = "retry_later"
)
func Decide(
result *VerificationResult,
) Decision {
switch result.Status {
case "deliverable":
return Allow
case "undeliverable":
return Reject
case "risky":
return Review
case "unknown":
return RetryLater
default:
return Review
}
}
Risky is not the same as invalid. Unknown is not proof that the address does not exist.
See the Bouncer FAQ for the current status and reason definitions.
Use reason codes for better decisions
func ShouldPromptForCorrection(
result *VerificationResult,
) bool {
if result.Status != "undeliverable" {
return false
}
switch result.Reason {
case "invalid_email",
"invalid_domain",
"rejected_email":
return true
default:
return false
}
}
This lets your UI distinguish between a likely user mistake and a technical state that needs a different fallback.
The email verification buyer's guide also covers result structure, timeout rules and batch behavior.
Use verification inside an HTTP handler
func SignupHandler(
verifier *bouncer.Client,
) http.HandlerFunc {
return func(
w http.ResponseWriter,
r *http.Request,
) {
email := r.FormValue("email")
ctx, cancel := context.WithTimeout(
r.Context(),
12*time.Second,
)
defer cancel()
result, err :=
verifier.Verify(ctx, email)
if err != nil {
http.Error(
w,
"verification unavailable",
http.StatusServiceUnavailable,
)
return
}
switch Decide(result) {
case Reject:
http.Error(
w,
"check your email address",
http.StatusBadRequest,
)
return
case RetryLater:
// Product decision:
// queue, retry or accept with a flag.
}
w.WriteHeader(http.StatusCreated)
}
}
For ideas around point-of-entry verification, see real-time email validation.
Should you verify emails with goroutines?
Use bounded concurrency for small real-time workloads.
Do not spawn one unbounded goroutine per address across a huge dataset.
For 50,000 or 200,000 addresses, use bulk email verification instead.
A large list is a background-processing problem. Treat it like one.
Retry transient failures carefully
Retries belong around transient technical failures.
They do not belong around every result you dislike.
A sane policy includes:
- – small retry cap
- – exponential backoff
- – jitter
- – context cancellation
- – separate handling for auth and malformed requests
- – logs without secrets
Do not retry an undeliverable result just because the email matters to your product.
When to switch from real-time to batch
Use real-time verification for:
- – signups
- – invitations
- – checkout
- – user profile updates
- – lead capture
Use Batch API for:
- – imports
- – CRM cleanup
- – recurring database hygiene
- – campaign preparation
- – large migrations
Bouncer’s guide to a bulk validator with API and CSV upload support gives more context on where real-time, batch and file-based workflows fit.
If your users work inside AI assistants, Bouncer also supports email verification through MCP, which is a separate integration model from the REST flow described here.
Go production checklist
Before release:
- – reuse
http.Client - – set application and request timeouts
- – keep API keys out of source code
- – distinguish transport failures from verification statuses
- – keep status and reason where useful
- – cap retries
- – limit concurrency
- – monitor rate-limit responses
- – send bulk work to Batch API
- – avoid unnecessary raw email data in logs
If your team also uses Python, Bouncer already has a separate guide to email validation in Python.
FAQ
Can Go verify whether an email mailbox exists?
Go can validate syntax locally. Mailbox-level verification needs external mail-infrastructure signals, which is where an email verification API fits.
Is regex enough for email validation in Go?
No. Regex checks shape, not mailbox deliverability.
Should I use goroutines for bulk email verification?
Use bounded concurrency for small real-time workloads. Use Batch API for large datasets instead of launching an unbounded number of requests.
What should I do with unknown results?
Keep unknown separate from invalid. Decide whether to retry later, accept with a flag or send the record into another workflow.
When should I use Batch API?
Use it once verification becomes a background data-processing task rather than a request where a user is waiting for an immediate answer.
Final thought
Go makes the integration layer pleasantly small.
Reuse the HTTP client, pass context through the stack and keep verification statuses separate from transport failures.
The biggest architecture win comes from recognizing when the workload has moved beyond real-time verification and should become a batch job.
—

