Rate limits
Each API key can make 120 requests per minute and 10,000 requests per day by default. Limits are per key, not per IP address, so integrations that share IP addresses (Zapier, Make, n8n) never slow each other down.
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.
The limits
| Limit | Default | Window |
|---|---|---|
| Requests per key per minute | 120 | Fixed one-minute windows (by the clock, UTC). |
| Requests per key per day | 10,000 | Calendar day in UTC. Resets at 00:00 UTC. |
| AI conversation replies in progress | 4 per server | Their own pool: a burst of conversations never blocks the rest of the API. |
| Request body size | 1 MB | Per request. Larger bodies answer 413 PAYLOAD_TOO_LARGE. |
| Page size | 100 records | Per list request (default 25). |
| Leads per enrollment call | 100 | POST /campaigns/{id}/leads |
Need more? Use more than one key only when the integrations are really separate — and tell us about your use case through the contact page.
Rate limit headers
Every authenticated response tells you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the current minute resets (a duration, not a timestamp). |
X-RateLimit-Daily-Limit | Requests allowed per UTC day. |
X-RateLimit-Daily-Remaining | Requests left today. |
Retry-After | Seconds to wait. Sent with 429 and 503. |
X-Request-Id | Id of the request. Quote it when you contact support. |
Content-Language | Language of error.message (en or es). |
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 41
X-RateLimit-Daily-Limit: 10000
X-RateLimit-Daily-Remaining: 9988
X-Request-Id: 5f2c9a1b7e3dWhen you hit a limit: 429
- Over the per-minute limit:
429 RATE_LIMITEDwithRetry-After= seconds until the next minute (at most 60). - Over the daily limit:
429 RATE_LIMITEDwithRetry-After= seconds until 00:00 UTC. - Requests rejected by the per-minute limit don't count against the daily limit.
When we are busy: 503
503 SERVICE_BUSY: the server is handling its maximum of requests at this moment.Retry-After: 5(15for AI conversation replies). It protects every customer's AI Employees, which run on the same servers.503 ELIGIBILITY_UNAVAILABLE: we could not verify your plan for a moment.Retry-After: 10.- Both are temporary and safe to retry. Writes that failed with
503were not applied.
Retry with backoff
Retry 429 and 503 after Retry-After seconds plus a little random jitter, so many workers don't retry at the same instant. For creates, send an Idempotency-Key so a retry can never create a duplicate (Idempotency).
const BASE = 'https://app.spacekru.com/api/public/v1';
// Retries 429 and 503 after Retry-After (+ jitter). Gives up on long waits
// (the daily limit resets at 00:00 UTC) and after 5 retries.
export async function spacekru(path, options = {}, attempt = 0) {
const res = await fetch(`${BASE}${path}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if ((res.status === 429 || res.status === 503) && attempt < 5) {
const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** attempt;
if (retryAfter > 60) throw new Error('Daily limit reached: resume after 00:00 UTC.');
const waitMs = retryAfter * 1000 + Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, waitMs));
return spacekru(path, options, attempt + 1);
}
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (request ${body.error.request_id})`);
return body;
}
// Usage
const page = await spacekru('/leads?limit=100');
console.log(page.data.length);import os
import random
import time
import requests
BASE = "https://app.spacekru.com/api/public/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}"}
def spacekru(method, path, **kwargs):
"""Retries 429 and 503 after Retry-After (+ jitter), at most 5 times."""
for attempt in range(6):
r = requests.request(method, BASE + path, headers=HEADERS, timeout=30, **kwargs)
if r.status_code in (429, 503) and attempt < 5:
retry_after = int(r.headers.get("Retry-After", 2 ** attempt))
if retry_after > 60:
raise RuntimeError("Daily limit reached: resume after 00:00 UTC.")
time.sleep(retry_after + random.random())
continue
body = r.json()
if not r.ok:
err = body["error"]
raise RuntimeError(f"{err['code']}: {err['message']} (request {err['request_id']})")
return body
page = spacekru("GET", "/leads", params={"limit": 100})
print(len(page["data"]))Staying well under the limits
- Use webhooks instead of polling. One subscription to
lead.createdreplaces thousands ofGET /leadscalls (Webhooks). - Sync incrementally with
updated_sinceandlimit=100instead of re-reading everything (Pagination & sync). - Spread batch jobs over time instead of firing them all at once.