Paginación y sincronización incremental
Las listas se paginan con un cursor, del más reciente al más antiguo, hasta 100 registros por página. Combina updated_since con el cursor para mantener otro sistema sincronizado sin releer todo.
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.
Cómo se ve una lista
Cada endpoint de lista (GET /leads, /deals, /quotes, /orders, /products, /bookings, /campaigns…) responde con la misma forma:
{
"data": [
{
"object": "lead",
"id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"type": "person",
"full_name": "Maria Lopez",
"business_name": null,
"job_title": "Operations Manager",
"work_email": "maria.lopez@example.com",
"personal_email": null,
"work_phone": "+13055550142",
"personal_phone": null,
"company_name": "Acme Corp",
"city": "Miami",
"country": "United States",
"status": "new",
"source": "api",
"tags": [
"typeform",
"spring-promo"
],
"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-19T14:32:10.000Z"
}
],
"has_more": true,
"next_cursor": "eyJ0IjoiMjAyNi0xMC0xOVQxNDozMjoxMC4wMDBaIiwiaSI6IjNmMmI4YzFlLTlhNGQtNGU2Zi1iN2E4LTFjMmQzZTRmNWE2YiJ9"
}data: los registros de esta página.has_more:truecuando hay más registros después de esta página.next_cursor: pásalo comocursorpara la siguiente página.nullen la última.
Parámetros
| Parámetro | Descripción |
|---|---|
limit | De 1 a 100. Por defecto 25. |
cursor | El next_cursor de la página anterior. Es opaco: no lo construyas ni lo modifiques. Un cursor inválido responde 400 INVALID_REQUEST. |
updated_since | Solo registros cuyo updated_at es igual o posterior a este momento. ISO 8601 con zona horaria, por ejemplo 2026-10-19T00:00:00Z. |
status | Coincidencia exacta con status (por ejemplo won para deals, accepted para cotizaciones). |
external_id | Coincidencia exacta con tu propio id (leads, deals, cotizaciones, órdenes, productos, reservas). |
type | person o business (leads); el tipo de campaña (campañas). |
lead_id, stage_id, resource_id, sku, from, to | Filtros extra de deals, reservas y productos; mira cada lista en la referencia. |
Los filtros se combinan con AND. Cualquier otro parámetro se rechaza con 400 (así un error de tipeo nunca devuelve todo en silencio).
Orden
- Siempre del más reciente al más antiguo por
created_at, luego porid, en todas las listas. - Los registros que se actualizan mientras recorres una lista no cambian de página.
- Los registros creados mientras recorres una lista aparecen al inicio; una sincronización incremental (abajo) los recoge en la siguiente ejecución.
Lo que las listas dejan fuera
GET /ordersno lista órdenes archivadas. Puedes leerlas por id y los webhooks las reportan.GET /leadsnunca lista los registros internos que SpaceKru guarda para su propio equipo.
Lista todo
Sigue next_cursor hasta que has_more sea false:
const BASE = 'https://app.spacekru.com/api/public/v1';
const headers = { Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}` };
// Lists EVERY record of a resource ('leads', 'deals', 'quotes', 'orders', 'products', 'bookings'…).
async function listAll(resource, params = {}) {
const all = [];
let cursor = null;
do {
const query = new URLSearchParams({ limit: '100', ...params });
if (cursor) query.set('cursor', cursor);
const res = await fetch(`${BASE}/${resource}?${query}`, { headers });
const page = await res.json();
if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
all.push(...page.data);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
return all;
}
const leads = await listAll('leads', { updated_since: '2026-10-01T00:00:00Z' });
console.log(`${leads.length} leads changed since October 1`);import os
import requests
BASE = "https://app.spacekru.com/api/public/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}"}
def list_all(resource, **params):
"""Lists EVERY record of a resource ('leads', 'deals', 'quotes', 'orders', 'products', 'bookings'...)."""
records, cursor = [], None
while True:
query = {"limit": 100, **params}
if cursor:
query["cursor"] = cursor
r = requests.get(f"{BASE}/{resource}", headers=HEADERS, params=query, timeout=30)
page = r.json()
if not r.ok:
raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}")
records.extend(page["data"])
if not page["has_more"]:
return records
cursor = page["next_cursor"]
leads = list_all("leads", updated_since="2026-10-01T00:00:00Z")
print(f"{len(leads)} leads changed since October 1")Recorrer 10.000 leads toma 100 solicitudes, muy por debajo del límite por minuto. Para trabajos más grandes, mira Límites de uso.
Sincronización incremental con updated_since
- Guarda la hora antes de que empiece la ejecución, no después, para recoger la próxima vez los cambios hechos durante la ejecución.
- Pide a cada recurso
updated_since=<inicio de la última ejecución>y sigue el cursor. - Haz upsert de cada registro por
id. Los solapamientos no hacen daño: el mismo registro dos veces es el mismo upsert. - Guarda la hora de inicio como
updated_sincede la siguiente ejecución.
// Run it on a schedule (e.g. every night). `state` is wherever you keep the
// last run time: a database row, a file, a key-value store.
const startedAt = new Date().toISOString(); // 1. note the start time FIRST
const since = state.lastSync ?? '1970-01-01T00:00:00Z';
for (const resource of ['leads', 'deals', 'quotes', 'orders', 'products', 'bookings']) {
const changed = await listAll(resource, { updated_since: since }); // 2. only what changed
for (const record of changed) await db.upsert(resource, record.id, record); // 3. upsert by id
}
state.lastSync = startedAt; // 4. next run starts where this one beganEjemplo de solicitud
curl "https://app.spacekru.com/api/public/v1/deals?status=won&updated_since=2026-10-01T00%3A00%3A00Z&limit=100" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/deals?status=won&updated_since=2026-10-01T00%3A00%3A00Z&limit=100', {
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
},
});
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.get(
"https://app.spacekru.com/api/public/v1/deals?status=won&updated_since=2026-10-01T00%3A00%3A00Z&limit=100",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
},
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)