Skip to main content
Every failure @endstate-sdk/core raises is a typed error carrying the information support needs. Retry behaviour is not guesswork either: it is derived from the API spec at build time, so a call is retried only when repeating it is safe.

Branch on the code

Branch on error.code, never on message or the HTTP status. Several codes share one status. Codes are added over time, so treat an unrecognized one as a generic failure rather than throwing.
Narrow to specific codes with isEndstateError(error, ...codes):

The error types

All extend EndstateError, which carries requestId, operationId, and attempts. Log requestId on every failure - it is the fastest way for support to find your request.

Which calls are retried

Retry eligibility comes from the spec, not from the method name. Every operation falls into one of three classes. Two consequences worth internalizing: A keyed write is never retried on a 5xx. The API clears its idempotency record on any non-2xx, so a same-key retry would execute a second time instead of replaying the first. The SDK will not do that for you. Writes are never repeated at all. units.update, collections.update, settings.update, settings.corsOrigins.replace, sessionTokens.revoke, and testHelpers.createTap accept no idempotency key, so the API cannot deduplicate them and a second send is a second write. Raising maxAttempts does not change this, and that is not a bug. The single exception is 429 rate_limit.exceeded, which the API refuses before any handler runs, so nothing was written.

Where the idempotency key comes from

For a keyed create, the SDK sends an Idempotency-Key on your behalf. Which key it sends determines whether a later run is safe, and error.safeToRetry tells you which case you are in.
This matters when a call fails without an answer. EndstateNetworkError and EndstateTimeoutError - the two cases where you cannot know whether the write landed - carry safeToRetry and the idempotencyKey that was used. safeToRetry means “calling again recovers this same operation”. It is true for a read, and for a write whose key is stable across calls - one you supplied, or the one verify() derives from the tap. It is false when the SDK generated the key, because a fresh call would generate a different one and the API would treat it as a second write. Pass the key back to recover the original:
When the same logical create can be issued by a later run - a nightly sync, a job that may be replayed - supply your own key. The resource’s external_id or a job id is usually the right choice, so the second run replays the first response instead of creating a second resource.

Tuning it

Retries use equal-jitter exponential backoff and honour Retry-After. Configure per client or per call:
DEFAULT_RETRY_POLICY is what applies when you set none - spread it to change one field without restating the rest:
Observe what is happening with onRequest, onResponse, and onRetry:
reason is one of network, timeout, rate_limited, server_error, or idempotency_in_progress.