Idempotent requests
Retry supported DEUNA order and payment operations without creating a duplicate operation.
On this page
Use an idempotency key when a network failure or client timeout leaves the result of a state-changing request uncertain. DEUNA associates the key with the operation and request body, then replays the stored response instead of processing the same operation again.
Retry flow#
The merchant owns key generation, persistence, and retry timing. DEUNA owns the association between the key, operation, request body, and stored response within the documented retention window.
Supported operations#
Send X-Idempotency-Key with these public operations:
| Operation | Method and path | Reference |
|---|---|---|
| Create an order · Optional flow | POST /merchants/orders | Create an order |
| Create a payment · Purchase V2 | POST /v2/merchants/orders/purchase | Create a payment |
| Capture an authorized payment | POST /v2/merchants/orders/{order_token}/capture | Capture a payment |
| Void an authorization | POST /merchants/orders/{order_token}/void | Void an authorization |
| Refund a payment | POST /v2/merchants/orders/{order_token}/refund | Refund a payment |
Do not assume that every POST endpoint supports idempotency. Use the operation list above or check whether the endpoint reference includes the X-Idempotency-Key header.
Generate and persist the key#
Generate one UUID v4 in your merchant backend for each distinct business operation. Store it with your own operation record before calling DEUNA.
const idempotencyKey = crypto.randomUUID();Follow these rules:
- Reuse the same key only when retrying the same operation.
- Send the exact same request body on every retry. DEUNA compares the body hash, so even a semantically equivalent body serialized differently can be rejected.
- Generate a new key for a new payment, capture, void, refund, or order-creation attempt.
- Generate a new key when you correct request data after a validation or business error.
- Do not derive keys from a short sequence that can collide across systems or environments.
The key is scoped to the merchant and operation, but your integration should still treat it as globally unique and never deliberately reuse it for another action.
Send the request#
Add the key to the original request and every retry of that request. This Purchase V2 example uses an order token that the integration already created:
curl --request POST \
--url https://api.sandbox.deuna.io/v2/merchants/orders/purchase \
--header "X-Api-Key: YOUR_PRIVATE_API_KEY" \
--header "X-Idempotency-Key: 9f510c4a-0f7d-4c31-94c5-590b85e4d231" \
--header "Content-Type: application/json" \
--data '{
"order_token": "ORDER_TOKEN",
"payer_info": {
"email": "buyer@example.com",
"external_user_id": "customer_1042"
},
"payment_source": {
"method_type": "credit_card",
"payment_method": "PAYMENT_METHOD_TOKEN"
}
}'const response = await fetch("https://api.sandbox.deuna.io/v2/merchants/orders/purchase", {
method: "POST",
headers: {
"X-Api-Key": "YOUR_PRIVATE_API_KEY",
"X-Idempotency-Key": "9f510c4a-0f7d-4c31-94c5-590b85e4d231",
"Content-Type": "application/json"
},
body: JSON.stringify({
"order_token": "ORDER_TOKEN",
"payer_info": {
"email": "buyer@example.com",
"external_user_id": "customer_1042"
},
"payment_source": {
"method_type": "credit_card",
"payment_method": "PAYMENT_METHOD_TOKEN"
}
})
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();import requests
response = requests.request(
"POST",
"https://api.sandbox.deuna.io/v2/merchants/orders/purchase",
headers={
"X-Api-Key": "YOUR_PRIVATE_API_KEY",
"X-Idempotency-Key": "9f510c4a-0f7d-4c31-94c5-590b85e4d231",
"Content-Type": "application/json"
},
json={
"order_token": "ORDER_TOKEN",
"payer_info": {
"email": "buyer@example.com",
"external_user_id": "customer_1042"
},
"payment_source": {
"method_type": "credit_card",
"payment_method": "PAYMENT_METHOD_TOKEN"
}
},
)
response.raise_for_status()
data = response.json()<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.sandbox.deuna.io/v2/merchants/orders/purchase",
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: YOUR_PRIVATE_API_KEY",
"X-Idempotency-Key: 9f510c4a-0f7d-4c31-94c5-590b85e4d231",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"order_token" => "ORDER_TOKEN",
"payer_info" => [
"email" => "buyer@example.com",
"external_user_id" => "customer_1042"
],
"payment_source" => [
"method_type" => "credit_card",
"payment_method" => "PAYMENT_METHOD_TOKEN"
]
])
]);
$response = curl_exec($curl);
curl_close($curl);import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sandbox.deuna.io/v2/merchants/orders/purchase"))
.header("X-Api-Key", "YOUR_PRIVATE_API_KEY")
.header("X-Idempotency-Key", "9f510c4a-0f7d-4c31-94c5-590b85e4d231")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{\n \"order_token\": \"ORDER_TOKEN\",\n \"payer_info\": {\n \"email\": \"buyer@example.com\",\n \"external_user_id\": \"customer_1042\"\n },\n \"payment_source\": {\n \"method_type\": \"credit_card\",\n \"payment_method\": \"PAYMENT_METHOD_TOKEN\"\n }\n }"))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
request, err := http.NewRequest("POST", "https://api.sandbox.deuna.io/v2/merchants/orders/purchase", strings.NewReader("{\n \"order_token\": \"ORDER_TOKEN\",\n \"payer_info\": {\n \"email\": \"buyer@example.com\",\n \"external_user_id\": \"customer_1042\"\n },\n \"payment_source\": {\n \"method_type\": \"credit_card\",\n \"payment_method\": \"PAYMENT_METHOD_TOKEN\"\n }\n }"))
if err != nil { panic(err) }
request.Header.Set("X-Api-Key", "YOUR_PRIVATE_API_KEY")
request.Header.Set("X-Idempotency-Key", "9f510c4a-0f7d-4c31-94c5-590b85e4d231")
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
fmt.Println(string(body))
}If the response has X-Idempotency-Replay: true, DEUNA returned the stored result for an earlier request with the same key and body. Handle its status code and payload exactly as you would the original response.
Understand the retention window#
The key-to-body association begins when DEUNA first receives the key. The response replay window begins when DEUNA stores the completed result, and its behavior depends on that result:
| Original result | Replay behavior |
|---|---|
| Successful response | DEUNA stores the response for 24 hours by default after the operation completes. A retry with the same key and body during that window returns the stored status code, headers, and payload. |
4xx or 5xx response | DEUNA stores the error response for approximately 2 seconds to absorb immediate duplicates. The association between the key and the original request body remains for the full 24-hour window. |
| Request still in progress | A concurrent request waits briefly for the first result. If the first request is still running, DEUNA can return HTTP 409 with IS-4409. |
After the successful-response retention window expires, the same key can be treated as a new operation. Never depend on that behavior: create a new key for every new business attempt, even when an older key has expired.
For an error response, do not reuse the key with corrected request data. Because the original body association remains active, the changed body returns IS-4401 until the retention window ends. Correct the request and send it as a new operation with a new key.
Handle retries and conflicts#
| Situation | What DEUNA returns | What your integration should do |
|---|---|---|
| Same key and same body after a completed successful request | The stored response with X-Idempotency-Replay: true | Accept the response, then reconcile the current order state when it is not final. |
| Same key with a different body | HTTP 400, code IS-4401 | Do not retry that combination. Use a new key only when the changed body represents a corrected or new operation. |
| Duplicate arrives while the first request is still running | HTTP 409, code IS-4409, when the in-flight wait ends | Back off, then retry the exact request with the same key. Reconcile the original order before starting a new attempt. |
| Rate limit is exceeded | HTTP 429, which can include code IS-4429 | Honor Retry-After when present, then retry the same body and key. |
| Client timeout or connection loss | The client may receive no response although processing continues | Preserve the key and body, reconcile the order, and retry only according to the documented timeout procedure. |
| Non-retryable validation or business error | A 4xx response | Fix the request and use a new key. Do not retry a changed body under the old key. |
Use bounded exponential backoff with jitter. Do not send several concurrent retries for the same business operation.
See API errors for the response-code model and client timeouts for the reconciliation procedure.
Verify the behavior in sandbox#
- Send a valid supported request to
https://api.sandbox.deuna.iowith a new UUID v4 key. - Save its status code, response headers, and payload.
- Send the exact request again with the same key and body.
- Verify that the second response has the same result and includes
X-Idempotency-Replay: true. - During a separate negative test, change the body but keep the key and verify that DEUNA returns HTTP
400withIS-4401.
Do not reuse either test key for another sandbox operation.