# Idempotency

> Send an Idempotency-Key with POST /emails and /emails/batch to retry safely. The response is replayed for 24 hours; a different body with the same key gets 409.

Source: https://docs.pidgely.com/idempotency/

Networks fail. If a send times out you can't know whether Pidgely received it, and retrying blindly can deliver the same email twice. An **idempotency key** makes the retry safe: Pidgely stores the first response and replays it for every retry with the same key.

```js tab="Node.js"
import { Pidgely } from 'pidgely';

const pidgely = new Pidgely(process.env.PIDGELY_API_KEY);

const { data, error } = await pidgely.emails.send({
  from: 'Acme <orders@acme.com>',
  to: ['steve@example.com'],
  subject: 'Order confirmed',
  html: '<p>Thanks!</p>',
}, {
  idempotencyKey: 'order-1234/confirmation',
});
console.log(data ?? error);
```

```python tab="Python"
import os
import pidgely

pidgely.api_key = os.environ["PIDGELY_API_KEY"]

result = pidgely.Emails.send({
    "from": "Acme <orders@acme.com>",
    "to": ["steve@example.com"],
    "subject": "Order confirmed",
    "html": "<p>Thanks!</p>",
}, {
    "idempotency_key": "order-1234/confirmation",
})
print(result)
```

```php tab="PHP"
<?php

require __DIR__ . '/vendor/autoload.php';

$pidgely = Pidgely::client(getenv('PIDGELY_API_KEY'));

$result = $pidgely->emails->send([
    'from' => 'Acme <orders@acme.com>',
    'to' => ['steve@example.com'],
    'subject' => 'Order confirmed',
    'html' => '<p>Thanks!</p>',
], [
    'idempotency_key' => 'order-1234/confirmation',
]);
var_dump($result);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/pidgely/pidgely-go"
)

func main() {
	ctx := context.Background()
	client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY"))

	result, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{
		From:    "Acme <orders@acme.com>",
		To:      []string{"steve@example.com"},
		Subject: "Order confirmed",
		Html:    "<p>Thanks!</p>",
	}, pidgely.WithIdempotencyKey("order-1234/confirmation"))
	if err != nil {
		panic(err)
	}
	fmt.Println(result)
}
```

```ruby tab="Ruby"
require "pidgely"

Pidgely.api_key = ENV["PIDGELY_API_KEY"]

result = Pidgely::Emails.send({
  from: "Acme <orders@acme.com>",
  to: ["steve@example.com"],
  subject: "Order confirmed",
  html: "<p>Thanks!</p>"
}, {
  idempotency_key: "order-1234/confirmation"
})
puts result
```

```bash tab="cURL"
curl -X POST 'https://api.pidgely.com/emails' \
  -H "Authorization: Bearer $PIDGELY_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-1234/confirmation' \
  -d '{
  "from": "Acme <orders@acme.com>",
  "to": [
    "steve@example.com"
  ],
  "subject": "Order confirmed",
  "html": "<p>Thanks!</p>"
}'
```

With the Node.js SDK, pass it as an option: `pidgely.emails.send(params, { idempotencyKey: 'order-1234/confirmation' })`. The SDK then also retries failed sends automatically.

## How it works

| | Behaviour |
|---|---|
| Endpoints | `POST /emails`, `POST /emails/batch` (and `/v1/emails`, `/v1/emails/batch`). Over [SMTP](https://docs.pidgely.com/guides/smtp/#special-headers), use the `Pidgely-Idempotency-Key` header. |
| Key | 1 to 256 characters; longer keys get `400 invalid_idempotency_key`. An empty header means no idempotency. |
| Scope | Per account: two API keys of the same account share the key space. |
| Lifetime | 24 hours from the first request. |
| Same key, same request | The stored status code and body are returned, with the header `Idempotent-Replayed: true`. Nothing is sent again. |
| Same key, different request | `409 invalid_idempotent_request` (`Idempotency-Key was used with a different request`). The request is compared by method, path and exact body. |
| Errors | `4xx` responses are stored and replayed like successes. `5xx` responses are not stored, so a retry runs the request again. |

## Choosing keys

Derive the key from the business event that triggers the email, not from the attempt:

- `user-42/welcome`, `order-1234/confirmation`, `invoice-2026-10/acme`: retries of the same event reuse the key, a different event gets a different key.
- A random UUID generated **once per event** and stored with it works too. A new UUID per retry defeats the purpose.

> [!WARNING]
> Two requests with the same new key that arrive at exactly the same time are not serialized: both can run. Retry after a timeout or an error, not in parallel.

## Retry recipe

1. Send with an idempotency key.
2. On a timeout, a connection error, `429` or `5xx`, retry with the **same key and the same body**, with exponential backoff.
3. A replayed success has `Idempotent-Replayed: true` and the original email id.
4. Don't retry other `4xx` errors unchanged; see [Errors](https://docs.pidgely.com/errors/#retrying).
