GuidesIdempotent Requests

Idempotent Requests

All mutating POST endpoints require an Idempotency-Key header. This prevents duplicate resource creation when you retry failed requests.

How It Works

  1. You send a POST request with an Idempotency-Key header
  2. The API processes the request and stores the result keyed by your idempotency key
  3. If you retry with the same key and same body, you get the original response back (no duplicate created)
  4. If you retry with the same key but different body, you get a 409 Conflict
curl -X POST https://api.firsthandapi.com/v1/jobs \
  -H "Authorization: Bearer fh_live_..." \
  -H "Idempotency-Key: job-create-order-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "data_collection",
    "description": "Photo of product packaging",
    "files_needed": 10,
    "accepted_formats": ["image/jpeg"],
    "price_per_file_cents": 50
  }'

Generating Idempotency Keys

Use any unique string. Common patterns:

// UUID v4 (recommended for general use)
const key = crypto.randomUUID();
 
// Deterministic key (recommended for order-based workflows)
const key = `create-job-order-${orderId}`;
 
// Timestamp-based (for one-off operations)
const key = `job-${Date.now()}-${Math.random().toString(36).slice(2)}`;

Endpoints Requiring Idempotency Keys

EndpointPurpose
POST /v1/jobsCreate a job
POST /v1/jobs/:id/cancelCancel a job
POST /v1/jobs/:id/rateRate a completed job
POST /v1/api_keysCreate an API key
POST /v1/webhook_endpointsCreate a webhook endpoint
POST /v1/billing/purchasePurchase credits

GET, PATCH, and DELETE requests are naturally idempotent and don’t require the header.

Key Expiration

Idempotency keys expire after 24 hours. After expiration, the same key can be reused for a new request.

Error: Conflicting Idempotency Key

If you send the same idempotency key with a different request body:

{
  "error": {
    "type": "idempotency_conflict",
    "message": "A different request was already made with this idempotency key",
    "request_id": "req_01HV..."
  }
}

Status code: 409 Conflict

SDK Support

Both official SDKs generate idempotency keys automatically:

// TypeScript SDK — auto-generates idempotency keys
const job = await client.createJob({
  type: 'data_collection',
  description: 'Photo of storefront',
  files_needed: 5,
  accepted_formats: ['image/jpeg'],
  price_per_file_cents: 40,
});
// Idempotency-Key header is set automatically

To pass your own key:

const job = await client.createJob(body, {
  idempotencyKey: 'my-custom-key-123',
});