Idempotencia y duplicados

Las redes fallan y las herramientas de automatización reintentan. Tres mecanismos mantienen tus datos limpios: el header Idempotency-Key hace que una creación reintentada devuelva el resultado original, la detección de duplicados rechaza un segundo lead con el mismo correo o teléfono, y external_id le da a cada registro tu propio id único.

La API de SpaceKru (v1.1) es una API REST/JSON disponible en todos los planes de pago de SpaceKru. Sincroniza leads en ambos sentidos (upsert por external_id), añade leads a campañas, crea y mueve deals y pide seguimientos a los Empleados de IA, gestiona productos, cotizaciones, órdenes, pagos y reservas, permite que tu plataforma converse con tu Empleado de IA de Customer Service y envía webhooks firmados para 16 eventos que dicen quién causó cada cambio. URL base: https://app.spacekru.com/api/public/v1. Autenticación: llave de API Bearer (sk_live_…). Documentación: https://www.spacekru.com/es/developers. OpenAPI 3.1: https://app.spacekru.com/api/public/v1/openapi.json.

Read in English

El header Idempotency-Key

Envía Idempotency-Key: <cualquier texto único>, por ejemplo el id de la respuesta del formulario, de la orden o del pago en tu sistema. Hasta 255 caracteres.

OperaciónIdempotency-Key
POST /orders, POST /quotes/{id}/convert, POST /orders/{id}/paymentsObligatoria (mueven dinero y stock). Sin ella: 400 INVALID_REQUEST con details[0].field = "Idempotency-Key".
POST /leads, PUT /leads/external/{id}, POST /deals, /products, /quotes, /bookings, /conversationsOpcional, muy recomendada.
Las demás operacionesSe ignora. PATCH se puede repetir sin riesgo.
Qué pasaResultado
Misma llave, mismo body, dentro de 24 hRecibes otra vez la respuesta original, con el header Idempotent-Replayed: true. Nada se crea dos veces.
Misma llave, body distinto409 IDEMPOTENCY_CONFLICT. Usa una llave nueva para una solicitud distinta.
Misma llave mientras la primera solicitud sigue en cursoLa segunda solicitud espera hasta 5 segundos a la primera y devuelve su resultado; si sigue en curso, 409 IDEMPOTENCY_CONFLICT: reintenta en un momento.
La primera solicitud falló (cualquier error)No se guarda nada: corrige la causa y reintenta con la misma llave.
Después de 24 hLa llave se olvida y se puede usar de nuevo.
  • Las llaves se aíslan por cuenta, operación y registro: la misma llave en dos cotizaciones distintas son dos solicitudes distintas.
  • El body se compara por contenido, así que el orden de los campos del JSON no importa.
  • Una cotización se convierte en orden una sola vez: una segunda conversión con otra llave responde 409 con la orden como existing_id.

Detección de duplicados

Con o sin el header, POST /leads nunca crea un segundo lead para la misma persona:

  • Si algún correo o teléfono que envías coincide con un lead de tu cuenta, la API responde 409 DUPLICATE_RESOURCE con el existing_id de ese lead y no crea nada.
  • Si no envías ni correo ni teléfono, la coincidencia se busca por el perfil social (Instagram, LinkedIn).
  • PATCH /leads/{id} no busca duplicados por correo o teléfono: actualiza exactamente el lead que indicas.
Respuesta 409
{
  "error": {
    "code": "DUPLICATE_RESOURCE",
    "message": "A matching record already exists.",
    "existing_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
    "request_id": "5f2c9a1b7e3d"
  }
}

Tu propio id: external_id

Leads, deals, productos, cotizaciones, órdenes y reservas aceptan external_id, único por cuenta: un segundo registro con el mismo valor responde 409 DUPLICATE_RESOURCE con existing_id. Cada lista filtra por él (?external_id=…). El patrón completo está en Sincronización en ambos sentidos.

Crear o actualizar (upsert)

Con tu id: una llamada

Si tienes un id para la persona en tu sistema, PUT /leads/external/{external_id} crea, actualiza o adopta el lead en una sola llamada y te dice qué hizo en X-SpaceKru-Upsert.

curl
curl -X PUT "https://app.spacekru.com/api/public/v1/leads/external/crm-88412" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: your-unique-request-id" \
  -d '{
  "full_name": "Maria Lopez",
  "work_phone": "+13055550199",
  "job_title": "Head of Operations"
}'
Node.js
const res = await fetch('https://app.spacekru.com/api/public/v1/leads/external/crm-88412', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'your-unique-request-id',
  },
  body: JSON.stringify({
    "full_name": "Maria Lopez",
    "work_phone": "+13055550199",
    "job_title": "Head of Operations"
  }),
});
const body = await res.json();
if (!res.ok) {
  // Branch on body.error.code; quote body.error.request_id to support.
  throw new Error(`${body.error.code}: ${body.error.message} (request ${body.error.request_id})`);
}
console.log(body);
Python
import os

import requests

r = requests.put(
    "https://app.spacekru.com/api/public/v1/leads/external/crm-88412",
    headers={
        "Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
        "Idempotency-Key": "your-unique-request-id",
    },
    json={
        "full_name": "Maria Lopez",
        "work_phone": "+13055550199",
        "job_title": "Head of Operations",
    },
    timeout=30,
)
body = r.json()
if not r.ok:
    # Branch on body["error"]["code"]; quote body["error"]["request_id"] to support.
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} (request {body['error']['request_id']})")
print(body)

Sin tu id: POST y luego PATCH

POST; si la respuesta es 409 DUPLICATE_RESOURCE, haz PATCH al existing_id.

Node.js
const BASE = 'https://app.spacekru.com/api/public/v1';
const headers = {
  Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
  'Content-Type': 'application/json',
};

// Creates the lead, or updates it when it already exists. Returns the lead.
async function upsertLead(lead, requestId) {
  const res = await fetch(`${BASE}/leads`, {
    method: 'POST',
    headers: { ...headers, 'Idempotency-Key': requestId },
    body: JSON.stringify(lead),
  });
  const body = await res.json();
  if (res.ok) return body;

  if (res.status === 409 && body.error.code === 'DUPLICATE_RESOURCE') {
    // PATCH does not accept initial_note, and its `tags` REPLACE the current tags.
    const { initial_note, ...changes } = lead;
    const upd = await fetch(`${BASE}/leads/${body.error.existing_id}`, {
      method: 'PATCH',
      headers,
      body: JSON.stringify(changes),
    });
    const updated = await upd.json();
    if (!upd.ok) throw new Error(`${updated.error.code}: ${updated.error.message}`);
    return updated;
  }
  throw new Error(`${body.error.code}: ${body.error.message} (request ${body.error.request_id})`);
}

const lead = await upsertLead(
  { full_name: 'Maria Lopez', work_email: 'maria.lopez@example.com', city: 'Miami' },
  'crm-sync-maria-lopez-2026-10-19',
);
console.log(lead.id);
Python
import os

import requests

BASE = "https://app.spacekru.com/api/public/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}"}


def upsert_lead(lead, request_id):
    """Creates the lead, or updates it when it already exists. Returns the lead."""
    r = requests.post(f"{BASE}/leads", headers={**HEADERS, "Idempotency-Key": request_id},
                      json=lead, timeout=30)
    body = r.json()
    if r.ok:
        return body
    if r.status_code == 409 and body["error"]["code"] == "DUPLICATE_RESOURCE":
        # PATCH does not accept initial_note, and its "tags" REPLACE the current tags.
        changes = {k: v for k, v in lead.items() if k != "initial_note"}
        u = requests.patch(f"{BASE}/leads/{body['error']['existing_id']}", headers=HEADERS,
                           json=changes, timeout=30)
        updated = u.json()
        if not u.ok:
            raise RuntimeError(f"{updated['error']['code']}: {updated['error']['message']}")
        return updated
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} (request {body['error']['request_id']})")


lead = upsert_lead({"full_name": "Maria Lopez", "work_email": "maria.lopez@example.com", "city": "Miami"},
                   "crm-sync-maria-lopez-2026-10-19")
print(lead["id"])