Idempotency
Networks fail. If a POST times out, you cannot tell whether Vehiso received it, and retrying blindly could create a second enquiry or a second vehicle. The Idempotency-Key header makes the retry safe.
Every POST endpoint accepts it. Generate a unique value (a UUID v4 is ideal) for each distinct operation, and send the same value when you retry that operation.
curl https://api.vehiso.com/v1/enquiries \
-H "Authorization: Bearer $VEHISO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8e1f7c2a-4b3d-4e5f-9a6b-7c8d9e0f1a2b" \
-d @enquiry.json
How it behaves
| Situation | Result |
|---|---|
| First request with a key | Processed normally. The response is stored for 24 hours. |
| Same key, same request, within 24 hours | The stored response is returned again, with the header Idempotent-Replayed: true. Nothing is done twice. |
| Same key, different request body or path | Refused with 409 idempotency_key_reused. |
| Same key while the first request is still being processed | Refused with 409 idempotency_key_in_use. Wait, then retry. |
The first attempt failed with a 5xx | Not stored, so a retry with the same key is processed afresh. |
Keys are scoped to your API key, so two integrations cannot collide. A key can be up to 255 characters.
Stored responses include errors below 500: if the first attempt was refused with a 422, a retry with the same key gets the same 422. Fix the request and send it with a new key.
Example: retry with backoff
- Node.js
- Python
import {randomUUID} from 'node:crypto';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function postWithRetry(path, payload, attempts = 4) {
const idempotencyKey = randomUUID(); // one key for every attempt of this operation
for (let attempt = 1; ; attempt++) {
try {
const res = await fetch(`https://api.vehiso.com/v1${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VEHISO_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
const retryable = res.status >= 500 || res.status === 429 || res.status === 409;
if (!retryable || attempt === attempts) return res;
const retryAfter = Number(res.headers.get('Retry-After'));
await sleep(retryAfter ? retryAfter * 1000 : 2 ** attempt * 1000);
} catch (err) {
// Network error: we do not know whether it arrived, so retry with the same key.
if (attempt === attempts) throw err;
await sleep(2 ** attempt * 1000);
}
}
}
import os
import time
import uuid
import requests
def post_with_retry(path, payload, attempts=4):
idempotency_key = str(uuid.uuid4()) # one key for every attempt of this operation
headers = {
"Authorization": f"Bearer {os.environ['VEHISO_API_KEY']}",
"Idempotency-Key": idempotency_key,
}
for attempt in range(1, attempts + 1):
try:
res = requests.post(
f"https://api.vehiso.com/v1{path}", json=payload, headers=headers, timeout=30
)
except requests.RequestException:
# Network error: we do not know whether it arrived, so retry with the same key.
if attempt == attempts:
raise
time.sleep(2 ** attempt)
continue
retryable = res.status_code >= 500 or res.status_code in (409, 429)
if not retryable or attempt == attempts:
return res
time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
A 409 is retried here because idempotency_key_in_use clears once the first attempt finishes. idempotency_key_reused will not clear, but it only happens if your code sends a different body under the same key, which the helper above never does.