Pagination & incremental sync
Lists are paginated with a cursor, newest first, up to 100 records per page. Combine updated_since with the cursor to keep another system in sync without re-reading everything.
The SpaceKru API (v1.1) is a REST/JSON API available on every paid SpaceKru plan. It syncs leads both ways (upsert by external_id), adds leads to campaigns, creates and moves deals and asks AI Employees to follow up, manages products, quotes, orders, payments and bookings, lets your platform chat with your Customer Service AI Employee, and sends signed webhooks for 16 events that say who caused each change. Base URL: https://app.spacekru.com/api/public/v1. Authentication: Bearer API key (sk_live_…). Documentation: https://www.spacekru.com/developers (Spanish: https://www.spacekru.com/es/developers). OpenAPI 3.1: https://app.spacekru.com/api/public/v1/openapi.json.
How a list looks
Every list endpoint (GET /leads, /deals, /quotes, /orders, /products, /bookings, /campaigns…) answers the same shape:
{
"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: the records of this page.has_more:truewhen there are more records after this page.next_cursor: pass it ascursorto get the next page.nullon the last page.
Parameters
| Parameter | Description |
|---|---|
limit | 1 to 100. Default 25. |
cursor | The next_cursor of the previous page. It is opaque: don't build or modify it. An invalid cursor answers 400 INVALID_REQUEST. |
updated_since | Only records whose updated_at is at or after this moment. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z. |
status | Exact match on status (e.g. won for deals, accepted for quotes). |
external_id | Exact match on your own id (leads, deals, quotes, orders, products, bookings). |
type | person or business (leads); the campaign type (campaigns). |
lead_id, stage_id, resource_id, sku, from, to | Extra filters of deals, bookings and products — see each list in the reference. |
Filters combine with AND. Any other query parameter is rejected with 400 (so a typo never silently returns everything).
Sort order
- Always newest first by
created_at, then byid— for every list. - Records that are updated while you page through a list don't move between pages.
- Records created while you page through a list appear at the start; an incremental sync (below) picks them up on its next run.
What lists leave out
GET /ordersdoes not list archived orders. You can still retrieve one by id, and webhooks report them.GET /leadsnever lists the internal records SpaceKru keeps for its own team.
List everything
Follow next_cursor until has_more is 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")A full pass over 10,000 leads takes 100 requests — far below the per-minute limit. For bigger jobs, see Rate limits.
Incremental sync with updated_since
- Save the time before the run starts — not after — so changes made during the run are picked up next time.
- Ask each resource for
updated_since=<last run start>and follow the cursor. - Upsert every record by
id. Overlaps are harmless: the same record twice is just the same upsert. - Save the start time as the next run's
updated_since.
// 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 beganExample request
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)