# Idempotentní požadavky

> Hlavičkou Idempotency-Key bezpečně opakujte požadavky na odeslání a přeposlání. Formát klíče, 24hodinové okno, opakované odpovědi, odpovědi 409 a 503 a volba klíčů.

Sítě selhávají. Když u požadavku na odeslání vyprší časový limit, nevíte, jestli ho Emailit přijal, a když ho odešlete znovu, může váš zákazník dostat e-mail dvakrát. Hlavička `Idempotency-Key` dělá opakování bezpečným: Emailit zpracuje první požadavek a na každé opakování se stejným klíčem vrátí stejnou odpověď.

## Jak to funguje

Přidejte hlavičku `Idempotency-Key` k `POST /emails` nebo `POST /emails/{id}/forward`.

1. **První požadavek.** Emailit klíč pro váš workspace zarezervuje a požadavek zpracuje.
2. **Úspěch.** Emailit uloží odpověď na 24 hodin. Každý požadavek se stejným klíčem v tomto okně dostane uloženou odpověď s `200` a žádný nový e-mail se nevytvoří.
3. **Selhání.** Pokud požadavek selže, například s `400` nebo `402`, Emailit klíč uvolní. Opravte problém a požadavek zopakujte se stejným klíčem.
4. **Překryv.** Pokud dorazí druhý požadavek, zatímco první ještě probíhá, dostane `409` a nic se neodešle. Za chvíli ho zopakujte se stejným klíčem.

Klíče platí v rámci vašeho workspace, takže dva workspace mohou použít stejný klíč bez konfliktu.

> **Požadavek určuje klíč, ne tělo:** Emailit těla požadavků neporovnává. Opakování se stejným klíčem vrátí původní odpověď, i když je tělo jiné. Pro každý jiný e-mail použijte nový klíč.

## Formát klíče

| Pravidlo | Hodnota |
| --- | --- |
| Délka | 1 až 256 znaků |
| Znaky | Písmena `A–Z` a `a–z`, číslice `0–9`, spojovník `-` a podtržítko `_` |
| Platnost | Pro každý workspace zvlášť |
| Okno | 24 hodin od první úspěšné odpovědi |

Klíč s jinými znaky, například `:` nebo `/`, se odmítne s `400 Invalid Idempotency-Key`.

## Zvolte klíč

Odvoďte klíč od události, která e-mail způsobuje, aby každá cesta opakování vytvořila stejný klíč:

| E-mail | Příklad klíče |
| --- | --- |
| Potvrzení objednávky | `order-1042-receipt` |
| Obnovení hesla | `password-reset-7f3c9a1e` (ID tokenu pro obnovení) |
| Týdenní přehled | `digest-user-881-2026-w40` |
| Úloha na pozadí | ID úlohy, nebo UUID, které vygenerujete při zařazení úlohy do fronty a uložíte s ní |

Vyhněte se klíčům, které se mezi pokusy mění, například časovým razítkům nebo UUID generovanému uvnitř smyčky opakování. Kvůli nim vypadá každé opakování jako nový požadavek.

## Odešlete e-mail s idempotenčním klíčem

Příklady pro Node.js, Python a PHP opakují požadavek při chybách sítě a při odpovědích `409`, `429` a `5xx` a pokaždé použijí stejný klíč. Příklad pro cURL využívá vestavěné opakování curl, které pokrývá vypršení časového limitu, `429` a většinu odpovědí `5xx`.

**cURL**

```bash
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-receipt" \
  --retry 3 \
  -d '{
    "from": "Acme <orders@acme.com>",
    "to": "ada@example.com",
    "subject": "Receipt for order 1042",
    "text": "Thanks for your order."
  }'
```

**Node.js**

```javascript
async function sendOnce(payload, key, attempts = 4) {
  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      const res = await fetch('https://api.emailit.com/v2/emails', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': key,
        },
        body: JSON.stringify(payload),
      });
      if (res.ok) return res.json();
      if (![409, 429].includes(res.status) && res.status < 500) {
        throw new Error(`Send failed: ${res.status} ${await res.text()}`);
      }
      const wait = Number(res.headers.get('retry-after')) || attempt * 2;
      await new Promise((r) => setTimeout(r, wait * 1000));
    } catch (err) {
      if (err.message.startsWith('Send failed') || attempt === attempts) throw err;
      await new Promise((r) => setTimeout(r, attempt * 2000));
    }
  }
  throw new Error('Send failed after retries');
}

const email = await sendOnce(
  {
    from: 'Acme <orders@acme.com>',
    to: 'ada@example.com',
    subject: 'Receipt for order 1042',
    text: 'Thanks for your order.',
  },
  'order-1042-receipt',
);
```

**Python**

```python
import os
import time
import requests

def send_once(payload, key, attempts=4):
    for attempt in range(1, attempts + 1):
        try:
            res = requests.post(
                "https://api.emailit.com/v2/emails",
                headers={
                    "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}",
                    "Idempotency-Key": key,
                },
                json=payload,
                timeout=30,
            )
        except requests.RequestException:
            if attempt == attempts:
                raise
            time.sleep(attempt * 2)
            continue
        if res.ok:
            return res.json()
        if res.status_code not in (409, 429) and res.status_code < 500:
            res.raise_for_status()
        time.sleep(int(res.headers.get("retry-after", attempt * 2)))
    raise RuntimeError("Send failed after retries")

email = send_once(
    {
        "from": "Acme <orders@acme.com>",
        "to": "ada@example.com",
        "subject": "Receipt for order 1042",
        "text": "Thanks for your order.",
    },
    "order-1042-receipt",
)
```

**PHP**

```php
function sendOnce(array $payload, string $key, int $attempts = 4): array
{
    for ($attempt = 1; $attempt <= $attempts; $attempt++) {
        $ch = curl_init('https://api.emailit.com/v2/emails');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . getenv('EMAILIT_API_KEY'),
                'Content-Type: application/json',
                'Idempotency-Key: ' . $key,
            ],
            CURLOPT_POSTFIELDS => json_encode($payload),
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        if ($body !== false && $status >= 200 && $status < 300) {
            return json_decode($body, true);
        }
        if ($body !== false && !in_array($status, [409, 429]) && $status < 500) {
            throw new RuntimeException("Send failed: $status $body");
        }
        sleep($attempt * 2);
    }
    throw new RuntimeException('Send failed after retries');
}

$email = sendOnce([
    'from' => 'Acme <orders@acme.com>',
    'to' => 'ada@example.com',
    'subject' => 'Receipt for order 1042',
    'text' => 'Thanks for your order.',
], 'order-1042-receipt');
```

## Odpovědi

| Stav | Kdy | Co dělat |
| --- | --- | --- |
| `200` | První úspěšný požadavek nebo jeho opakování do 24 hodin | Použijte odpověď. Opakovaná odpověď má stejné tělo včetně stejného `id`. |
| `400 Invalid Idempotency-Key` | Klíč je prázdný, příliš dlouhý, nebo obsahuje neplatné znaky | Opravte klíč. |
| `409 Idempotency key in progress` | Požadavek se stejným klíčem se ještě zpracovává | Chvíli počkejte a pak požadavek zopakujte se stejným klíčem. |
| `503 Idempotency unavailable` | Emailit se nemohl spojit se svým úložištěm idempotenčních klíčů, a tak požadavek raději odmítl, než aby riskoval duplicitu | Zopakujte požadavek se stejným klíčem. |
| Jakákoli jiná chyba | Požadavek selhal a klíč se uvolnil | Odstraňte příčinu a zopakujte požadavek se stejným klíčem. |

Limity rychlosti se kontrolují před klíčem, takže i opakování může dostat `429`. Počkejte podle hlavičky `retry-after` a odešlete stejný klíč znovu.

## Související

- [Idempotence](/cs/docs/api-reference/idempotency/) v referenci API
- [Odeslání e-mailu](/cs/docs/email-api/send-email/)
- [Přeposlání e-mailu](/cs/docs/email-api/retry-and-forward/)
- [Limity rychlosti](/cs/docs/api-reference/rate-limits/)
- [Proč se mé e-maily odesílají dvakrát?](/cs/docs/kb/duplicate-emails-sent/)

---
Zdroj: https://emailit.com/cs/docs/email-api/idempotency/
