Skip to content
useEventStack 0.3.0 is live.Read the quickstart

Reference

Emit and read events from Go

Use the useeventstack-go module to emit events, replay history, and query derived state. Zero dependencies beyond the standard library.

Install

go get github.com/useeventstack/useeventstack-go

Go 1.22 or newer. The client is built on net/http andencoding/json, so it adds nothing to your dependency tree.

The module path is the repository URL, so the Go module proxy resolves it directly — there is no replace directive to add. The SDK is MIT licensed; the platform itself is not open source.

Register a contract first

Every event type needs a registered contract in the project you are writing to. Contracts are an allow-list, not advisory validation: an event whose type has no contract is rejected. Register one in the dashboard, or generate the payload to POST with the CLI:

useeventstack contract --type deployment.completed \
  --field service --field version --field environment --field status

Create a client

import ues "github.com/useeventstack/useeventstack-go"

client, err := ues.NewClient(
    os.Getenv("USEEVENTSTACK_API_KEY"),
    ues.WithProjectID(os.Getenv("USEEVENTSTACK_PROJECT_ID")),
    ues.WithOrganizationID(os.Getenv("USEEVENTSTACK_ORGANIZATION_ID")),
    ues.WithBaseURL("https://api.useeventstack.com"),
)
if err != nil {
    return err
}

The API key and WithProjectID are both required;NewClient returns ErrMissingAPIKey orErrMissingProjectID rather than failing later at the first request. WithOrganizationID is needed for the workflow, custom action and API key services, which are addressed by organization.WithHTTPClient lets you supply your own timeouts, proxy or instrumentation.

What the API key decides

An API key belongs to exactly one organization, one project and one environment. All three are resolved on the server from the key, so a client cannot write outside the project its key was issued for. Three consequences are worth knowing before you start:

If youThen
omit WithProjectIDNewClient fails. The target is always explicit.
name a project the key does not ownthe request is refused with project_mismatch. It is not quietly redirected to the key's own project.
ask for an environment the key does not ownthe request is refused with environment_mismatch. It is not quietly coerced.

The last two are errors on purpose. Silently coercing them meant a developer pointing a production key at the sandbox wrote real data to production and was never told. To use the sandbox, use a key issued for the sandbox — sandbox environments require a plan that includes them, and otherwise return402.

Emit events

result, err := client.Events.Emit(ctx, "deployment.completed", map[string]any{
    "service":     "api",
    "version":     "2.1.0",
    "environment": "production",
    "status":      "healthy",
}, ues.WithIdempotencyKey("deploy-api-2.1.0"))
if err != nil {
    return err
}

fmt.Println(result.Event.ID, result.IdempotentReplay)

Emit returns a typed *EmitResult holding the storedEvent. WithIdempotencyKey makes the call safe to retry: repeating it returns the original event and setsIdempotentReplay. Keys are scoped to a project, so the same key used in two projects creates two events.

Use WithCausationID to record which event caused this one, andWithCorrelationID to group several events into one logical operation. Together they build the chain that Trace returns.

Read events

events, err := client.Events.Query(ctx,
    ues.WithEventType("deployment.completed"),
    ues.WithQueryCorrelationID("release-2.1.0"),
)

event, err := client.Events.Get(ctx, eventID)
chain, err := client.Events.Trace(ctx, eventID)

All three return typed Event values scoped to the client's project. An event that exists in a different project returns 404, not 403 — the response does not confirm that the id is real elsewhere.

Replay

// One event. Only an event in a processed, failed or dead_letter state can be
// replayed; a freshly ingested one returns 400.
_, err := client.Replay.Single(ctx, eventID, true /* suppress webhooks */)

// A filtered set. At least one filter is required — an unfiltered bulk replay
// is refused.
result, err := client.Replay.Bulk(ctx, ues.BulkReplayOptions{
    EventType:               "payment.failed",
    Since:                   "2026-01-01T00:00:00Z",
    SkipExternalSideEffects: true,
    Limit:                   500,
})
fmt.Println(result.RequeuedCount)

Both are project-scoped: a replay never touches a sibling project's stream.SkipExternalSideEffects suppresses outbound webhooks, which is what you want when rebuilding state rather than re-notifying.

DLQ.Reprocess(ctx, eventType) re-queues dead-lettered events of one type through the same project-scoped replay.

Errors

var apiErr *ues.APIError
if errors.As(err, &apiErr) {
    switch {
    case apiErr.IsRateLimited():
        // per-organization ingestion limit; retry shortly
    case apiErr.IsPlanLimit():
        // monthly volume, or a sandbox the plan does not include
    case apiErr.Code == "invalid_event_payload":
        // the payload does not satisfy the registered contract
    case apiErr.IsUnauthorized():
        // the API key was rejected
    }
    log.Printf("%s: %s (correlation_id=%s)",
        apiErr.Code, apiErr.Message, apiErr.CorrelationID)
}

Every non-2xx response is an *APIError. Branch onCode, which is stable — Message is prose and may change. CorrelationID identifies the request in our logs; include it in support requests. A response that is not our JSON envelope, for example a proxy error page, still arrives as an *APIError with code unexpected_response rather than a decode failure.

Service reference

ServiceMethodsReturns
client.EventsEmit, Query, Get, Tracetyped
client.CustomActionsList, Get, Delete, Simulatetyped
client.ReplaySingle, Bulktyped
client.WorkflowsList, Get, Create, Update, Delete, Enable, Disablejson.RawMessage
client.DLQList, Reprocessmixed
client.APIKeysList, Create, Deletejson.RawMessage
client.ProjectionsQuery, Consistency, Rebuildjson.RawMessage

Calls that need more than an API key

Two operations deliberately refuse key authentication.APIKeys.Create and APIKeys.Delete require a signed-in session, so a leaked key cannot mint more keys.Projections.Rebuild is destructive and requires a session plus a re-entered password. Use the dashboard or the CLI for both.

Uploading a custom action

Uploading a WebAssembly module is a multipart/form-data request and is not part of this SDK. Use the dashboard or the CLI. The SDK reads and simulates actions, which is what an application needs at runtime. A custom action belongs to one project, and a workflow step may only reference an action in its own project; CustomAction.ProjectID tells you which workspace owns it. The compiled module is never served back over the API.