Your AI Employee inside your platform
Your Customer Service AI Employee can answer your customers inside your own app, website or support tool. Your platform sends the customer's message; the reply comes back in the same response, written with the same tools the AI Employee uses in WhatsApp: quotes, orders, bookings, catalog, knowledge base and escalation to a person.
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.
Requirements
- An API key with Read & write and AI conversations (the account owner ticks it when creating the key).
- In SpaceKru, an inbound campaign that includes the Web Chat channel, assigned to a Customer Service AI Employee that has the Web Chat channel turned on. API conversations are served by that campaign, exactly like the website chat.
- AI tokens in the plan or the wallet: each reply uses them, like any other reply of your AI Employees.
Check it at any time:
curl "https://app.spacekru.com/api/public/v1/conversations/availability" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/conversations/availability', {
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/conversations/availability",
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){
"object": "conversation_availability",
"available": true,
"reason": null,
"agent": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"name": "Sofia",
"language": "es"
},
"campaign": {
"id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
"name": "Customer service",
"status": "running"
}
}Lifecycle
POST /conversations → POST /conversations/{id}/messages → … as many turns as needed … → POST /conversations/{id}/close
1. Open a conversation
For an existing lead (lead_id) or a new one (lead, same fields as creating a lead; if it matches an existing lead by email or phone, that lead is used). Send your own id as external_conversation_id (a ticket or chat id): opening it again returns the same conversation, so retries are safe.
curl -X POST "https://app.spacekru.com/api/public/v1/conversations" \
-H "Authorization: Bearer $SPACEKRU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-unique-request-id" \
-d '{
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"external_conversation_id": "ticket-5521",
"locale": "es"
}'const res = await fetch('https://app.spacekru.com/api/public/v1/conversations', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-request-id',
},
body: JSON.stringify({
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"external_conversation_id": "ticket-5521",
"locale": "es"
}),
});
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/conversations",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
"Idempotency-Key": "your-unique-request-id",
},
json={
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"external_conversation_id": "ticket-5521",
"locale": "es",
},
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)2. Send the customer's messages
Each call sends one message (up to 4000 characters) and waits for the AI Employee. Most replies take a few seconds; with tools (quoting, booking) it can take up to about a minute. Set your HTTP timeout to at least 90 seconds.
curl -X POST "https://app.spacekru.com/api/public/v1/conversations/18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7/messages" \
-H "Authorization: Bearer $SPACEKRU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Hi, do you have a table for 4 on Saturday at 8pm?"
}'const res = await fetch('https://app.spacekru.com/api/public/v1/conversations/18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"text": "Hi, do you have a table for 4 on Saturday at 8pm?"
}),
});
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/conversations/18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7/messages",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
},
json={
"text": "Hi, do you have a table for 4 on Saturday at 8pm?",
},
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": "conversation_turn",
"conversation_id": "18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7",
"messages": [
{
"object": "conversation_message",
"id": "29304b5c-6d7e-4f80-91a2-b3c4d5e6f708",
"conversation_id": "18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7",
"role": "agent",
"text": "Hi Maria! Yes, Table 4 on the terrace is free on Saturday at 8pm. Shall I book it for you?",
"created_at": "2026-10-20T15:00:06.000Z"
}
],
"deferred": false,
"deferred_reason": null
}The AI Employee can answer with more than one message: show every item of messages in order.
3. Close it
Conversations do not expire. Close one when the case ends (POST /conversations/{id}/close); a closed conversation rejects new messages with 422. Open a new one to talk again.
When there is no reply
The customer's message is always saved in the SpaceKru Inbox. When the AI Employee does not answer right away, deferred is true and deferred_reason says why:
deferred_reason | Meaning | What to do |
|---|---|---|
replies_disabled | A person took over this customer, or AI replies were turned off for them. | Wait for the person: their message arrives by webhook. |
campaign_inactive | The inbound campaign is paused. A supervisor is alerted. | Resume the campaign in SpaceKru. |
message_filtered | The message was filtered (spam, opt-out, auto-reply…). | Nothing. |
no_inbound_campaign / agent_not_ready | The setup changed after the conversation was opened. | Check GET /conversations/availability. |
no_reply | The AI Employee decided not to write anything this time. | Nothing. |
A person can take over
Every API conversation appears in the SpaceKru Inbox as Web Chat. Anyone on the team can answer from there. Every message written to the customer — by a person or by the AI Employee, including the replies you already received in the turn response — fires the conversation.message_created webhook (data.role is human for a person and agent for the AI Employee; deduplicate by data.id). Subscribe to it to never miss a reply:
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": "conversation.message_created",
"target_url": "https://your-app.example.com/hooks/spacekru"
}'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": "conversation.message_created",
"target_url": "https://your-app.example.com/hooks/spacekru"
}),
});
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": "conversation.message_created",
"target_url": "https://your-app.example.com/hooks/spacekru",
},
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)GET /conversations/{id}/messages returns the whole history, oldest first.
Tokens, limits and errors
| Situation | Answer |
|---|---|
| No AI tokens left in the plan or the wallet | 402 AI_QUOTA_EXHAUSTED |
| The setup is incomplete | 409 CONVERSATIONS_NOT_CONFIGURED (details[0].issue says what is missing) |
| More than 4 AI replies in progress on one server | 503 SERVICE_BUSY with Retry-After |
| The model did not answer in time | 503 SERVICE_BUSY: the customer's message is already saved; retry later or let a person answer |
| The daily Web Chat message limit of the account was reached | 429 RATE_LIMITED |
| Conversations are switched off | 503 CONVERSATIONS_UNAVAILABLE |