Skip to content

Idempotency and retries

Paylera.Sdk ships sensible defaults for both idempotency-key stamping and HTTP retries. You get safe behaviour without configuration; you can override both when you need to.

Idempotency-Key auto-stamping

Every state-changing call (POST, PUT, PATCH, DELETE) carries an Idempotency-Key header. If the caller doesn’t supply one, the SDK stamps a fresh UUID v7 in the auth handler before the request leaves the process.

// Auto-stamped: the SDK generates a UUID v7 for you.
await client.Api.CreateCustomerAsync(idempotency_Key: Guid.NewGuid().ToString(), body);
// Explicit: pass your own key for client-side deduplication.
var key = $"signup:{userId}";
await client.Api.CreateCustomerAsync(idempotency_Key: key, body);

Why UUID v7

UUID v7 embeds a millisecond timestamp in the high bits, so:

  • Keys sort naturally by creation time, which keeps Paylera’s internal idempotency cache index-friendly.
  • Burst-generation collisions are vanishingly unlikely — 12 bits of per-millisecond randomness plus 62 bits of high-entropy randomness.
  • Logs sort the way humans expect, which makes “find this call by request id” cheaper at incident time.

If you’re on a stack without native UUID v7, the SDK provides PayleraIdempotency.NewKey() which emits a string of the right shape:

var key = PayleraIdempotency.NewKey();
// "01985f3c-9af3-7e5b-bdc1-30f0c6210e1f"

Idempotency on the wire

Paylera’s idempotency middleware caches the response for 24 hours. A retry with the same key returns the cached response — 200 OK is the success path, 409 Conflict is what you get when the same key is replayed with a different body (treat that as a programming bug).

The typed-exception hierarchy maps these to:

StatusException
409 with paylera.idempotency_conflictPayleraIdempotencyConflictException
409 with paylera.idempotency_in_progressPayleraIdempotencyInProgressException

IdempotencyInProgressException means another request with the same key is still executing — retry after a short delay.

Retries

A Polly v8 pipeline is wired into the SDK’s HttpClientFactory registration. Defaults:

ConditionBehaviour
HttpRequestException (transport)retry
408 Request Timeoutretry
429 Too Many Requestsretry, honouring Retry-After
5xxretry
4xx other than 408/429no retry
Max attempts5 (1 initial + 4 retries)
Backoffexponential with full jitter, starting at 100 ms

The retry handler sits inside the problem-details handler. By the time your try/catch sees a typed PayleraException, retries are done — you get the final outcome, not a transient 503 mid-pipeline.

Why retries respect idempotency keys

Because the SDK stamps an idempotency key on every state-changing call, retries are safe by construction: Paylera’s idempotency cache returns the original response on retry rather than performing the side effect a second time.

The combination — auto-stamped UUID v7 keys + Polly retry on 5xx + 24-hour server-side cache — is what makes “fire and forget” semantics safe in practice. You don’t need to write your own retry loop and you don’t need to worry about double-charging.

Configuring the pipeline

builder.Services.AddPaylera(opts =>
{
opts.ApiToken = configuration["Paylera:ApiToken"];
opts.Timeout = TimeSpan.FromSeconds(45); // default 30s
// Tune the Polly pipeline; nulls leave defaults intact.
opts.Retry.MaxAttempts = 7;
opts.Retry.BaseDelay = TimeSpan.FromMilliseconds(250);
opts.Retry.MaxDelay = TimeSpan.FromSeconds(10);
});

To disable automatic retries (you have a smarter outer loop):

opts.Retry.MaxAttempts = 0;

Caller-supplied keys for natural deduplication

The most useful pattern is to derive an idempotency key from the intent, not from a counter or a Guid.NewGuid() at the call site:

// Same logical signup, regardless of how many times the user mashes the button.
var key = $"signup:{userId}";
await client.Api.CreateSubscriptionAsync(key, body);

This collapses retries across process restarts, queue redeliveries, and client refreshes. Paylera’s cache TTL is the upper bound: a key is good for 24 hours, after which a fresh request with the same key is treated as a new call.

Webhook handler idempotency

The receiver in Paylera.AspNetCore does not deduplicate inbound deliveries — Paylera retries every delivery exactly once per failure, so handlers must be idempotent on event.Id. Pattern:

opts.On<InvoicePaidEvent>("invoice.paid", async (ev, ctx) =>
{
if (!await _seenEvents.TryAddAsync(ev.Id, ctx.RequestAborted))
{
return; // Already processed; ack as 200.
}
await _fulfilment.MarkPaidAsync(ev.Id, ctx.RequestAborted);
});

The de-dup store can be a database table with a unique constraint on event_id, a Redis SET NX, or whatever fits your stack. Idempotency on the receiving side is your responsibility — but the SDK gives you the event.Id to key off, every time.