> ## Documentation Index
> Fetch the complete documentation index at: https://tapace.lk/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and retries

> Handle TapAce SDK errors, understand timeout and retry defaults, and reuse stable idempotency keys when your application repeats an operation after failure.

The TapAce SDK retries temporary request failures during a call. Once those attempts are exhausted, your application receives an error and decides what to do next.

## Retry behavior

| Outcome | SDK behavior |
| - | - |
| Network exception or timeout | Retry while attempts remain. |
| HTTP 5xx | Retry while attempts remain. |
| HTTP 429 | Retry using numeric Retry-After seconds when available; each such wait is capped at 30 seconds. |
| HTTP 409 idempotency conflict, reason in\_flight | Retry while attempts remain. |
| Key reused with a different body | Throw TapAceIdempotencyError, reason key\_reused. |
| Invalid JSON in a successful response | Throw TapAceError without retrying the request. |
| Other API errors, including other 409 conflicts | Throw the corresponding error without automatic retry. |

Defaults are three retries after the initial attempt and a 30-second timeout per attempt. Ordinary retry waits use exponential backoff with jitter: roughly 300–400 ms, 600–800 ms and 1200–1600 ms. Exponential backoff is capped at 30 seconds before jitter, including when you configure more retries. There is no single overall call deadline. Set `maxRetries: 0` to disable retries.

## Automatic and stable keys

The SDK generates a random key once for each POST, PATCH, PUT or DELETE call unless you supply one. It reuses that key and the same serialized request body across internal retries. A separate call generates a different key.

For an operation your application might repeat, derive a stable key from a saved operation ID:

```typescript theme={null}
import { TapAce, idempotencyKey, type CreatePassParams } from "@tapace/node";

async function issueRegistration(
  tapace: TapAce,
  registrationId: string,
  params: CreatePassParams,
) {
  return tapace.passes.create(params, {
    idempotencyKey: idempotencyKey("membership-pass", registrationId),
  });
}
```

Same helper inputs, in the same order and with the same types, produce the same key after a restart. The helper accepts strings, finite numbers and booleans, requires at least one part and rejects blank strings. It hashes the parts rather than placing raw identifiers in the header.

Use an operation ID, not just a customer ID: one customer can legitimately receive several passes or points awards. Keep the request details unchanged when retrying.

The API's [idempotency guide](https://tapace.lk/docs/idempotency.md) documents seven-day retention. After that window, reconcile the original operation before issuing another request; a stable key is not permanent duplicate protection. An in-flight conflict may outlast SDK retries. See [Retry after a restart](#retry-after-a-restart).

> [!NOTE]
>
> Sending a key and enforcing idempotency are separate. Stored-response replay applies to POST, PATCH and DELETE API requests. PUT event and signing requests do not currently provide the same replay protection. Read their current state before deciding how to repeat a change.

## Error classes

Transport, configuration, API and webhook errors extend `TapAceError`. Invalid inputs to `idempotencyKey()` throw `TypeError`; a request body that cannot be JSON-serialized can also throw `TypeError` before any HTTP request. API errors extend `TapAceAPIError` and expose `status`, `type` and optional `code`, `issues` and `requestId`.

| Class | Meaning |
| - | - |
| `TapAceAuthError` | HTTP 401 or 403. |
| `TapAceNotFoundError` | HTTP 404. |
| `TapAceConflictError` | HTTP 409. |
| `TapAceIdempotencyError` | Idempotency conflict; extends TapAceConflictError. |
| `TapAceRateLimitError` | HTTP 429 after retries; may include retryAfterSeconds. |
| `TapAceWebhookSignatureError` | Local webhook verification failure. |
| `TapAceAPIError` | Other API error responses. |
| `TapAceError` | Includes local configuration errors and exhausted transport failures. |

Check the more specific subclass first:

```typescript theme={null}
import {
  TapAce, TapAceAPIError, TapAceIdempotencyError, TapAceError,
} from "@tapace/node";

async function checkAccount(tapace: TapAce) {
  try {
    return await tapace.me.retrieve();
  } catch (error) {
    if (error instanceof TapAceIdempotencyError) {
      console.error("Idempotency conflict:", error.reason);
    } else if (error instanceof TapAceAPIError) {
      console.error("TapAce API error:", error.status, error.code, error.requestId);
    } else if (error instanceof TapAceError) {
      console.error("TapAce request error:", error.message);
    }
    throw error;
  }
}
```

A timeout does not prove the server rejected the operation. Retry a mutation with the original stable key rather than generating a new one.

## Keep separate keys for separate changes

For loyalty credits, derive the key from a saved sale or award ID. Retrying a two-stamp award must reuse its original key and `{ add: 2 }` body; a second purchase needs a different key. The same rule applies to points, notifications and deployments.

## Retry after a restart

A key is not a job queue. When the process exits, the SDK loses its in-memory request and does not discover unfinished work. To retry across restarts, save the request before sending it, then resend it with the same key and body.

| Failure after SDK retries | Action |
| - | - |
| Timeout, network error or temporary 5xx | Retry with the same key and body. |
| Rate limit | Delay the retry; inspect retryAfterSeconds if available. |
| Idempotency in\_flight | Delay the retry with the same key and body. |
| Key reused with different body | Stop and investigate the operation/key mismatch. |
| Invalid input, invalid key or missing resource | Correct or reconcile the problem before retrying. |
| Other conflict | Inspect its cause; do not assume every conflict is temporary. |

How a retry behaves depends on what happened to the original request:

* **TapAce finished it before your process died:** within retention, the same key and body replay the saved response.
* **It died partway through:** the key stays in flight until ten minutes after it was reserved, then a request with the same key and body can take over. The ten-minute mark does not schedule anything, and default SDK retries finish much sooner, so your own retries must continue past it.
* **It is more than seven days old:** the key may be gone, so check your saved pass ID or other records before repeating it. An `externalId` helps find passes but is not unique.

This recovery behavior does not guarantee that every mutation runs exactly once.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.