mailgun-go and the Go client that covers sending, events and webhooks
Go library for the Mailgun API.
At a glance
- What is it?
- mailgun-go is the Go client for the Mailgun API, covering message sending, the events feed, webhook handling with signature verification, email validation and a long tail of domain and IP management endpoints. The module is on its fifth major version, the event types are concrete Go structs rather than maps, and the model layer is generated from Mailgun's own OpenAPI specification.
- Who is it for?
- Adopt mailgun-go if you are writing a Go service that sends through Mailgun or consumes its event stream, since the event types, the iterator interface and the webhook verification path are all handled for you and the models are regenerated from the vendor specification.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A v5 module, and the version is part of your import path
mailgun-go is a Go library for interacting with the Mailgun API. The installation is a single module fetch, and the README is explicit that the major version suffix has to be part of your import paths:
go get github.com/mailgun/mailgun-go/v5The module declaration in go.mod matches, with the module path github.com/mailgun/mailgun-go/v5 and a language requirement of Go 1.24.0. The runtime dependencies are few: the chi router for HTTP handling, google/uuid for identifiers, oapi-codegen/runtime for the generated model layer, and stretchr/testify for tests.
That dependency list is a clue about how the library is built. A generated OpenAPI runtime in the dependency list means the request and response types are produced from a specification rather than written by hand, which is the subject of its own section below. The chi router suggests the library can host your webhook endpoint directly, or at least that HTTP handling code in the package uses it.
The library is maintained by Mailgun itself, and the repository carries a CHANGELOG.md, a CONTRIBUTING.md, a .golangci.yml configuration and a Makefile. The releases move steadily: v5.19.2 on 2026-08-17, v5.19.3 on 2026-08-25 and v5.19.4 on 2026-10-14, with the last push to the repository on 2026-10-19. The repository is not archived and the licence is BSD-3-Clause.
Sending a message and picking the right endpoint
The client is created with a key and a domain, and a message object carries the content:
message := mailgun.NewMessage(yourDomain, sender, subject, body, recipient)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Send the message with a 10-second timeout
resp, err := mg.Send(ctx, message)The README notes that the message object is where attachments and blind carbon copy recipients go, and the send call returns a response with an ID and a message string. Every call takes a context.Context, so the timeout policy stays with the caller, and the example uses a ten second deadline.
One configuration detail catches people out. The client is created with mailgun.NewMailgun(apiKey) and, by default, talks to the default API base. The README shows the alternative for European domains, where the endpoint has to be specified explicitly with a call to mg.SetAPIBase(mailgun.APIBaseEU). If you are serving EU customers and leave the default in place, the requests go to the wrong region and the failure will look like a configuration problem rather than a region problem, so set the base deliberately per environment.
The library also covers sending HTML templates, with its own section in the README, so template rendering for a send is part of the supported surface rather than something you assemble from raw API calls.
Events arrive as concrete types, not as maps
The events feed is the part of the API most Go services end up depending on, and mailgun-go models it with concrete types rather than a map of strings. The iterator is created with a domain and paging options:
it := mg.ListEvents("your-domain.com", &mailgun.ListEventOptions{Limit: 100})and each page is filled by calling it.Next with a context and a slice pointer. Within a page, the concrete event type is what the switch matches on:
switch event := e.(type) {
case *events.Accepted:
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
case *events.Delivered:
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
case *events.Failed:
fmt.Printf("Failed reason: %s\n", event.Reason)
case *events.Clicked:
fmt.Printf("Clicked GeoLocation: %s\n", event.GeoLocation.Country)The full set in the example covers accepted, delivered, failed, clicked, opened, rejected, stored and unsubscribed. Two accessors, GetName and GetTimestamp, work across the interface, so generic logging needs no type switch, while the type switch gives you the specific fields, the authentication flags on an accepted event, the transport on a delivered one, the reject reason, the storage URL or the client operating system.
That design is the reason to prefer this library over a hand-rolled HTTP client: the compile-time guarantee that a delivered event has a transport field, and that you will notice the day Mailgun adds an event type you have never handled, because the switch will no longer be exhaustive against the interface.
The events package is a separate import path, github.com/mailgun/mailgun-go/v5/events, so the types are available on their own.
Polling and webhooks: two delivery paths with different failure modes
You can consume events either by polling or by receiving webhooks, and the library supports both.
The polling iterator takes a begin time and a poll interval:
it := mg.PollEvents("your-domain.com", &mailgun.ListEventOptions{
// Only events with a timestamp after this date/time will be returned
Begin: begin,
// How often we poll the api for new events
PollInterval: time.Second * 30,
})and pages arrive through it.Poll with a context and a slice pointer. The example cancels that context to stop the loop, so the polling lifetime is bounded by the caller's context, which is the right place to put it.
The webhook path is the one with a security requirement, and the library does the cryptographic part for you. You set a signing key with mg.SetWebhookSigningKey, decode the request body into a mtypes.WebhookPayload, and ask the client to verify the signature:
verified, err := mg.VerifyWebhookSignature(payload.Signature)
if err != nil {
fmt.Printf("verify error: %s\n", err)
w.WriteHeader(http.StatusNotAcceptable)
return
}
if !verified {
w.WriteHeader(http.StatusNotAcceptable)
fmt.Printf("failed verification %+v\n", payload.Signature)
return
}Note that VerifyWebhookSignature returns two values, an error and a boolean, and the example checks both. Treating the boolean as the only signal is a classic way to end up accepting unverified requests, so keep both checks and reject with a non-success status before parsing the event, which is exactly the order the example uses, with events.ParseEvent called only after verification succeeds.
The example server listens on port 9090 and the library's chi dependency suggests it can be mounted into an existing HTTP server rather than run as its own process.
Generated models, with the OpenAPI spec as the source
The model layer is generated, and the Makefile shows the pipeline. There is a target called get-and-gen-models that depends on get-openapi and gen-models, and the comment above it describes the job as downloading OpenAPI 3.1 spec files and generating models. The spec is pulled from Mailgun's own documentation host:
get-openapi:
# https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun
curl -o $(TYPES_PATH)/mailgun/mailgun.yaml https://documentation.mailgun.com/_spec/docs/mailgun/api-reference/send/mailgun.yaml?downloadThe generator is oapi-codegen, pinned in the Makefile at version v2.8.0, and the generated types live under ./internal/types, a path that a comment in the Makefile flags as a place that might move into the mtypes and internal packages. The mtypes package is what the webhook example imports for its WebhookPayload type.
This is the most interesting design decision in the repository, and it has a clear benefit: the request and response structures track the vendor's specification instead of drifting behind it. It also has a cost worth naming. Because the types come from a specification published by the vendor, the API surface of your Go code can change when Mailgun publishes a new spec and the maintainers regenerate, and the type names in the generated package will be whatever the generator produces from the spec, not necessarily what a Go developer would have named them.
For a library that has been on its fifth major version, that pipeline is probably why. The bulk of the API surface is wide and mechanical, and a generator is the only sane way to keep it current.
Testing, linting and a nilness checker in the Makefile
The Makefile is where the project's engineering habits are visible, and there are several of them.
The default target runs the tests with the race detector and no caching, so a clean run is the default rather than an option:
test:
go test ./... -race -count=1Integration tests are separated by a build tag and need an environment file: the test-integration target sources .env and runs with the integration tag and a race detector. Keeping them behind a tag means the default test run needs no credentials and no network access to Mailgun, which is what makes it usable in continuous integration.
Beyond that there is a lint step that installs a pinned golangci-lint, version v2.10.1, from its install script and runs it over the repository, matching the .golangci.yml configuration at the root. There is a nilaway target that installs the nilaway tool and runs it over the package with test files excluded, and a comment in the Makefile notes that the mocks still need to be fixed before nilaway can be enabled for them too. That is an honest TODO left in the build file, and it tells you the nilness checking is applied to the library itself but not yet to its test doubles.
There is also a godoc target that stages the repository into a temporary GOPATH and serves the documentation on localhost:6060, and the Makefile notes the package path it uses for that staging.
The repository root shows the same breadth in code: separate files for alerts, analytics, api_keys, bounces, credentials, domain keys, domains and their connection, tracking and iterator variants, exports, inbox placement, inboxready domains, IP warmups, IPs, limits and mailing lists, alongside an events/ package, an internal/ directory, an examples/ directory and a test file per area, including integration tests for the domain iterator and for analytics.
A BSD licence and a patch-level release cadence
The licence is BSD-3-Clause, with the licence text at the root of the repository. The BSD family is permissive, allowing commercial use and modification with the obligation to retain the copyright and warranty disclaimers, and it is a licence that plays well with corporate open source policies. Reading the licence file for the exact wording is the right step, and this is not legal advice.
The release pattern is worth noting for anyone pinning the dependency. The v5 line has been shipping patch releases steadily, v5.19.2 on 2026-08-17, v5.19.3 on 2026-08-25 and v5.19.4 on 2026-10-14, and the last push to the repository was on 2026-10-19, days after that tag. A library that is still taking patch releases within the same minor version is usually one that is adding API surface or fixing behaviour without breaking changes, which is what you want from a client for a third-party API.
Two things follow for an adoption decision. First, because the module path carries the major version, you can depend on v5 and expect the import path to stay stable, so an upgrade is a version bump rather than a rewrite of your imports. Second, because models are regenerated from the vendor specification, a patch release can occasionally add or change a field in a response struct, so a regression test that unmarshals a real event into your own type is worth having.
The documentation for the API itself lives on Mailgun's documentation site, and the GoDoc badge in the README points at the package documentation for the v5 path.
Editorial conclusion
Adopt mailgun-go if you are writing a Go service that sends through Mailgun or consumes its event stream, since the event types, the iterator interface and the webhook verification path are all handled for you and the models are regenerated from the vendor specification. Skip it if you need to talk to Mailgun from a language other than Go, since this library covers only Go, and expect a fast-moving surface, because the v5 line shipped three patch releases between August and October 2026. Verify first that your domain and API key are right for your region, since an EU domain needs the API base set explicitly, and that your webhook handler rejects unverified signatures, because the library returns a boolean you have to check.
Frequently asked questions
How do I install the Mailgun Go library?
Run go get github.com/mailgun/mailgun-go/v5. The README stresses that the /v5 suffix has to be included in your import paths, and go.mod declares a Go 1.24.0 requirement.
How do I use an EU Mailgun domain with mailgun-go?
Call mg.SetAPIBase(mailgun.APIBaseEU) on the client, as the README shows for EU domains, since the client otherwise talks to the default API base.
How do I receive Mailgun events in a Go service?
Either poll with mg.PollEvents and a ListEventOptions value carrying a Begin time and a PollInterval, iterating pages with it.Poll, or receive webhooks and verify them with mg.SetWebhookSigningKey and mg.VerifyWebhookSignature before parsing the payload with events.ParseEvent.
Does mailgun-go support email validation?
Yes, through mg.ValidateEmail with a context and the address, which returns a result whose Risk field the example prints. The README notes that your plan has to include email validations.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mailgun-mailgun-go)Was this useful?