Sincronización en ambos sentidos con tu plataforma

Tu plataforma y SpaceKru pueden editar los mismos clientes, deals, productos y reservas. Estas cuatro piezas mantienen ambos lados iguales sin bucles de eco: tu propio id (external_id), un upsert, quién causó cada cambio (triggered_by) y suscripciones que omiten tus propios cambios (exclude_own_changes).

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 problema: los bucles de eco

Tu plataforma actualiza un teléfono en SpaceKru → SpaceKru envía lead.updated → tu plataforma lo recibe y guarda el teléfono "nuevo" → si guardar dispara otra llamada a la API, nunca termina. Dos reglas lo evitan:

  1. No recibas tus propios cambios: suscríbete con exclude_own_changes: true.
  2. No reescribas lo que no cambió: compara antes de llamar a la API.

1. Guarda tu id en SpaceKru: external_id

Leads, deals, productos, cotizaciones, órdenes y reservas tienen un external_id: el id de ese registro en TU sistema, único por organización. Con él no necesitas guardar los ids de SpaceKru, y cada webhook te devuelve tu id en data.external_id.

  • Envíalo al crear el registro (POST /leads, /deals, /products, /quotes, /orders, /bookings).
  • Busca por él: cada lista acepta ?external_id=….
  • Cámbialo o bórralo con PATCH (null lo borra). Un valor que ya usa otro registro responde 409 con existing_id.

2. Escribe con el upsert

PUT /leads/external/{external_id} es la única llamada que tu plataforma necesita para los leads. Decide sola:

SituaciónResultadoX-SpaceKru-Upsert
Existe un lead con ese external_idSe actualiza (200)updated
Existe un lead con el mismo correo o teléfono y sin external_idSe actualiza y se enlaza a tu id (200): la primera sincronización de una base que existe en ambos ladosadopted
Nada coincideSe crea (201); necesita un canal de contactocreated
Mismo correo o teléfono, pero OTRO external_id409 con existing_id: dos registros tuyos apuntan a una misma persona; corrígelo en tu lado—
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)
Respuesta 200
{
  "object": "lead",
  "id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
  "type": "person",
  "full_name": "Maria Lopez",
  "business_name": null,
  "job_title": "Head of Operations",
  "work_email": "maria.lopez@example.com",
  "personal_email": null,
  "work_phone": "+13055550199",
  "personal_phone": null,
  "company_name": "Acme Corp",
  "city": "Miami",
  "country": "United States",
  "status": "new",
  "source": "api",
  "tags": [
    "typeform",
    "spring-promo",
    "vip"
  ],
  "instagram_profile": null,
  "linkedin_profile_url": null,
  "website": null,
  "external_id": "crm-88412",
  "created_at": "2026-10-19T14:32:10.000Z",
  "updated_at": "2026-10-19T15:02:44.000Z"
}

3. Escucha sin eco: exclude_own_changes

Suscríbete con la MISMA llave con la que escribe tu plataforma, y exclude_own_changes: true. Los cambios hechos con esa llave no se entregan a esa suscripción; los hechos en la app, por los Empleados de IA, por formularios o por otras llaves, sí.

curl
curl -X POST "https://app.spacekru.com/api/public/v1/subscriptions" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event_type": "lead.updated",
  "target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
  "exclude_own_changes": true
}'
Node.js
const res = await fetch('https://app.spacekru.com/api/public/v1/subscriptions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "event_type": "lead.updated",
    "target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
    "exclude_own_changes": true
  }),
});
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.post(
    "https://app.spacekru.com/api/public/v1/subscriptions",
    headers={
        "Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
    },
    json={
        "event_type": "lead.updated",
        "target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
        "exclude_own_changes": True,
    },
    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)

lead.updated solo se dispara por estos campos: type, full_name, business_name, job_title, work_email, personal_email, work_phone, personal_phone, company_name, status, tags, city, country, instagram_profile, linkedin_profile_url, website, external_id. Los contadores internos nunca lo disparan.

4. Sabe quién lo causó: triggered_by

Cada webhook trae triggered_by: { "type": "api", "api_key_id": "…" } cuando el cambio lo causó una solicitud con una llave de API de tu organización, o { "type": "app", "api_key_id": null } en cualquier otro caso (tu equipo, un Empleado de IA, un formulario, una importación). Úsalo para tus propias reglas; por ejemplo, ignorar los cambios de otras integraciones.

Receta: leads en ambos sentidos

Primera sincronización

  1. Para cada cliente de tu plataforma, llama PUT /leads/external/{tu id}: los leads que ya existen en SpaceKru se adoptan y el resto se crean.
  2. Luego lista todos los leads de SpaceKru (paginación); los que tienen external_id: null todavía no existen en tu plataforma: créalos allí y escribe tu id nuevo con PATCH /leads/{id}.

De ahí en adelante

  1. Tu plataforma → SpaceKru: en cada cambio, PUT /leads/external/{tu id} con los campos que cambiaron.
  2. SpaceKru → tu plataforma: suscribe lead.created y lead.updated con exclude_own_changes: true y aplícalos:
Manejador del webhook
// Node.js (Express): one handler for lead.created and lead.updated
app.post('/hooks/spacekru', express.raw({ type: 'application/json' }), async (req, res) => {
  // 1. Verify the signature first (see /developers/webhooks#verify-signatures).
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200); // answer fast, work afterwards

  if (await alreadyProcessed(event.id)) return;   // at-least-once delivery
  if (event.triggered_by.type === 'api') return;   // optional: ignore API writes (yours or other tools')

  const lead = event.data;
  const local = lead.external_id
    ? await db.customers.findById(lead.external_id)
    : await db.customers.findBySpaceKruId(lead.id);
  if (local) {
    await db.customers.update(local.id, { phone: lead.work_phone, email: lead.work_email, spacekru_id: lead.id });
  } else {
    const created = await db.customers.create({ name: lead.full_name, phone: lead.work_phone, spacekru_id: lead.id });
    // Link both sides: SpaceKru now knows your id.
    await fetch(`${BASE}/leads/${lead.id}`, { method: 'PATCH', headers: HEADERS, body: JSON.stringify({ external_id: created.id }) });
  }
});

Deals, productos, ventas y reservas

RecursoEscribirEscuchar
DealsPOST /deals, PATCH /deals/{id}deal.created, deal.stage_changed, deal.updated
ProductosPOST /products, PATCH /products/{id}product.created, product.updated
CotizacionesPOST /quotes, PATCH /quotes/{id}quote.created, quote.status_changed
ÓrdenesPOST /orders, PATCH /orders/{id}, POST /orders/{id}/paymentsorder.created, order.status_changed
ReservasPOST /bookings, PATCH /bookings/{id}booking.created, booking.updated

El mismo patrón: tu id en external_id, la misma llave para escribir y suscribirte, y exclude_own_changes: true. Estos recursos no tienen upsert: busca el registro con ?external_id= y luego créalo o actualízalo.