Write operations in the Vendaze API support idempotency via the Idempotency-Key header. This allows you to safely retry a request if you did not receive a response, without the risk of creating duplicate records.
How to use it
Include the Idempotency-Key header with a unique string you generate:
curl -X POST https://api.vendaze.com/v1/deals \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f8a9b2c-3d4e-5f60-a1b2-c3d4e5f60718" \
-d '{
"name": "Annual Acme Contract",
"pipeline_id": "pipeline-uuid",
"stage_id": "stage-uuid",
"currency": "USD",
"price_cts": 1200000
}'
If you send the same request again with the same Idempotency-Key, the API returns the original response without processing it again.
The key must be:
- A string of up to 255 characters
- Unique per operation (recommended: UUID v4)
- Generated by your system, not reused across different operations
import { randomUUID } from 'crypto';
// Generate a unique key per operation attempt
const idempotencyKey = randomUUID();
Behavior by scenario
| Scenario | Behavior |
|---|
| First request | Processes normally and stores the response. |
| Repeated request with same key and same body | Returns the original response without reprocessing. |
| Repeated request with same key and different body | Returns 422 with code idempotency_conflict. |
| Repeated request after 24 hours | Key expired. Processes as a new request. |
Why PATCH and DELETE are naturally idempotent
PATCH and DELETE endpoints do not require the Idempotency-Key header because they are idempotent by definition:
- PATCH: applying the same update twice produces the same result. Setting
full_name to "Ana Costa" a second time leaves the record unchanged.
- DELETE: deleting a record that has already been deleted returns
404. The end state (record does not exist) is the same regardless of how many times you call it.
The Idempotency-Key header is only necessary for POST operations, where each call without a key would create a new record.
Endpoints that support idempotency
The Idempotency-Key header is accepted on all write endpoints:
POST /v1/people
POST /v1/companies
POST /v1/deals
Recommended pattern for critical creations
import { randomUUID } from 'crypto';
async function createDeal(payload, accessToken) {
// Generate the key once per logical operation, before the first attempt
const idempotencyKey = randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch('https://api.vendaze.com/v1/deals', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
// Do not retry on validation errors or client errors
if (res.status >= 400 && res.status < 500) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
// Retry on network failures or 5xx
if (res.status >= 500) {
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
} catch (err) {
if (attempt === 2) throw err;
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
}
Tracking idempotency keys in your database
For operations where you need an audit trail or want to correlate your internal records with API responses, store the idempotency key alongside the created resource:
CREATE TABLE deal_creations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key UUID NOT NULL UNIQUE,
vendaze_deal_id UUID,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
resolved_at TIMESTAMPTZ
);
async function createDealWithTracking(payload, accessToken) {
const idempotencyKey = randomUUID();
// Record the attempt before sending
await db.dealCreations.insert({
idempotency_key: idempotencyKey,
payload,
status: 'pending',
});
try {
const res = await fetch('https://api.vendaze.com/v1/deals', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
const deal = await res.json();
await db.dealCreations.update(
{ idempotency_key: idempotencyKey },
{ status: 'success', vendaze_deal_id: deal.data.id, resolved_at: new Date() },
);
return deal;
} catch (err) {
await db.dealCreations.update(
{ idempotency_key: idempotencyKey },
{ status: 'failed', resolved_at: new Date() },
);
throw err;
}
}
This pattern lets you recover from failures by looking up the idempotency_key in your database and checking whether the operation succeeded before retrying.
Idempotency keys persist for 24 hours. Always use a UUID v4 generated at the time of the
operation. Never reuse keys across different operations.
Key conflict
If you send a previously used key with a different body, the API returns:
{
"error": {
"code": "idempotency_conflict",
"message": "An idempotency key was reused with a different request body.",
"request_id": "uuid"
}
}
This protects against bugs where the same key is accidentally reused for two distinct operations.