Skip to content
Finowl

Go · Email breach checks

Check email breaches in Go with net/http

Go decodes the email breach response into structs with very little ceremony. The breach records use PascalCase keys, so fields named Title, BreachDate, DataClasses and IsVerified line up with the JSON without any struct tags. The envelope fields breached and breaches also match exported Go fields because encoding/json compares names case-insensitively when no tag is present.

The program below builds one http.Client with a 10 second timeout and reuses it across attempts, which is the idiomatic way to keep connections pooled. A switch on the status code separates validation errors, authentication problems and rate limiting. On a rate_limited 429 it sleeps for the Retry-After seconds and tries again, while quota_exceeded falls through to an error so a worker can stop cleanly.

Steps

  1. 1

    Use Go 1.21 or newer, since the example relies on the built-in max function.

  2. 2

    Export FINOWL_API_KEY and save the program as main.go in a new module.

  3. 3

    Run go run main.go to print each breach's title, date and exposed data classes.

  4. 4

    Replace the hard-coded address with input from your handler or job queue.

Check email breaches in Go with net/http
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

type Breach struct {
	Title       string
	BreachDate  string
	DataClasses []string
	IsVerified  bool
}

type EmailResult struct {
	Email    string
	Breached bool
	Breaches []Breach
}

func checkEmail(client *http.Client, email string) (*EmailResult, error) {
	body, _ := json.Marshal(map[string]string{"email": email})
	for attempt := 0; attempt < 3; attempt++ {
		req, _ := http.NewRequest(http.MethodPost, "https://api.finowl.co/v1/breaches/email", bytes.NewReader(body))
		req.Header.Set("Authorization", "Bearer "+os.Getenv("FINOWL_API_KEY"))
		req.Header.Set("Content-Type", "application/json")
		resp, err := client.Do(req)
		if err != nil {
			return nil, err
		}
		var out struct {
			EmailResult
			Error struct{ Code string }
		}
		err = json.NewDecoder(resp.Body).Decode(&out)
		resp.Body.Close()
		switch code := out.Error.Code; {
		case resp.StatusCode == http.StatusOK:
			return &out.EmailResult, err
		case resp.StatusCode == http.StatusBadRequest, resp.StatusCode == http.StatusUnauthorized:
			return nil, fmt.Errorf("request rejected (%d): %s", resp.StatusCode, code)
		case resp.StatusCode == http.StatusTooManyRequests && code == "rate_limited":
			secs, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
			time.Sleep(time.Duration(max(secs, 1)) * time.Second)
		default:
			return nil, fmt.Errorf("finowl error %d: %s", resp.StatusCode, code)
		}
	}
	return nil, fmt.Errorf("still rate limited after 3 attempts")
}

func main() {
	result, err := checkEmail(&http.Client{Timeout: 10 * time.Second}, "alex@example.com")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s breached=%t\n", result.Email, result.Breached)
	for _, b := range result.Breaches {
		fmt.Printf("%s (%s) verified=%t\n", b.Title, b.BreachDate, b.IsVerified)
		fmt.Println("  exposed:", strings.Join(b.DataClasses, ", "))
	}
}

IN PRODUCTION

Production tips

  • Share one http.Client across goroutines rather than creating one per request. It is safe for concurrent use and reuses TCP connections to api.finowl.co.

  • Use a buffered channel or golang.org/x/time/rate limiter in front of checkEmail when fanning out over many users, so all goroutines together respect 60 requests per minute.

  • Pass a context from your HTTP handler with http.NewRequestWithContext in production code, so a cancelled client request also cancels the outbound breach lookup.

Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.

A FEW GOOD QUESTIONS

Common questions

Check email breaches in Go with net/http

Grab an API key, paste the example and run it. 100 free credits every month.