Idempotency
Idempotency keys let you safely retry API requests without accidentally creating duplicate resources or triggering duplicate actions.
Why Idempotency Matters
Network requests can fail at any point:
- The request reaches the server and succeeds, but the response is lost in transit
- The connection times out before the server responds
- Your client crashes after sending the request
Without idempotency, retrying a POST /environments/{id}/actions/redeploy after a timeout could trigger two deployments. With an idempotency key, the second request returns the same result as the first — no duplicate action.
Idempotency keys are especially important in CI/CD pipelines where flaky networks are common. Add them to every state-changing request.
How It Works
Add the X-Idempotency-Key header to any supported request:
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000Behavior:
- First request — processed normally, result is cached for 24 hours
- Same key within 24 hours — cached response is returned immediately, no action is taken
- Same key after 24 hours — key is expired, request is treated as new
The cached result is returned with the same HTTP status code as the original request. If the original returned 201 Created, retries also return 201 Created — even though no new resource was created.
Supported Endpoints
| Endpoint | Method | Effect of duplicate |
|---|---|---|
POST /projects | Create project | Returns existing project |
POST /environments | Create environment | Returns existing environment |
POST /webhooks | Create webhook | Returns existing webhook |
POST /environments/{id}/actions/deploy | Trigger deploy | Returns original job status |
POST /environments/{id}/actions/redeploy | Trigger redeploy | Returns original job status |
POST /environments/{id}/actions/restart | Restart environment | Returns original result |
POST /environments/{id}/actions/start | Start environment | Returns original result |
POST /environments/{id}/actions/stop | Stop environment | Returns original result |
GET, PUT, PATCH, and DELETE requests are naturally idempotent and do not require idempotency keys (though they are accepted and ignored).
Key Format
Use a UUID v4 — it's random, globally unique, and well-supported in every language:
# Generate a UUID and use it
KEY=$(uuidgen)
curl -X POST https://api.oec.sh/api/public/v1/environments/env_01abc.../actions/redeploy \
-H "Authorization: Bearer oec_live_rw_your_key" \
-H "X-Idempotency-Key: $KEY"Naming Patterns
| Pattern | Example | Good for |
|---|---|---|
| Random UUID | 550e8400-e29b-41d4-a716-446655440000 | One-off operations |
| Resource + commit hash | redeploy-abc123def456 | Git-triggered deploys (same commit = same key) |
| Resource + timestamp | redeploy-env_01abc-1741958400 | Scheduled automation |
| Pipeline run ID | gh-actions-run-12345678 | CI/CD pipelines |
Keys must be unique per operation. If you use the same key for two different deployments (e.g., reusing redeploy-env_01abc for every deploy), the second and all subsequent deploys will return the cached result of the first and never actually run. Use a commit hash or timestamp in the key to make it unique per operation.
Safe Retry Pattern
Generate an idempotency key before the request
Generate the key once and reuse it for all retries of the same operation:
import uuid
import time
import requests
def deploy_with_retry(session, env_id, max_retries=3):
# Generate key once — reuse it for all retries
idempotency_key = str(uuid.uuid4())
url = f"https://api.oec.sh/api/public/v1/environments/{env_id}/actions/redeploy"
for attempt in range(max_retries):
try:
response = session.post(
url,
headers={"X-Idempotency-Key": idempotency_key},
timeout=10,
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
if attempt == max_retries - 1:
raise
wait = 2 ** attempt # 1s, 2s, 4s
print(f"Timeout on attempt {attempt + 1}, retrying in {wait}s...")
time.sleep(wait)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 409:
# Action already running — that's fine, return the current status
return e.response.json()
raiseCheck the response
A 200 or 201 response that was served from cache includes the X-Idempotency-Replayed: true header:
HTTP/1.1 201 Created
X-Idempotency-Replayed: trueThis confirms the response is a replay and the original action was not re-executed.
Conflict Responses
If a request with an idempotency key is still in-flight when a duplicate arrives, the API returns:
HTTP/1.1 409 Conflict{
"error": "idempotency_key_in_use",
"message": "A request with this idempotency key is already being processed. Retry after a moment."
}Wait a few seconds and retry with the same key.