# Idempotency

> Safely retry BloomText API writes with the Idempotency-Key header.

Source: https://www.bloomtext.com/developers/api/idempotency/

Networks fail. A request can time out after BloomText has already sent your message. Idempotency keys let you retry any write without sending it twice.

Every `POST` request requires an `Idempotency-Key` header. Generate a new UUID for each logical write, and reuse the same key when you retry that write.

```http filename="Header"
Idempotency-Key: 9f1c2b3a-7d4e-4f5a-8b6c-1d2e3f4a5b6c
```

## How it works

| You send | BloomText returns |
| --- | --- |
| A new key | Processes the request normally and remembers the result for 24 hours. |
| The same key, method, path, and body within 24 hours | The original response, without running the write again. |
| The same key with a different method, path, or body | `409 Conflict` with `code: idempotency_key_reused`. |
| The same key while the first request is still running | `409 Conflict` with `code: idempotency_request_in_progress`. Retry shortly. |

`DELETE` requests are identified by their path, so they don't take a key. Deleting something twice returns `404` the second time, which you can treat as success.

## Retry safely

```js filename="send-with-retry.mjs"
async function sendMessage(conversationId, body) {
  const idempotencyKey = crypto.randomUUID() // one key per logical send

  for (let attempt = 1; attempt <= 4; attempt++) {
    try {
      const response = await fetch(`https://api.bloomtext.com/v1/conversations/${conversationId}/messages`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ body }),
      })
      if (response.status < 500 && response.status !== 429) return await response.json()
    } catch {
      // Network error: safe to retry with the same key.
    }
    await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 250))
  }
  throw new Error('Message not sent after 4 attempts')
}
```

```python filename="send_with_retry.py"
import os
import time
import uuid

import requests


def send_message(conversation_id: str, body: str) -> dict:
    idempotency_key = str(uuid.uuid4())  # one key per logical send

    for attempt in range(1, 5):
        try:
            response = requests.post(
                f"https://api.bloomtext.com/v1/conversations/{conversation_id}/messages",
                headers={
                    "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}",
                    "Idempotency-Key": idempotency_key,
                },
                json={"body": body},
                timeout=10,
            )
            if response.status_code < 500 and response.status_code != 429:
                return response.json()
        except requests.RequestException:
            pass  # Network error: safe to retry with the same key.
        time.sleep(2**attempt * 0.25)
    raise RuntimeError("Message not sent after 4 attempts")
```

```go filename="retry.go"
func sendMessage(conversationID, text string) (*http.Response, error) {
	idempotencyKey := uuid.NewString() // one key per logical send
	payload, _ := json.Marshal(map[string]string{"body": text})
	url := "https://api.bloomtext.com/v1/conversations/" + conversationID + "/messages"

	for attempt := 1; attempt <= 4; attempt++ {
		req, _ := http.NewRequest("POST", url, bytes.NewReader(payload))
		req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY"))
		req.Header.Set("Idempotency-Key", idempotencyKey)
		req.Header.Set("Content-Type", "application/json")

		res, err := http.DefaultClient.Do(req)
		if err == nil && res.StatusCode < 500 && res.StatusCode != 429 {
			return res, nil
		}
		if err == nil {
			res.Body.Close()
		}
		time.Sleep(time.Duration(1<<attempt) * 250 * time.Millisecond)
	}
	return nil, errors.New("message not sent after 4 attempts")
}
```

```ruby filename="send_with_retry.rb"
require "net/http"
require "json"
require "securerandom"

def send_message(conversation_id, body)
  idempotency_key = SecureRandom.uuid # one key per logical send
  uri = URI("https://api.bloomtext.com/v1/conversations/#{conversation_id}/messages")

  4.times do |attempt|
    begin
      request = Net::HTTP::Post.new(uri)
      request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}"
      request["Idempotency-Key"] = idempotency_key
      request["Content-Type"] = "application/json"
      request.body = { body: body }.to_json

      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
      code = response.code.to_i
      return JSON.parse(response.body) if code < 500 && code != 429
    rescue StandardError
      # Network error: safe to retry with the same key.
    end
    sleep(2**(attempt + 1) * 0.25)
  end
  raise "Message not sent after 4 attempts"
end
```

## Choosing keys

- **Generate the key once per logical action,** before the first attempt, and keep it with the job. For a reminder job, derive it from something stable like the appointment ID and send date, so a rerun of the whole job can't double-send.
- **Never reuse a key for a different message.** You'll get `409 idempotency_key_reused`.
- **Keys are scoped to your organization.** Other organizations' keys never collide with yours.
