Go SDK — idempotency
Paylera treats every state-changing request as idempotent on the
Idempotency-Key header. The Go SDK auto-stamps keys when you don’t
supply one and surfaces idempotency conflicts as typed errors. See
the cross-language Idempotency-Key contract for
the wire-level semantics.
Auto-stamping
// Auto-stamped: a fresh UUID v7 per call. The SDK injects the header// on every state-changing operation that requires it._, err := c.TrackUsage(ctx, nil, paylera.TrackRequest{ FeatureCode: "api_calls", Quantity: "1",})UUID v7 is monotonic-ish over time, which lines up with how Paylera indexes the per-tenant idempotency table — small but real win on write throughput in high-traffic accounts.
Caller-supplied keys
Pass an explicit Params struct to take control:
_, err := c.TrackUsage(ctx, &paylera.TrackUsageParams{IdempotencyKey: "req-abc-123"}, paylera.TrackRequest{ FeatureCode: "api_calls", Quantity: "1", },)Caller-supplied keys always win — if you set one, the SDK never
overwrites it. The friendlier alias dedup_key works for /v1/track
in the request body and is forwarded as the header (the relay normalises
this on the wire so merchants don’t see two parallel contracts).
What auto-stamps
| Operation | Behaviour |
|---|---|
TrackUsage | auto-stamps when Params.IdempotencyKey is empty |
Attach | auto-stamps |
CreateCustomer | auto-stamps |
CheckEntitlement | optional — opt in by passing Params.IdempotencyKey |
anything via Raw().* | not auto-stamped; pass the header yourself |
Generating a key yourself
import paylera "github.com/paylera/paylera-go"
key := paylera.NewIdempotencyKey() // UUID v7 stringUse this when you want the SDK’s exact key shape for your own request-replay handling.
Conflicts
Paylera distinguishes two cases:
paylera.idempotency_conflict — the same key arrived with a
different body. The merchant must either pick a fresh key or
re-send the original bytes. The SDK surfaces this as
*paylera.IdempotencyConflictError:
var conflict *paylera.IdempotencyConflictErrorif errors.As(err, &conflict) { log.Printf("key %s reused with a different body — dedup failure upstream", myKey) return}paylera.idempotency_in_progress — the same key arrived while
the original is still processing. Safe to retry the same call after
a short backoff; the cached response will be served once the first
request lands. Surfaced as *paylera.IdempotencyInProgressError:
var inProgress *paylera.IdempotencyInProgressErrorif errors.As(err, &inProgress) { time.Sleep(200 * time.Millisecond) // retry the same call with the same key}Retry interaction
The default retry policy fires on 5xx and 429 with jittered
exponential backoff (5 attempts, 200ms → 8s, ±25% jitter). The
Idempotency-Key is sticky across retries — the second attempt sends
the same key the first attempt did, so Paylera serves the cached
response if the first attempt actually committed.
This is the load-bearing safety property: a network failure that hits after Paylera processed the request but before your client saw the response will return the same result on the retry, not a duplicate side effect.
c, _ := paylera.NewClient( paylera.WithAPIToken("pl_live_..."), paylera.WithRetry(paylera.RetryConfig{ MaxAttempts: 3, InitialBackoff: 500 * time.Millisecond, MaxBackoff: 5 * time.Second, Multiplier: 2.0, JitterFraction: 0.1, }),)Relay handler
The relay subpackage auto-stamps idempotency keys on the routes that
require them (Attach, Track, BillingPortal, Upgrade, Cancel)
when the inbound browser request didn’t supply one. Caller-supplied
keys from the browser are passed through verbatim.
The /track endpoint accepts a dedup_key field in the JSON body as
a friendlier alias for Idempotency-Key; the relay strips that field
and stamps the corresponding header before forwarding.
See also
- Errors —
errors.AsagainstIdempotencyConflictError - Relay — how the relay stamps keys for the browser
- Cross-language idempotency