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.
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:
- No recibas tus propios cambios: suscríbete con
exclude_own_changes: true. - 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(nulllo borra). Un valor que ya usa otro registro responde409conexisting_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ón | Resultado | X-SpaceKru-Upsert |
|---|---|---|
Existe un lead con ese external_id | Se actualiza (200) | updated |
Existe un lead con el mismo correo o teléfono y sin external_id | Se actualiza y se enlaza a tu id (200): la primera sincronización de una base que existe en ambos lados | adopted |
| Nada coincide | Se crea (201); necesita un canal de contacto | created |
Mismo correo o teléfono, pero OTRO external_id | 409 con existing_id: dos registros tuyos apuntan a una misma persona; corrígelo en tu lado | — |
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"
}'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);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){
"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 -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
}'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);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
- 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. - Luego lista todos los leads de SpaceKru (paginación); los que tienen
external_id: nulltodavía no existen en tu plataforma: créalos allí y escribe tu id nuevo conPATCH /leads/{id}.
De ahí en adelante
- Tu plataforma → SpaceKru: en cada cambio,
PUT /leads/external/{tu id}con los campos que cambiaron. - SpaceKru → tu plataforma: suscribe
lead.createdylead.updatedconexclude_own_changes: truey aplícalos:
// 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
| Recurso | Escribir | Escuchar |
|---|---|---|
| Deals | POST /deals, PATCH /deals/{id} | deal.created, deal.stage_changed, deal.updated |
| Productos | POST /products, PATCH /products/{id} | product.created, product.updated |
| Cotizaciones | POST /quotes, PATCH /quotes/{id} | quote.created, quote.status_changed |
| Órdenes | POST /orders, PATCH /orders/{id}, POST /orders/{id}/payments | order.created, order.status_changed |
| Reservas | POST /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.