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.

Leer en español

How a list looks

Every list endpoint (GET /leads, /deals, /quotes, /orders, /products, /bookings, /campaigns…) answers the same shape:

GET /leads?limit=1
{
  "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: true when there are more records after this page.
  • next_cursor: pass it as cursor to get the next page. null on the last page.

Parameters

ParameterDescription
limit1 to 100. Default 25.
cursorThe next_cursor of the previous page. It is opaque: don't build or modify it. An invalid cursor answers 400 INVALID_REQUEST.
updated_sinceOnly records whose updated_at is at or after this moment. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z.
statusExact match on status (e.g. won for deals, accepted for quotes).
external_idExact match on your own id (leads, deals, quotes, orders, products, bookings).
typeperson or business (leads); the campaign type (campaigns).
lead_id, stage_id, resource_id, sku, from, toExtra 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 by id — 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 /orders does not list archived orders. You can still retrieve one by id, and webhooks report them.
  • GET /leads never lists the internal records SpaceKru keeps for its own team.

List everything

Follow next_cursor until has_more is false:

Node.js
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`);
Python
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

  1. Save the time before the run starts — not after — so changes made during the run are picked up next time.
  2. Ask each resource for updated_since=<last run start> and follow the cursor.
  3. Upsert every record by id. Overlaps are harmless: the same record twice is just the same upsert.
  4. Save the start time as the next run's updated_since.
Node.js — nightly sync (uses listAll from above)
// 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 began

Example request

curl
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"
Node.js
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);
Python
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)