Skip to content
NewA free test that shows whether your emails will reach the inbox.
Quickstart run with Go 1.27, 5 min read

Send email from Go with the Runearc SDK

One module, standard library only. This page installs it, sends an email to a test inbox, reads it back and verifies a signed webhook. The output below is from a real run.

Terminal
go get github.com/runearc-labs/runearc-go

Not published yet: this is the command that will work after the release. See the SDK page for the status of every package.

Terminal output of the Go quickstart: an inbox address, the id of the email sent, the received subject, and webhook verified
The Go quickstart, run once with go run .: it creates an inbox, sends an email to it, reads it back and verifies a signed webhook. Output unedited.

Install

Go 1.21 or later. The module uses only the standard library. The commands on this page are the ones that will work once the package is released; none of the five Runearc SDK packages is on a registry yet, and until then the Resend SDK or plain HTTP calls with your API key do the same work. The SDK covers emails, senders, inboxes, messages, threads, conversations, domains, webhooks, keys, suppressions and events. Contacts, lists, campaigns, sequences, templates, forms, pages and analytics are not in it: use the REST API, the CLI or MCP for those.

Terminal
go get github.com/runearc-labs/runearc-go

Create an API key in the dashboard and export it as RUNEARC_API_KEY. The quickstart below also reads RUNEARC_FROM, a sender address on a domain you have verified, and RUNEARC_BASE_URL to point at another API host. Tested on Go 1.27.

Send an email

The whole quickstart: create an inbox, send an email to it, wait for it to arrive and check a webhook signature.

// Command quickstart creates an inbox, sends to it, waits for the message and verifies a webhook.
package main

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	runearc "github.com/runearc-labs/runearc-go"
)

func main() {
	ctx := context.Background()
	client, err := runearc.New("") // reads RUNEARC_API_KEY and RUNEARC_BASE_URL
	check(err)

	inbox, err := client.Inboxes.Create(ctx, runearc.CreateInboxRequest{DisplayName: "quickstart"})
	check(err)
	fmt.Println("inbox", inbox.Address)

	subject := fmt.Sprintf("Hello from the Go SDK %d", time.Now().UnixNano()%1_000_000)
	sent, err := client.Emails.Send(ctx, &runearc.SendEmailRequest{
		From: os.Getenv("RUNEARC_FROM"), To: []string{inbox.Address}, Subject: subject, Text: "It works.",
	})
	check(err)
	fmt.Println("sent", sent.ID)

	waited, err := client.Messages.Wait(ctx, inbox.ID, runearc.WaitParams{Subject: subject, After: inbox.CreatedAt, Timeout: 30 * time.Second})
	check(err)
	if waited == nil {
		log.Fatal("no message arrived within 30 seconds")
	}
	fmt.Println("received", waited.Message.Subject)

	// Sign a sample event the way Runearc does (Standard Webhooks), then verify it.
	raw := make([]byte, 32)
	_, err = rand.Read(raw)
	check(err)
	secret := "whsec_" + base64.StdEncoding.EncodeToString(raw)
	payload := fmt.Sprintf(`{"id":"evt_1","type":"message.delivered","data":{"email_id":%q}}`, sent.ID)
	ts := time.Now().Unix()
	mac := hmac.New(sha256.New, raw)
	fmt.Fprintf(mac, "evt_1.%d.%s", ts, payload)
	headers := http.Header{}
	headers.Set("Webhook-Id", "evt_1")
	headers.Set("Webhook-Timestamp", fmt.Sprint(ts))
	headers.Set("Webhook-Signature", "v1,"+base64.StdEncoding.EncodeToString(mac.Sum(nil)))
	_, err = runearc.VerifyWebhook(runearc.VerifyWebhookInput{Payload: []byte(payload), Headers: headers, Secret: secret})
	check(err)
	fmt.Println("webhook verified")

	check(client.Inboxes.Delete(ctx, inbox.ID))
}

func check(err error) {
	if err != nil {
		log.Fatal(strings.TrimSpace(err.Error()))
	}
}

Emails.Send takes the same fields as the REST API and returns the new email's id. It sends an Idempotency-Key for you, so a retry does not send twice. Passing After: inbox.CreatedAt to Messages.Wait is needed because Wait only matches messages received after it starts, and the email can arrive before the wait begins. Every call takes a context.Context, and cancelling it stops the call and any wait between retries.

Terminal output of the Go quickstart: an inbox address, the id of the email sent, the received subject, and webhook verified
The Go quickstart, run once with go run .: it creates an inbox, sends an email to it, reads it back and verifies a signed webhook. Output unedited.

The same output as selectable text

inbox inbox-ckz889@z45v4z.runearc.email
sent eml_01m70s235hsd207x8vzy2xdd12
received Hello from the Go SDK 655000
webhook verified

Ids and the inbox address are from that one run, on November 9, 2026.

Read an inbox

Messages.Wait blocks for up to Timeout and returns nil when nothing arrived, so check for it. When a message matches, it comes back with the one time codes and links already extracted, which is what a sign-up test needs. The screenshot shows the same inbox in the dashboard. See wait for an email for every filter.

waited, err := client.Messages.Wait(ctx, inbox.ID, runearc.WaitParams{Subject: subject, After: inbox.CreatedAt, Timeout: 30 * time.Second})
check(err)
if waited == nil {
	log.Fatal("no message arrived within 30 seconds")
}
fmt.Println("received", waited.Message.Subject)
// waited.Codes and waited.Links hold the one time codes and links found in the message
Runearc dashboard showing the email sent by the Go quickstart, open in the test inbox
The email that go run . sent, as it appears in the Runearc dashboard. Local stack, throwaway workspace and test addresses only.

Verify a webhook

The lines in the quickstart verify a sample event the script signs itself with a random secret. It is not a live delivery from Runearc; it shows that VerifyWebhook accepts a correctly signed body and checks the timestamp, which must be within five minutes. For a real delivery, pass the raw request body and the request headers, as in the handler tab.

_, err = runearc.VerifyWebhook(runearc.VerifyWebhookInput{Payload: []byte(payload), Headers: headers, Secret: secret})
check(err)
fmt.Println("webhook verified")

The first tab is from the quickstart we ran. The receiver is from the package README and was not part of that run. The signature headers and the secret format are on the webhooks page.

Errors and retries

Methods return a *runearc.Error; use errors.As. It carries Status, a stable Code to branch on, a Kind such as KindForbidden, a Message, and a Fix when there is something to do.

_, err := client.Emails.Send(ctx, &runearc.SendEmailRequest{
	From: "noreply@acme.com", To: []string{"bob@example.com"}, Subject: "Hi", Text: "Hi",
})
var apiErr *runearc.Error
if errors.As(err, &apiErr) {
	fmt.Println(apiErr.Status) // 403
	fmt.Println(apiErr.Code)   // "sender_not_verified", a stable string to branch on
	fmt.Println(apiErr.Fix)    // what to do about it, when there is something to do
}

// A stable key makes your own retries safe too, for example from a job queue.
client.Emails.Send(ctx, email, runearc.WithIdempotencyKey("welcome/"+user.ID))

A request is retried up to two times by default (WithMaxRetries), with exponential backoff and jitter starting at 500 ms, and the wait honours Retry-After. A 429 is always retried; a 5xx, a network error or a timeout is retried only when repeating the request is safe. Other 4xx errors are never retried, and creating an inbox is not repeated after a 5xx. Mail sending gets an Idempotency-Key automatically and every retry reuses it; Runearc remembers keys for 24 hours.

This code is from the package README, not from the quickstart run. Every error code and its fix is listed on the errors page.

Where other SDKs are better

Read on November 9, 2026. Runearc's SDK is new and covers eleven resources; the others have longer histories and, in several cases, more features. We list them in alphabetical order.

  • Amazon SES

    The AWS SDK sending guide we read had tabs for .NET, Java, PHP, Ruby and Python and no Go tab in the content we saw. SES is still the choice when you already work with AWS credentials and IAM.

  • Mailgun

    Mailgun lists a Go SDK among its SDKs for Node.js, PHP, Java, Ruby and Python. Its page does not say which are officially maintained.

  • Postmark

    Postmark lists Go only among community libraries, not official ones, so it is not the pick if you want a vendor-maintained Go SDK.

  • Resend

    Resend publishes an official Go package, resend-go. Its docs link examples for attachments, templates, scheduling, audiences, domains and inbound webhooks, which are outside Runearc's eleven resources.

Maturity and deliverability track record are where Runearc is behind: the providers above have sent email for longer than we have. For the same comparison across the whole product, see Runearc compared with Resend.

Questions

Can I go get the module yet?

Not yet. The module is written and tested, and go get github.com/runearc-labs/runearc-go is the command that will work after the release. Until then, net/http with your API key does the same work.

Does it have dependencies?

No. The module uses only the Go standard library and needs Go 1.21 or later. We ran the quickstart on Go 1.27.

How do I stop a slow call?

Pass a context with a deadline or cancel it. Cancelling the context stops the call and any wait between retries, and errors.Is(err, context.Canceled) works on the result.

Is the webhook line in the quickstart a real delivery?

No. The script signs a sample event with a random secret, the way Runearc signs deliveries, and then verifies it. It shows that verification works; it does not show a delivery from Runearc.

Does the SDK cover contacts, campaigns or sequences?

No. It covers emails, senders, inboxes, messages, threads, conversations, domains, webhooks, keys, suppressions and events. Everything else is in the REST API, the CLI and MCP.

Put Runearc to work

Get inboxes, sending, your own domains and MCP in one workspace. The free plan includes 3 inboxes, 3,000 emails a month and 3 custom domains, with no card.