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

# Retry safely without sending duplicate faxes

> Idempotency keys, retry logic, and exponential backoff

## Outcome

You will be able to attach idempotency keys to fax requests, distinguish which failures mintfax retries from which ones your code retries, and build backoff logic that does not stampede the API when something goes wrong.

## Prerequisites

* A mintfax account with a sandbox API key (`mfx_test_...`)
* curl or the `mintfax` Node package

## What is idempotent and what is not

`GET`, `PUT`, and `DELETE` endpoints are idempotent by design. Repeating them produces the same result.

`POST` endpoints create resources. Without an idempotency key, each `POST` creates a new fax. With one, mintfax treats the second request as a replay and returns the stored response.

| Method   | Idempotent by default | Idempotency key supported |
| -------- | --------------------- | ------------------------- |
| `GET`    | Yes                   | No (not needed)           |
| `PUT`    | Yes                   | No (not needed)           |
| `DELETE` | Yes                   | No (not needed)           |
| `POST`   | No                    | Yes                       |

## Step 1: Generate an idempotency key

Generate a UUID (or any unique string up to 255 characters) on the client before you send the request. The key should stay the same across retries of the same operation.

One approach: hash the recipient number, a document fingerprint, and a reference ID from your system. That way, retrying the same logical fax always produces the same key.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.mintfax.com/v1/faxes \
    -H "Authorization: Bearer mfx_test_abc123def456" \
    -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
    -F "to=+12015550100" \
    -F "file=@document.pdf"
  ```

  ```javascript node theme={null}
  import Mintfax from 'mintfax';

  const client = new Mintfax('mfx_test_abc123def456');

  const fax = await client.fax.send(
    {
      to: '+12015550100',
      file: './document.pdf',
    },
    {
      idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
    }
  );
  ```
</CodeGroup>

**Verify:** Send the same request twice with the same key. The second response returns the same fax ID and `queued` status as the first, with no duplicate fax created.

## Step 2: Understand key scope and TTL

Each idempotency record is keyed by your **account ID** plus the key you provide.

* Two different accounts can use the same key string without collision.
* Sandbox and live environments are separate scopes, so keys do not cross over.
* Keys expire after **24 hours**. After that, the same string can be reused.

mintfax does not compare request bodies. If you reuse a key that has not expired, you get back the original response regardless of what the new body contains. Use one key per logical operation.

## Step 3: Detect a replay

mintfax tells you whether a `POST` response was newly processed or returned from the idempotency cache, so retry logic can distinguish a fresh result from a deduped one without inspecting the response body.

Two response headers carry the signal:

| Header                 | When set                                                     | Value                              |
| ---------------------- | ------------------------------------------------------------ | ---------------------------------- |
| `X-Idempotency-Key`    | Set when the request supplied `Idempotency-Key`.             | The exact string from the request. |
| `Idempotency-Replayed` | Set only when this response came from the idempotency cache. | The literal string `true`.         |

If you did not supply an `Idempotency-Key` on the request, neither header appears. If you supplied a key and this is the first request with it, `X-Idempotency-Key` echoes back and `Idempotency-Replayed` is absent. If you supplied a key and the server has a cached response for it, both headers are set.

Treat `Idempotency-Replayed: true` as an authoritative signal that no side effect re-ran. The fax was queued only once; the response you are reading is a snapshot of that first attempt.

For production integrations, supply an `Idempotency-Key` on every `POST`. Keys are optional but recommended.

## The retry contract

Two layers of retries exist in a mintfax integration. Knowing which layer owns what keeps your code simple.

### What mintfax retries for you

Once mintfax accepts a fax (HTTP 201), it owns delivery to the receiving machine. Busy signals, temporary line errors, negotiation failures - mintfax retries these automatically. The `retries` field controls how many attempts it makes (default 3, configurable 0-10 per fax or via account fax settings). You do not build retry logic for carrier failures.

Track delivery through [webhook events](/webhooks) (`fax.delivered`, `fax.failed`) or by polling `GET /fax/{id}`.

### What you retry yourself

Your code retries when the HTTP request fails before mintfax can accept the fax:

* Network errors (timeouts, connection resets, DNS failures)
* HTTP 500 or 503 responses

Do **not** retry these:

| Status | Meaning              | Action                                                              |
| ------ | -------------------- | ------------------------------------------------------------------- |
| 400    | Bad request          | Fix the request body. See [errors](/errors).                        |
| 401    | Unauthorized         | Check your API key.                                                 |
| 402    | Insufficient balance | Top up your balance before retrying.                                |
| 422    | Validation failed    | Fix the invalid fields. See [errors](/errors).                      |
| 429    | Rate limited         | Wait for the `Retry-After` header. See [rate limits](/rate-limits). |

## Step 4: Build retry logic with backoff and jitter

For retryable failures, use exponential backoff with jitter. Jitter adds a random offset so that many clients failing at the same time do not all retry on the same tick.

```
delay = min(base * 2^attempt + random_jitter, max_delay)
```

Use a base delay of 1 second, a max delay of 30 seconds, jitter between 0 and 1 second, and cap at 5 attempts.

<CodeGroup>
  ```bash curl theme={null}
  #!/bin/bash
  IDEMPOTENCY_KEY="550e8400-e29b-41d4-a716-446655440000"
  MAX_ATTEMPTS=5
  BASE_DELAY=1

  for attempt in $(seq 0 $((MAX_ATTEMPTS - 1))); do
    response=$(curl -s -w "\n%{http_code}" -X POST https://api.mintfax.com/v1/faxes \
      -H "Authorization: Bearer mfx_test_abc123def456" \
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
      -F "to=+12015550100" \
      -F "file=@document.pdf")

    http_code=$(echo "$response" | tail -1)

    if [ "$http_code" -eq 201 ] || [ "$http_code" -eq 200 ]; then
      echo "Success"
      break
    elif [ "$http_code" -eq 500 ] || [ "$http_code" -eq 503 ]; then
      delay=$(echo "$BASE_DELAY * (2 ^ $attempt) + $RANDOM / 32768" | bc -l)
      delay=$(echo "if ($delay > 30) 30 else $delay" | bc -l)
      echo "Attempt $((attempt + 1)) failed ($http_code). Retrying in ${delay}s..."
      sleep "$delay"
    else
      echo "Non-retryable error: $http_code"
      break
    fi
  done
  ```

  ```javascript node theme={null}
  import Mintfax from 'mintfax';

  const client = new Mintfax('mfx_test_abc123def456');

  async function sendWithRetry(params, idempotencyKey, maxAttempts = 5) {
    const baseDelay = 1000;
    const maxDelay = 30000;

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        return await client.fax.send(params, { idempotencyKey });
      } catch (err) {
        const status = err.statusCode;

        if (status && status !== 500 && status !== 503) {
          throw err; // non-retryable
        }

        if (attempt === maxAttempts - 1) {
          throw err; // last attempt
        }

        const jitter = Math.random() * 1000;
        const delay = Math.min(baseDelay * 2 ** attempt + jitter, maxDelay);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  const fax = await sendWithRetry(
    { to: '+12015550100', file: './document.pdf' },
    '550e8400-e29b-41d4-a716-446655440000'
  );
  ```
</CodeGroup>

**Verify:** Use the sandbox magic number `+15005550004` (transient failure) to confirm your retry logic recovers after the first attempt fails. See the [sandbox](/sandbox) page for the full magic number list.

## Detecting duplicates on the receiving side

If your downstream system processes the same webhook event twice, use the fax ID (`id` field in the response and the webhook payload) as your deduplication key. Store processed fax IDs and skip repeats.

## Key requirements

| Property           | Value                                           |
| ------------------ | ----------------------------------------------- |
| Max length         | 255 characters                                  |
| Recommended format | UUID v4                                         |
| Scope              | Account ID + key (no collision across accounts) |
| TTL                | 24 hours                                        |
| Body matching      | No (keyed only by account ID + key)             |

## Verify

Send a fax with an idempotency key using your sandbox API key. Repeat the request. The second response should return the same fax ID with no duplicate created. Then run your retry logic against `+15005550004` to confirm backoff works.

## What to do next

* [Error catalog](/errors) - see which errors are retryable and which require a fix
* [Rate limits](/rate-limits) - understand rate-limit headers and how they interact with retries
* [Sandbox](/sandbox) - test retry flows with magic fax numbers
