Skip to content

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

OperationBehaviour
TrackUsageauto-stamps when Params.IdempotencyKey is empty
Attachauto-stamps
CreateCustomerauto-stamps
CheckEntitlementoptional — 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 string

Use 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.IdempotencyConflictError
if 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.IdempotencyInProgressError
if 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