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

# Idempotency

> Safely retry requests with idempotency keys

Idempotency allows you to safely retry requests without causing duplicate operations. This is essential for reliable integrations, especially for create and update operations.

## How It Works

Include an `Idempotency-Key` header with a unique value for each logical operation:

```bash theme={null}
curl -X POST "https://api.widgetic.com/v1/widgets" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-widget-user123-1705317600" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Widget"}'
```

## Behavior

1. **First Request**: Processed normally. A successful (`2xx`) response is stored, encrypted, for 24 hours
2. **Retry with Same Key**: Returns the stored response with an `X-Idempotency-Cached: true` header. The operation is **not** run again
3. **Retry While the First Is Still Running**: Returns `409 Conflict` with a `Retry-After` header. The operation is not run a second time; wait, then retry with the **same** key (see [Retrying a request that is still running](#retrying-a-request-that-is-still-running))
4. **Retry After a Failure**: Only successful responses are stored. If the first attempt failed (any non-`2xx` response), retrying with the same key runs the operation again
5. **Retry When the Stored Response Is Gone**: In the rare case the first request is known to have succeeded but its response can no longer be returned, the retry returns `409 Conflict` and the operation is **not** run again (see [When the stored response is no longer available](#when-the-stored-response-is-no-longer-available))
6. **Same Key, Different Body**: Returns `422`. A key names exactly one request
7. **Different Key**: Processed as new request

## What Counts as the Same Request

A key only matches a request with the same:

* **Account** (or organization, for organization API keys)
* **HTTP method**
* **URL**, including path parameters and query string

The same key sent to a different endpoint, or by a different account, is a different request. Your keys can never collide with another customer's, so you don't need to add a namespace of your own.

## Supported Methods

A call takes a key when the [API Reference](https://widgetic.com/api-reference/introduction.md) lists `Idempotency-Key` among its parameters, and the reference says so for every call that does, including the `PUT` and `PATCH` calls that change something. Where a call requires a key, omitting the header returns `400`.

`GET` requests are naturally idempotent and don't need keys, and most deletions don't take one either. The call that makes an [editor session](https://widgetic.com/docs/partners/editor-sessions.md) takes none: its answer holds a token that is never stored where a retry could read it back, so a retry makes a new session.

## Key Requirements

* **Unique per operation**: Use a combination of action + context + timestamp
* **Client-generated**: You control the key generation
* **Format**: 8 to 50 characters — letters, digits, dashes and underscores (a UUID works well)
* **Expiration**: Stored responses expire after 24 hours

## Generating Good Keys

```typescript theme={null}
// Include context that makes the key unique
const idempotencyKey = [
  'create-widget',
  userId,
  Date.now(),
  randomString(8)
].join('-');

// Example: create-widget-user123-1705317600-a1b2c3d4
```

## Example Implementation

```typescript theme={null}
import { WidgetsApi, ResponseError } from '@widgetic/api-sdk-ts';

const widgets = new WidgetsApi('YOUR_API_KEY');

async function createWidgetSafely(name: string, userId: string) {
  const idempotencyKey = `create-widget-${userId}-${Date.now()}`;
  
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await widgets.createWidget({
        newWidget: { name, type: 'chart', content: {} },
        idempotencyKey
      });
    } catch (error) {
      // The SDK throws a ResponseError for an answer that is not a success; the status is on its response
      if (error instanceof ResponseError && error.response.status >= 500) {
        // Server error - safe to retry with same key
        await sleep(1000 * Math.pow(2, attempt));
        continue;
      }
      throw error; // Client error - don't retry
    }
  }
}
```

## Retrying a Request That Is Still Running

If your client times out and retries while the first request is still being processed, the retry is refused instead of running the operation twice:

```http theme={null}
HTTP/1.1 409 Conflict
Retry-After: 2
```

```json theme={null}
{
  "success": false,
  "error": {
    "code": "CONFLICT_IDEMPOTENCY_IN_PROGRESS",
    "message": "A request with this idempotency key is still being processed; retry once it has finished",
    "retryAfter": 2
  }
}
```

Wait for `Retry-After` seconds and send the request again with the **same** key. You will receive either the stored response (the first request succeeded) or a fresh run (the first request failed and released the key). Do not generate a new key: that would run the operation twice.

```typescript theme={null}
async function postWithRetry(url: string, body: unknown, idempotencyKey: string) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey, // same key on every attempt
      },
      body: JSON.stringify(body),
    });

    if (response.status === 409) {
      const { error } = await response.clone().json();
      if (error?.code === 'CONFLICT_IDEMPOTENCY_IN_PROGRESS') {
        const waitSeconds = Number(response.headers.get('Retry-After') ?? 2);
        await sleep(waitSeconds * 1000);
        continue;
      }
    }
    return response;
  }
  throw new Error('The original request is taking longer than expected');
}
```

> [!NOTE]
>
> A request that is interrupted (for example, the server restarts mid-request) frees its key within about a minute, so a retry is never blocked for long.

## When the Stored Response Is No Longer Available

A stored response is kept encrypted, and only for the request that produced it. In the rare case the first request is known to have succeeded but its response can no longer be returned (for example, it was written while our storage was being reconfigured), the retry is refused instead of running the operation a second time:

```http theme={null}
HTTP/1.1 409 Conflict
X-Idempotency-Cached: true
```

```json theme={null}
{
  "success": false,
  "error": {
    "code": "CONFLICT_IDEMPOTENCY_ANSWER_UNAVAILABLE",
    "message": "This request was already carried out under this idempotency key, but its answer was not kept, so it cannot be replayed",
    "details": {
      "conflictType": "answer_unavailable",
      "originalStatusCode": 201
    }
  }
}
```

There is no `Retry-After`: waiting does not help, and the same key will never return that response. `details.originalStatusCode` is the status the first attempt answered, when it is known. Do not send the request again under the same key.

* **Read what the request changed.** If it created something (a widget, an API key), list or fetch it instead of creating it again. A response that showed a secret once, such as a new API key, cannot be shown again: create a new one and remove the old one.
* **Repeat it under a new key** only when repeating is safe, that is, when running the operation twice would not do harm.

```typescript theme={null}
if (response.status === 409) {
  const { error } = await response.clone().json();
  if (error?.code === 'CONFLICT_IDEMPOTENCY_ANSWER_UNAVAILABLE') {
    // The operation ran once. Look at its result; do not retry with this key.
    return readCurrentState();
  }
}
```

## When to Use Idempotency

| Scenario | Use Idempotency? |
| - | - |
| Creating resources | ✅ Yes |
| Updating resources | ✅ Yes |
| Processing payments | ✅ Yes |
| Reading resources | ❌ No (GET is idempotent) |
| Network retry | ✅ Yes |
| User retry (new intent) | ❌ No (use new key) |

## Common Pitfalls

> [!WARNING]
>
> **Don't reuse keys across different operations**. Each unique business operation should have its own key.

```typescript theme={null}
// ❌ Bad - same key for different widgets
const key = 'my-key-0001';
await createWidget('Widget A', key);
await createWidget('Widget B', key); // Rejected (422): key already used with a different body

// ✅ Good - unique key per operation
await createWidget('Widget A', 'key-widget-a');
await createWidget('Widget B', 'key-widget-b');
```

> [!WARNING]
>
> If the request body is identical as well, a reused key returns the **first** response again and nothing new is created. A key that is easy to repeat, such as a counter or a bare timestamp, can therefore silently turn a second, separate operation into a replay. Use a UUID.


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