Idempotency ensures that an API request produces the same result regardless of how many times it is sent. This is particularly important for operations that modify state, like creating accounts or signing transactions, where duplicate requests could cause unintended side effects.
How it works
The CDP APIs support idempotency through the X-Idempotency-Key header. When you include an idempotency key with a request, the API responds as follows:
- Processes the request as normal if it’s the first use of the key within the last 24 hours
- Returns the exact same response as the first request if the same request is retried with the same key
- Returns an error if the same key is used with different request parameters
This mechanism ensures that temporary issues like network failures don’t result in duplicate operations.
Using idempotency keys
The X-Idempotency-Key header must be a valid UUID v4 string. For example:
X-Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324
Generating keys
Each unique request should use a new UUID v4. Here’s how to generate one in different languages:
TypeScript
JavaScript
Python
Go
Ruby
PHP
Java
C++
C#
import { v4 as uuidv4 } from 'uuid';
// Generate a new idempotency key
const idempotencyKey = uuidv4();
import { v4 as uuidv4 } from 'uuid';
// Generate a new idempotency key
const idempotencyKey = uuidv4();
import uuid
// Generate a new idempotency key
idempotency_key = str(uuid.uuid4())
import "github.com/google/uuid"
// Generate a new idempotency key
idempotencyKey := uuid.New().String()
require 'securerandom'
// Generate a new idempotency key
idempotency_key = SecureRandom.uuid
// Using PHP's built-in UUID generator (PHP 7.4+)
$idempotencyKey = uuid_create(UUID_TYPE_RANDOM);
// Alternative using ramsey/uuid package
use Ramsey\Uuid\Uuid;
$idempotencyKey = Uuid::uuid4()->toString();
import java.util.UUID;
// Generate a new idempotency key
String idempotencyKey = UUID.randomUUID().toString();
#include <uuid/uuid.h>
#include <string>
std::string generateIdempotencyKey() {
uuid_t uuid;
char uuid_str[37];
uuid_generate_random(uuid);
uuid_unparse_lower(uuid, uuid_str);
return std::string(uuid_str);
}
using System;
// Generate a new idempotency key
string idempotencyKey = Guid.NewGuid().ToString();
Best practices
-
Generate new keys: Always generate a new UUID v4 for each unique request.
-
Store keys: Keep track of idempotency keys and their responses for retry scenarios.
-
Key lifetime: Keys should be unique within a rolling 24-hour timeframe.
-
Retry logic: Implement exponential backoff when retrying failed requests:
TypeScript
JavaScript
Python
Go
Ruby
PHP
Java
C++
C#
import { v4 as uuidv4 } from 'uuid';
async function makeIdempotentRequest<T>(url: string, data: unknown): Promise<T> {
const idempotencyKey = uuidv4();
let retries = 0;
const maxRetries = 3;
while (retries < maxRetries) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"X-Idempotency-Key": idempotencyKey
},
body: JSON.stringify(data)
});
if (response.ok) {
return response.json();
}
if (response.status === 422) {
const error = await response.json();
if (error.errorType === "idempotency_error") {
throw new Error("Idempotency key conflict");
}
}
} catch (error) {
retries++;
if (retries === maxRetries) throw error;
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, retries) * 1000)
);
}
}
throw new Error("Max retries exceeded");
}
import { v4 as uuidv4 } from 'uuid';
async function makeIdempotentRequest(url, data) {
const idempotencyKey = uuidv4();
let retries = 0;
const maxRetries = 3;
while (retries < maxRetries) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"X-Idempotency-Key": idempotencyKey
},
body: JSON.stringify(data)
});
if (response.ok) {
return response.json();
}
if (response.status === 422) {
const error = await response.json();
if (error.errorType === "idempotency_error") {
throw new Error("Idempotency key conflict");
}
}
} catch (error) {
retries++;
if (retries === maxRetries) throw error;
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, retries) * 1000)
);
}
}
}
import time
import uuid
from typing import Any, Dict
def make_idempotent_request(url: str, data: Dict[str, Any]) -> Dict[str, Any]:
idempotency_key = str(uuid.uuid4())
retries = 0
max_retries = 3
while retries < max_retries:
try:
response = requests.post(
url,
headers={"X-Idempotency-Key": idempotency_key},
json=data
)
if response.ok:
return response.json()
if response.status_code == 422:
error = response.json()
if error.get("errorType") == "idempotency_error":
raise ValueError("Idempotency key conflict")
except Exception as e:
retries += 1
if retries == max_retries:
raise e
# Exponential backoff
time.sleep(2 ** retries)
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"time"
)
func makeIdempotentRequest(url string, data interface{}) (*http.Response, error) {
idempotencyKey := uuid.New().String()
maxRetries := 3
for retries := 0; retries < maxRetries; retries++ {
requestBody, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
req.Header.Set("X-Idempotency-Key", idempotencyKey)
client := &http.Client{}
resp, err := client.Do(req)
if err == nil && resp.StatusCode == http.StatusOK {
return resp, nil
}
if resp != nil && resp.StatusCode == 422 {
var errorResp struct {
ErrorType string `json:"errorType"`
}
json.NewDecoder(resp.Body).Decode(&errorResp)
if errorResp.ErrorType == "idempotency_error" {
return nil, fmt.Errorf("idempotency key conflict")
}
}
// Exponential backoff
time.Sleep(time.Second * time.Duration(1<<retries))
}
return nil, fmt.Errorf("max retries exceeded")
}
require 'securerandom'
def make_idempotent_request(url, data)
idempotency_key = SecureRandom.uuid
retries = 0
max_retries = 3
while retries < max_retries
begin
response = HTTP.headers(
'X-Idempotency-Key' => idempotency_key
).post(url, json: data)
return response.parse if response.status.success?
if response.code == 422
error = JSON.parse(response.body.to_s)
raise 'Idempotency key conflict' if error['errorType'] == 'idempotency_error'
end
rescue StandardError => e
retries += 1
raise e if retries == max_retries
# Exponential backoff
sleep(2 ** retries)
retry
end
end
end
function makeIdempotentRequest($url, $data) {
$idempotencyKey = uuid_create(UUID_TYPE_RANDOM);
$maxRetries = 3;
$retries = 0;
while ($retries < $maxRetries) {
try {
$client = new GuzzleHttp\Client();
$response = $client->post($url, [
'headers' => ['X-Idempotency-Key' => $idempotencyKey],
'json' => $data
]);
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody(), true);
}
} catch (GuzzleHttp\Exception\ClientException $e) {
if ($e->getResponse()->getStatusCode() === 422) {
$error = json_decode($e->getResponse()->getBody(), true);
if ($error['errorType'] === 'idempotency_error') {
throw new Exception('Idempotency key conflict');
}
}
$retries++;
if ($retries === $maxRetries) {
throw $e;
}
// Exponential backoff
sleep(pow(2, $retries));
}
}
throw new Exception('Max retries exceeded');
}
import java.util.UUID;
public class IdempotentRequest {
public static <T> T makeIdempotentRequest(String url, Object data, Class<T> responseType)
throws Exception {
String idempotencyKey = UUID.randomUUID().toString();
int maxRetries = 3;
int retries = 0;
while (retries < maxRetries) {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-Idempotency-Key", idempotencyKey)
.POST(HttpRequest.BodyPublishers.ofString(
new ObjectMapper().writeValueAsString(data)))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return new ObjectMapper()
.readValue(response.body(), responseType);
}
if (response.statusCode() == 422) {
JsonNode error = new ObjectMapper()
.readTree(response.body());
if ("idempotency_error".equals(
error.get("errorType").asText())) {
throw new Exception("Idempotency key conflict");
}
}
} catch (Exception e) {
retries++;
if (retries == maxRetries) throw e;
// Exponential backoff
Thread.sleep((long) Math.pow(2, retries) * 1000);
}
}
throw new Exception("Max retries exceeded");
}
}
#include <cpprest/http_client.h>
#include <uuid/uuid.h>
using namespace web::http;
using namespace web::http::client;
class IdempotentRequest {
public:
static pplx::task<json::value> makeIdempotentRequest(
const string& url,
const json::value& data
) {
string idempotencyKey = generateUuid();
int maxRetries = 3;
int retries = 0;
while (retries < maxRetries) {
try {
http_client client(url);
http_request request(methods::POST);
request.headers().add("X-Idempotency-Key", idempotencyKey);
request.set_body(data);
return client.request(request)
.then([](http_response response) {
if (response.status_code() == 200) {
return response.extract_json();
}
if (response.status_code() == 422) {
auto error = response.extract_json().get();
if (error["errorType"].as_string() == "idempotency_error") {
throw std::runtime_error("Idempotency key conflict");
}
}
throw std::runtime_error("Request failed");
});
} catch (const std::exception& e) {
retries++;
if (retries == maxRetries) throw e;
// Exponential backoff
std::this_thread::sleep_for(
std::chrono::seconds(static_cast<int>(pow(2, retries)))
);
}
}
throw std::runtime_error("Max retries exceeded");
}
private:
static string generateUuid() {
uuid_t uuid;
char uuid_str[37];
uuid_generate_random(uuid);
uuid_unparse_lower(uuid, uuid_str);
return string(uuid_str);
}
};
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class IdempotentRequest
{
public static async Task<T> MakeIdempotentRequest<T>(
string url,
object data,
int maxRetries = 3)
{
var idempotencyKey = Guid.NewGuid().ToString();
var retries = 0;
using var client = new HttpClient();
while (retries < maxRetries)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Add("X-Idempotency-Key", idempotencyKey);
request.Content = new StringContent(
JsonSerializer.Serialize(data),
Encoding.UTF8,
"application/json"
);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content);
}
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
var error = await JsonSerializer.DeserializeAsync<ErrorResponse>(
await response.Content.ReadAsStreamAsync()
);
if (error?.ErrorType == "idempotency_error")
{
throw new Exception("Idempotency key conflict");
}
}
}
catch (Exception e)
{
retries++;
if (retries == maxRetries) throw;
// Exponential backoff
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retries)));
}
}
throw new Exception("Max retries exceeded");
}
}
Supported endpoints
You can identify endpoints that support idempotency by looking for the X-Idempotency-Key parameter in the documentation for the related endpoint. When present, this optional header parameter indicates that the endpoint supports idempotent requests.
For example, in the Create an EVM account endpoint documentation, you’ll see a field that allows you to pass the X-Idempotency-Key header.
Generally, all POST endpoints that modify state (like creating accounts or signing transactions) support idempotency.
Error handling
Idempotency key conflict
This occurs when you use the same idempotency key with different request parameters:
{
"errorType": "idempotency_error",
"errorMessage": "Idempotency key '8e03978e-40d5-43e8-bc93-6894a57f9324' was already used with a different request payload. Please try again with a new idempotency key."
}
Already processing
This error occurs in certain highly-concurrent scenarios whereby you send multiple requests within a short period with the same idempotency key. Note that under normal circumstances, sending the same request with the same idempotency
key within a 24-hour period will return the same response as the first request.
{
"errorType": "already_exists",
"errorMessage": "Another request with the same idempotency key is currently processing."
}
Usage constraints
Idempotency keys are subject to the following limitations:
- Keys must be valid UUID v4 strings
- Keys should be unique within a 24-hour window
- Duplicate requests with the same key count towards your API rate limits
Security considerations
While idempotency keys aren’t sensitive like API keys, we recommend following these security practices:
- Use cryptographically secure UUID v4 generators
- Don’t use sequential or predictable keys
- Don’t reuse keys across different operations
- Store keys securely if you need to reference them later