Skip to content

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

TypeHTTPWhen
*paylera.IdempotencyConflictError409same key, different body
*paylera.IdempotencyInProgressError409same key, original still processing
*paylera.AuthenticationError401missing / malformed / revoked API token
*paylera.AuthorizationError403valid token, wrong scope (e.g. test token → live resource)
*paylera.NotFoundError404resource does not exist
*paylera.PreconditionFailedError412optimistic-concurrency mismatch on resource version
*paylera.RateLimitError429exceeded rate limit; RetryAfter carries the parsed Retry-After
*paylera.ServerError5xxupstream 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.Error
if 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.PreconditionFailedError
if 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.Error
if 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
}

See also