Skip to main content

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​

SituationResult
First request with a keyProcessed normally. The response is stored for 24 hours.
Same key, same request, within 24 hoursThe stored response is returned again, with the header Idempotent-Replayed: true. Nothing is done twice.
Same key, different request body or pathRefused with 409 idempotency_key_reused.
Same key while the first request is still being processedRefused with 409 idempotency_key_in_use. Wait, then retry.
The first attempt failed with a 5xxNot 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​

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);
}
}
}

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.