Skip to main content

Idempotency

Every POST request to the Journeybee API must include a unique Idempotency-Key header. Retries with the same key return the cached response from the first successful attempt, so a network blip or client retry never creates a duplicate lead, deal, or subscription.

Quick start

Generate a UUID per logical operation and send it with your request:
curl -X POST https://api.journeybee.io/v1/leads \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 7f3a9b2c-4e8d-4a5b-9c1d-8e5f2a3b4c5d" \
  -H "Content-Type: application/json" \
  -d '{ "first_name": "Jane", "email": "jane@acme.test" }'
If the network times out and you retry with the same key and body, you get the same response — no duplicate lead is created.

Rules

ConditionResponse
First request with a new keyRuns the handler, caches the response for 24h
Retry with same key + same bodyReplays the cached response (same status + body)
Retry with same key + different body422 — key was reused for a different request
Retry while the first request is still running409 — tells you to back off and retry
5xx failure (server error)Not cached — your retry gets a fresh attempt
Missing header on any POST400 — header is required

Key format

  • 1–255 characters
  • Alphanumeric, underscore, hyphen ([A-Za-z0-9_-])
  • UUIDs recommended — they’re collision-free and easy to generate
  • Keys are scoped per company, so two companies using the same key string will not collide

Retention

Each key’s response is cached for 24 hours. After that, the same key becomes fresh again.

What to use as a key

Pick a value that uniquely identifies the logical operation the client intends, not the physical HTTP request:
  • crm-lead-7841-sync — one key per lead you’re syncing.
  • ✅ A UUID generated when the user clicked “Save”.
  • Date.now() — changes on every retry, defeats dedup.
  • ❌ A constant string — every request collides.

Client examples

Node.js

import { randomUUID } from "node:crypto";

async function createLead(payload) {
  const key = randomUUID();

  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch("https://api.journeybee.io/v1/leads", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.JOURNEYBEE_API_KEY}`,
        "Idempotency-Key": key,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (res.status === 409) {
      // in flight — back off and retry with the SAME key
      await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
      continue;
    }
    return res.json();
  }
  throw new Error("createLead: exhausted retries");
}

Python

import uuid
import time
import requests

def create_lead(payload, api_key):
    key = str(uuid.uuid4())
    for attempt in range(3):
        r = requests.post(
            "https://api.journeybee.io/v1/leads",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Idempotency-Key": key,
            },
            json=payload,
        )
        if r.status_code == 409:
            time.sleep(0.5 * 2 ** attempt)
            continue
        return r.json()
    raise Exception("exhausted retries")

Scope today

Idempotency is required on all POST endpoints. PATCH and DELETE are not currently required to carry the header — most PATCH requests are naturally idempotent (setting a field to the same value twice has the same effect as once) and DELETE is idempotent by definition.