Go SDK — relay handler
github.com/paylera/paylera-go/relay is the Go implementation of the
ten-route SDK relay protocol. The merchant’s backend mounts it under
any prefix (convention: /api/paylera) and the React SDK calls into
it from the browser — the relay forwards to Paylera with the secret
token injected upstream.
import ( "net/http" "os"
"github.com/paylera/paylera-go/relay")
mux := http.NewServeMux()mux.Handle("/api/paylera/", http.StripPrefix("/api/paylera", relay.Handler(relay.Options{ APIToken: os.Getenv("PAYLERA_API_TOKEN"), AutoCreateCustomer: true, CSRF: relay.CSRFEnabled, Identify: func(r *http.Request) (relay.CustomerIdentity, error) { sess, err := mySessionLookup(r) if err != nil { return relay.CustomerIdentity{}, err } return relay.CustomerIdentity{ CustomerID: sess.PayleraCustomerID, Email: sess.Email, Name: sess.DisplayName, TenantID: sess.TenantID, }, nil }, })))The handler is http.Handler and therefore framework-agnostic — any
router that consumes Go HTTP handlers can mount it.
Routes
| Route | Forwards to | CSRF | Idempotency |
|---|---|---|---|
GET /csrf-token | (relay-only) | n/a | n/a |
POST /attach | POST /v1/attach | yes | required |
POST /check | POST /v1/check | yes | optional |
GET /check?feature_code=… | POST /v1/check | no | n/a |
POST /track | POST /v1/track | yes | required |
GET /entitlements?subscription_id=… | GET /v1/subscriptions/{id}/entitlements | no | n/a |
GET /me | GET /v1/customers/{id} (auto-create if enabled) | no | n/a |
GET /plans | GET /v1/plans | no | n/a |
GET /invoices | GET /v1/customers/{id}/invoices | no | n/a |
POST /billing-portal | POST /v1/customers/{id}/billing-portal-sessions | yes | required |
POST /subscriptions/{id}/upgrade | POST /v1/subscriptions/{id}/change-plan | yes | required |
POST /subscriptions/{id}/cancel | POST /v1/subscriptions/{id}/cancel?version=N | yes | required |
Identify
The relay’s IdentifyFunc runs once per request inside the request
hot path. Keep it cheap and non-blocking — it’s where you bridge your
session store to a Paylera CustomerIdentity:
type CustomerIdentity struct { CustomerID string // optional when AutoCreateCustomer is on + Email is set Email string // required for auto-create Name string // forwarded on auto-create Currency string // ISO-4217; default "USD" Metadata map[string]any // merged on auto-create TenantID string // mixes into the auto-create idempotency key}Returning an error → 401 with an RFC 9457 problem-details body.
Returning an empty identity (no CustomerID, no Email) → 401.
When AutoCreateCustomer: true, calling /me with CustomerID == ""
and a usable Email mints the Paylera customer with the protocol’s
idempotency key:
paylera-relay-autocreate:{tenant_id}:{email}Concurrent first-time renders share the key — no double-create.
Mounting under common routers
The relay implements http.Handler so any router that consumes Go
HTTP handlers can mount it.
net/http
mux := http.NewServeMux()mux.Handle("/api/paylera/", http.StripPrefix("/api/paylera", relay.Handler(opts)))chi
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()r.Mount("/api/paylera", relay.Handler(opts))chi.Mount already strips the prefix.
gorilla/mux
import "github.com/gorilla/mux"
m := mux.NewRouter()m.PathPrefix("/api/paylera/").Handler(http.StripPrefix("/api/paylera", relay.Handler(opts)))Gin
import "github.com/gin-gonic/gin"
g := gin.New()relayMounted := http.StripPrefix("/api/paylera", relay.Handler(opts))g.Any("/api/paylera/*path", gin.WrapH(relayMounted))The wildcard *path catches every sub-route. Install your auth
middleware on the relay group before the wrap — the relay reads
the session off the *http.Request context, not off gin.Context.
Working example: examples/go/gin-server/.
Echo
import "github.com/labstack/echo/v4"
e := echo.New()e.Any("/api/paylera/*", echo.WrapHandler(http.StripPrefix("/api/paylera", relay.Handler(opts))))Fiber
import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/adaptor")
app := fiber.New()app.Use("/api/paylera", adaptor.HTTPHandler(http.StripPrefix("/api/paylera", relay.Handler(opts))))Options.MountPrefix is an alternative to http.StripPrefix when the
host router passes the full path through — set it to the same prefix
you’d otherwise strip:
relay.Handler(relay.Options{ APIToken: token, Identify: identify, MountPrefix: "/api/paylera",})CSRF
CSRFEnabled (the default) enforces the double-submit-cookie pattern
locked in the protocol:
GET /csrf-tokenmints a 32-byte base64url token, sets thepaylera_csrfcookie (SameSite=Lax,HttpOnly=false), and returns{ "token": "…", "expires_at": "…" }.- Every state-changing route (
POST,PUT,DELETE,PATCH) compares thePaylera-CSRF-Tokenheader to the cookie value withsubtle.ConstantTimeCompare. Missing / mismatched → 403 withpaylera.csrf_mismatch. GET,HEAD,OPTIONSskip the check by HTTP semantics.
CSRFDisabled turns the middleware off entirely — only use this when
you chain your host framework’s own anti-CSRF middleware upstream of
the relay.
relay.Handler(relay.Options{ APIToken: token, Identify: identify, CSRF: relay.CSRFEnabled, CookieDomain: ".example.com", CookiePath: "/api/paylera", CookieSecure: true,})Forwarding contract
- The Paylera response body is passed back verbatim — RFC 9457 problem-details from Paylera reach the browser untouched.
Content-Type,Content-Encoding, andX-Request-Idfrom upstream are preserved; hop-by-hop headers are stripped.Idempotency-Key: caller-supplied wins; otherwise the relay stamps a UUID v7 on every state-changing route that requires it (Attach,Track,BillingPortal,Upgrade,Cancel).AuthorizationandPaylera-Api-Versionare injected upstream — the browser never sees the secret token.
OpenTelemetry
The relay emits server-kind spans named Paylera.Relay.<Op> against
the tracer Paylera.Relay. Attributes:
paylera.endpointpaylera.customer_idpaylera.tenant_idpaylera.subscription_idpaylera.feature_codepaylera.idempotency_key(truncated to 8 chars)paylera.api_modepaylera.problem.typeon error
No-op when no OTel SDK is registered.