Go SDK — errors
Every non-2xx response from Paylera unwraps to *paylera.Error — the
base type carrying the RFC 9457 problem-details body, status, request
ID, and a stable Code. The common failure modes get dedicated
subtypes so you can write idiomatic Go:
import "errors"
if _, err := c.Attach(ctx, params, body); err != nil { var conflict *paylera.IdempotencyConflictError if errors.As(err, &conflict) { // same Idempotency-Key, different body — pick a new key return retryWithFreshKey() }
var rl *paylera.RateLimitError if errors.As(err, &rl) { time.Sleep(rl.RetryAfter) return retry() }
var auth *paylera.AuthenticationError if errors.As(err, &auth) { log.Fatal("paylera token expired or revoked") }
return err}Subtypes
| Type | HTTP | When |
|---|---|---|
*paylera.IdempotencyConflictError | 409 | same key, different body |
*paylera.IdempotencyInProgressError | 409 | same key, original still processing |
*paylera.AuthenticationError | 401 | missing / malformed / revoked API token |
*paylera.AuthorizationError | 403 | valid token, wrong scope (e.g. test token → live resource) |
*paylera.NotFoundError | 404 | resource does not exist |
*paylera.PreconditionFailedError | 412 | optimistic-concurrency mismatch on resource version |
*paylera.RateLimitError | 429 | exceeded rate limit; RetryAfter carries the parsed Retry-After |
*paylera.ServerError | 5xx | upstream failure; surfaced after retries exhausted |
Every subtype Unwrap()s to the base *paylera.Error, so a single
errors.As(err, &baseErr) catches everything when you don’t need to
discriminate.
The base *paylera.Error
type Error struct { Type string // RFC 9457 type URI, e.g. "paylera.idempotency_conflict" Status int // HTTP status code Code string // stable wire-form error code RequestID string // X-Request-Id, for correlating with Paylera support Retryable bool // server hint; the SDK retry policy is the source of truth Problem json.RawMessage // raw body — decode further for merchant-specific extensions}
func (e *Error) Error() string { /* "paylera: 409 paylera.idempotency_conflict (request_id=…)" */ }When you want the per-field errors from a validation failure:
var base *paylera.Errorif errors.As(err, &base) { var details struct { Errors []struct { Field string `json:"field"` Message string `json:"message"` } `json:"errors"` } if jsonErr := json.Unmarshal(base.Problem, &details); jsonErr == nil { for _, fe := range details.Errors { log.Printf("validation: %s → %s", fe.Field, fe.Message) } }}Pattern: handling a rate limit
RateLimitError.RetryAfter is pre-parsed — accepts either
delta-seconds or HTTP-date forms, returns zero when the server did
not advertise a delay. Falling back to the SDK’s default backoff is
safe in that case:
for attempt := 0; attempt < 5; attempt++ { _, err := c.TrackUsage(ctx, nil, req) if err == nil { return nil }
var rl *paylera.RateLimitError if errors.As(err, &rl) { wait := rl.RetryAfter if wait == 0 { wait = time.Duration(500*(1<<attempt)) * time.Millisecond } select { case <-time.After(wait): continue case <-ctx.Done(): return ctx.Err() } } return err // unrelated error}In practice the SDK’s built-in retry policy already handles 429 — this
pattern only fires when you’ve called WithRetry(RetryConfig{MaxAttempts: 0})
or exhausted the default attempts.
Pattern: optimistic concurrency on cancel
Cancel requires a version query param. A stale version → 412 →
*paylera.PreconditionFailedError:
var pre *paylera.PreconditionFailedErrorif errors.As(err, &pre) { // refetch the subscription to get the current version sub, gerr := c.Raw().GetSubscription(ctx, subID) if gerr != nil { return gerr } return c.Raw().CancelSubscription(ctx, subID, &paylera.CancelSubscriptionParams{ Version: sub.JSON200.Version, })}Pattern: route on Code for forward-compat
When Paylera adds a new problem type, your existing errors.As
switches keep working — the new code unwraps to the appropriate
status-class subtype. But if you want to react to a new type
specifically before the SDK ships a dedicated subtype, switch on
Code:
var base *paylera.Errorif errors.As(err, &base) { switch base.Code { case "paylera.checkout_session_expired": return restartCheckout() case "paylera.feature_gated": return showUpgradePrompt() }}Webhook handler errors
The webhook subpackage’s HandlerFunc returns a plain error. A
non-nil return turns into a 500 so Paylera retries; nil returns 200.
That’s the entire interface — no typed webhook errors, because the
choice is binary:
func handleInvoicePaid(ctx context.Context, ev webhook.Event) error { var data webhook.DataInvoicePaid if err := ev.DecodeData(&data); err != nil { return err // 500, Paylera retries } if err := recordPayment(ctx, ev.ID, data); err != nil { if errors.Is(err, errAlreadyProcessed) { return nil // 200, already deduped on ev.ID — don't retry } return err // 500, transient — retry is correct } return nil}