API reference
Every endpoint of the SpaceKru API v1 — 15 operations — with parameters, responses, errors and examples in curl, Node.js and Python, followed by the objects and the 16 webhook events. Generated from the OpenAPI 3.1 document.
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.
Base URL & authentication
https://app.spacekru.com/api/public/v1
Authorization: Bearer $SPACEKRU_API_KEYAll requests and responses are JSON (UTF-8). Dates are ISO 8601 in UTC. Writes need Content-Type: application/json. See Authentication, Errors and Rate limits.
Account
Test a key.
Test the connection
GET /ping · Scope: read
Returns the organization the key belongs to, its plan and the key's scopes. Use it to test a new key.
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— Authenticated. ReturnsPing.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/ping" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/ping', {
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/ping",
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": "ping",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Acme Corp",
"plan": "Business",
"scopes": [
"read",
"write"
]
}Leads
Create, update, read and annotate leads.
List leads (newest first)
GET /leads · Scope: read
Newest first (by created_at, then id). Filters combine with AND. Follow next_cursor until has_more is false. Internal records SpaceKru keeps for its own team are never listed.
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100. Default 25. 1 to 100. Default 25. |
cursor | query | string | The next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters. |
updated_since | query | string (date-time) | Only records whose updated_at is at or after this time. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z. |
status | query | string | Exact match on status. Common values: new, contacted, qualified, proposal, won. Max 50 characters. |
type | query | string | Only people or only businesses. One of: person, business. |
external_id | query | string | Exact match on the external_id of the lead (the id in your own system). Max 200 characters. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— A page of leads.400—INVALID_REQUEST— a query parameter is invalid (unknown parameter, badlimit,updated_sinceorcursor).401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/leads" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/leads', {
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/leads",
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){
"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"
}Create a lead
POST /leads · Scope: write
At least one contact channel is required: work_email, personal_email, work_phone, personal_phone, instagram_profile, linkedin_profile_url.
The lead is created with source = "api" (always) and type = "person" unless you send type. Blank strings and null are treated as "not sent" (no-code tools send "" for unmapped fields). Unknown fields are rejected with 400.
Duplicates: if an email or phone you send matches an existing lead, the API answers 409 DUPLICATE_RESOURCE with existing_id and creates nothing. When you send no email and no phone, the match is made on the social profile instead. An Instagram or LinkedIn URL sent in website is also stored in its own profile field.
initial_note is added to the lead's timeline, signed by the user who created the API key. If the note cannot be saved, the lead is still created.
Idempotency: send Idempotency-Key (up to 255 characters) to make retries safe. Same key and same body within 24 h: you get the original 201 again with Idempotent-Replayed: true. Same key, different body: 409 IDEMPOTENCY_CONFLICT. Only successful responses are stored: after an error you can retry with the same key.
Creating a lead does not make an AI Employee contact it: add the lead to a campaign in SpaceKru for that.
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Any unique string per logical request, e.g. a UUID or your form response id. Max 255 characters. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Request body (LeadCreate) — Requires at least one of: work_email, personal_email, work_phone, personal_phone, instagram_profile, linkedin_profile_url. Unknown fields are rejected. Unknown fields are rejected.
| Field | Type | Description |
|---|---|---|
full_name | string | Max 200 characters. |
business_name | string | Max 200 characters. |
job_title | string | Max 200 characters. |
work_email | string (email) | Max 320 characters. |
personal_email | string (email) | Max 320 characters. |
work_phone | string | Max 40 characters. |
personal_phone | string | Max 40 characters. |
company_name | string | Max 200 characters. |
city | string | Max 120 characters. |
country | string | Max 120 characters. |
website | string | Max 500 characters. |
instagram_profile | string | Max 300 characters. |
linkedin_profile_url | string | Max 500 characters. |
type | string | One of: person, business. |
tags | array of string | Up to 20 tags of up to 50 characters. A comma-separated string is also accepted (e.g. "typeform, vip"). Up to 20 items. |
initial_note | string | Added to the lead's timeline. Max 5000 characters. |
external_id | string | Your id for this lead (unique per organization). Max 200 characters. |
campaign_id | string (uuid) | Add the new lead to this prospecting or recontact campaign. |
Responses
201— Created. On a replay, the original response. Withcampaign_id, it also hasenrollment. Returns Lead.400—INVALID_REQUEST— validation failed (details[]lists up to 20 fields), the body is not valid JSON, orContent-Typeis notapplication/json.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.409—DUPLICATE_RESOURCE(a matching lead exists: seeexisting_id) orIDEMPOTENCY_CONFLICT.413—PAYLOAD_TOO_LARGE— the body is over 1 MB.422—UNPROCESSABLE— valid JSON that could not be applied (e.g. too many active subscriptions, a value the database rejected).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl -X POST "https://app.spacekru.com/api/public/v1/leads" \
-H "Authorization: Bearer $SPACEKRU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: typeform-resp-8f2a1c" \
-d '{
"full_name": "Maria Lopez",
"work_email": "maria.lopez@example.com",
"work_phone": "+13055550142",
"company_name": "Acme Corp",
"job_title": "Operations Manager",
"city": "Miami",
"country": "United States",
"tags": [
"typeform",
"spring-promo"
],
"external_id": "crm-88412",
"initial_note": "Asked for a demo from the spring promo form."
}'const res = await fetch('https://app.spacekru.com/api/public/v1/leads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'typeform-resp-8f2a1c',
},
body: JSON.stringify({
"full_name": "Maria Lopez",
"work_email": "maria.lopez@example.com",
"work_phone": "+13055550142",
"company_name": "Acme Corp",
"job_title": "Operations Manager",
"city": "Miami",
"country": "United States",
"tags": [
"typeform",
"spring-promo"
],
"external_id": "crm-88412",
"initial_note": "Asked for a demo from the spring promo form."
}),
});
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/leads",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
"Idempotency-Key": "typeform-resp-8f2a1c",
},
json={
"full_name": "Maria Lopez",
"work_email": "maria.lopez@example.com",
"work_phone": "+13055550142",
"company_name": "Acme Corp",
"job_title": "Operations Manager",
"city": "Miami",
"country": "United States",
"tags": ["typeform", "spring-promo"],
"external_id": "crm-88412",
"initial_note": "Asked for a demo from the spring promo form.",
},
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": "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"
}Retrieve a lead
GET /leads/{id} · Scope: read
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The lead id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— The lead. Returns Lead.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b', {
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/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
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": "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"
}Update a lead
PATCH /leads/{id} · Scope: write
Partial update: send only the fields to change (at least one). Send null or "" to clear a field.
tags: the array you send replaces the current tags; null removes them all; "" is ignored.
status and source are managed by SpaceKru and cannot be written (they are rejected as unknown fields).
Updates do NOT check for duplicates by email or phone. external_id must stay unique: another lead with the same one answers 409 with existing_id.
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The lead id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Request body (LeadUpdate) — Only the fields you send change. null or "" clears a field. status and source are not writable. Unknown fields are rejected.
| Field | Type | Description |
|---|---|---|
full_name | string | Max 200 characters. |
business_name | string | Max 200 characters. |
job_title | string | Max 200 characters. |
work_email | string (email) | Max 320 characters. |
personal_email | string (email) | Max 320 characters. |
work_phone | string | Max 40 characters. |
personal_phone | string | Max 40 characters. |
company_name | string | Max 200 characters. |
city | string | Max 120 characters. |
country | string | Max 120 characters. |
website | string | Max 500 characters. |
instagram_profile | string | Max 300 characters. |
linkedin_profile_url | string | Max 500 characters. |
type | string | One of: person, business. |
tags | array of string | Up to 20 tags of up to 50 characters. A comma-separated string is also accepted (e.g. "typeform, vip"). Up to 20 items. |
external_id | string or null | Your id for this lead. Another lead with the same one answers 409. Max 200 characters. |
Responses
200— Updated. Returns Lead.400—INVALID_REQUEST— validation failed (details[]lists up to 20 fields), the body is not valid JSON, orContent-Typeis notapplication/json.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).409—DUPLICATE_RESOURCE— another lead already uses thisexternal_id(seeexisting_id).413—PAYLOAD_TOO_LARGE— the body is over 1 MB.422—UNPROCESSABLE— valid JSON that could not be applied (e.g. too many active subscriptions, a value the database rejected).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl -X PATCH "https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b" \
-H "Authorization: Bearer $SPACEKRU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_title": "Head of Operations",
"tags": [
"typeform",
"spring-promo",
"vip"
]
}'const res = await fetch('https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"job_title": "Head of Operations",
"tags": [
"typeform",
"spring-promo",
"vip"
]
}),
});
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.patch(
"https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
},
json={
"job_title": "Head of Operations",
"tags": ["typeform", "spring-promo", "vip"],
},
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": "lead",
"id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"type": "person",
"full_name": "Maria Lopez",
"business_name": null,
"job_title": "Head of Operations",
"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",
"vip"
],
"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-19T15:02:44.000Z"
}Add a note to a lead
POST /leads/{id}/notes · Scope: write
Adds a note to the lead's timeline in SpaceKru, signed by the user who created the API key (or the account owner if that user no longer exists).
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The lead id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Request body Unknown fields are rejected.
| Field | Type | Description |
|---|---|---|
content (required) | string | Max 5000 characters. |
Responses
201— Created. Returns Note.400—INVALID_REQUEST— validation failed (details[]lists up to 20 fields), the body is not valid JSON, orContent-Typeis notapplication/json.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).413—PAYLOAD_TOO_LARGE— the body is over 1 MB.422—UNPROCESSABLE— valid JSON that could not be applied (e.g. too many active subscriptions, a value the database rejected).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl -X POST "https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b/notes" \
-H "Authorization: Bearer $SPACEKRU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Called back: wants pricing for 3 locations."
}'const res = await fetch('https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b/notes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPACEKRU_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "Called back: wants pricing for 3 locations."
}),
});
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/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b/notes",
headers={
"Authorization": f"Bearer {os.environ['SPACEKRU_API_KEY']}",
},
json={
"content": "Called back: wants pricing for 3 locations.",
},
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": "note",
"id": "5d6e7f80-91a2-4b3c-8d4e-5f6a7b8c9d0e",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"content": "Called back: wants pricing for 3 locations.",
"created_at": "2026-10-19T15:10:03.000Z"
}Deals
Read-only in v1.
List deals
GET /deals · Scope: read
Newest first (by created_at, then id). Filters combine with AND. Follow next_cursor until has_more is false.
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100. Default 25. 1 to 100. Default 25. |
cursor | query | string | The next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters. |
updated_since | query | string (date-time) | Only records whose updated_at is at or after this time. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z. |
status | query | string | Exact match on status. Common values: open, won, lost. Max 50 characters. |
external_id | query | string | Exact match on the external_id of the deal (the id in your own system). Max 200 characters. |
lead_id | query | string (uuid) | Only the deals of this lead. |
stage_id | query | string (uuid) | Only the deals in this pipeline stage (see GET /pipeline-stages). |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— A page of deals.400—INVALID_REQUEST— a query parameter is invalid (unknown parameter, badlimit,updated_sinceorcursor).401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/deals" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/deals', {
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",
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){
"data": [
{
"object": "deal",
"id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"title": "Acme Corp — 3 locations",
"value": 4800,
"stage": "Proposal",
"status": "open",
"close_date": "2026-11-30",
"lost_reason": null,
"stage_id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
"agent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"external_id": "opp-5521",
"next_step_action": "Follow-up scheduled (WhatsApp)",
"next_step_date": "2026-10-24T15:00:00.000Z",
"created_at": "2026-10-20T09:15:00.000Z",
"updated_at": "2026-10-22T16:40:12.000Z"
}
],
"has_more": false,
"next_cursor": null
}Retrieve a deal
GET /deals/{id} · Scope: read
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The deal id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— The deal. Returns Deal.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/deals/8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/deals/8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f', {
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/8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
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": "deal",
"id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"title": "Acme Corp — 3 locations",
"value": 4800,
"stage": "Proposal",
"status": "open",
"close_date": "2026-11-30",
"lost_reason": null,
"stage_id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
"agent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"external_id": "opp-5521",
"next_step_action": "Follow-up scheduled (WhatsApp)",
"next_step_date": "2026-10-24T15:00:00.000Z",
"created_at": "2026-10-20T09:15:00.000Z",
"updated_at": "2026-10-22T16:40:12.000Z"
}Quotes
Read-only in v1. Quotes include their line items.
List quotes
GET /quotes · Scope: read
Newest first (by created_at, then id). Filters combine with AND. Follow next_cursor until has_more is false.
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100. Default 25. 1 to 100. Default 25. |
cursor | query | string | The next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters. |
updated_since | query | string (date-time) | Only records whose updated_at is at or after this time. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z. |
status | query | string | Exact match on status. Common values: draft, sent, accepted, rejected. Max 50 characters. |
external_id | query | string | Exact match on the external_id of the quote (the id in your own system). Max 200 characters. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— A page of quotes.400—INVALID_REQUEST— a query parameter is invalid (unknown parameter, badlimit,updated_sinceorcursor).401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/quotes" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/quotes', {
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/quotes",
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){
"data": [
{
"object": "quote",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"number": "Q-1042",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "sent",
"subtotal": 1200,
"tax_total": 84,
"shipping_cost": 0,
"discount_total": 0,
"total": 1284,
"valid_until": "2026-11-15",
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "qt-7781",
"notes": "Prices valid for the 2026 rollout.",
"payment_terms": "Net 15",
"created_at": "2026-10-22T16:45:00.000Z",
"updated_at": "2026-10-22T16:47:31.000Z"
}
],
"has_more": false,
"next_cursor": null
}Retrieve a quote
GET /quotes/{id} · Scope: read
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The quote id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— The quote. Returns Quote.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/quotes/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/quotes/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d', {
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/quotes/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
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": "quote",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"number": "Q-1042",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "sent",
"subtotal": 1200,
"tax_total": 84,
"shipping_cost": 0,
"discount_total": 0,
"total": 1284,
"valid_until": "2026-11-15",
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "qt-7781",
"notes": "Prices valid for the 2026 rollout.",
"payment_terms": "Net 15",
"created_at": "2026-10-22T16:45:00.000Z",
"updated_at": "2026-10-22T16:47:31.000Z"
}Orders
Read-only in v1. Orders include their line items and payment status.
List orders
GET /orders · Scope: read
Newest first (by created_at, then id). Filters combine with AND. Follow next_cursor until has_more is false. Archived orders are NOT listed (you can still retrieve them by id, and webhooks report them).
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1 to 100. Default 25. 1 to 100. Default 25. |
cursor | query | string | The next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters. |
updated_since | query | string (date-time) | Only records whose updated_at is at or after this time. ISO 8601 with a time zone, e.g. 2026-10-19T00:00:00Z. |
status | query | string | Exact match on status. Common values: draft, pending, processing, shipped, delivered, cancelled. Max 50 characters. |
external_id | query | string | Exact match on the external_id of the order (the id in your own system). Max 200 characters. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— A page of orders.400—INVALID_REQUEST— a query parameter is invalid (unknown parameter, badlimit,updated_sinceorcursor).401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/orders" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/orders', {
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/orders",
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){
"data": [
{
"object": "order",
"id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
"number": "ORD-2088",
"quote_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "processing",
"payment_status": "paid",
"subtotal": 1200,
"tax": 84,
"shipping_cost": 0,
"total": 1284,
"tracking_number": null,
"carrier": null,
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "erp-so-3310",
"delivery_type": "pickup",
"shipping_address": null,
"billing_address": null,
"notes": null,
"amount_paid": 1284,
"created_at": "2026-10-24T10:05:00.000Z",
"updated_at": "2026-10-24T18:20:09.000Z"
}
],
"has_more": false,
"next_cursor": null
}Retrieve an order
GET /orders/{id} · Scope: read
Archived orders are returned too.
Scope: read.
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The order id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— The order. Returns Order.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/orders/9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/orders/9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a', {
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/orders/9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
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": "order",
"id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
"number": "ORD-2088",
"quote_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "processing",
"payment_status": "paid",
"subtotal": 1200,
"tax": 84,
"shipping_cost": 0,
"total": 1284,
"tracking_number": null,
"carrier": null,
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "erp-so-3310",
"delivery_type": "pickup",
"shipping_address": null,
"billing_address": null,
"notes": null,
"amount_paid": 1284,
"created_at": "2026-10-24T10:05:00.000Z",
"updated_at": "2026-10-24T18:20:09.000Z"
}Webhook subscriptions
Subscribe a URL to real-time events (REST Hooks). Every operation requires the write scope.
List webhook subscriptions
GET /subscriptions · Scope: write
Every webhook subscription of the organization — active and deactivated, created by any key or tool — newest first, up to 200. has_more is always false. Never includes the secret.
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— Subscriptions.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl "https://app.spacekru.com/api/public/v1/subscriptions" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/subscriptions', {
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/subscriptions",
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){
"data": [
{
"object": "subscription",
"id": "0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"event_type": "lead.created",
"target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
"is_active": true,
"failure_count": 0,
"disabled_reason": null,
"last_success_at": null,
"created_at": "2026-10-19T14:40:00.000Z",
"exclude_own_changes": false
}
],
"has_more": false,
"next_cursor": null
}Subscribe a URL to an event (REST Hook)
POST /subscriptions · Scope: write
The secret used to sign deliveries is returned ONLY in this response. Store it now.
target_url rules: https:// only, default port 443 only, no user or password in the URL, a public host (no localhost, .local, .internal or private/reserved IP addresses — checked again on every delivery). Redirects are never followed. Your endpoint must answer within 8 seconds.
At most 50 active subscriptions per organization (422 beyond that). One subscription = one event and one URL: subscribe the same URL several times for several events.
No backfill: only events that happen after you subscribe are sent. To load history, list the resource once.
If the key that created a subscription is revoked, the subscription is deactivated (disabled_reason: "key_revoked").
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Request body (SubscriptionCreate) Unknown fields are rejected.
| Field | Type | Description |
|---|---|---|
event_type (required) | string | One of: lead.created, lead.updated, deal.created, deal.stage_changed, deal.updated, quote.created, quote.status_changed, order.created, order.status_changed, campaign_lead.replied, campaign_lead.form_submitted, product.created, product.updated, booking.created, booking.updated, conversation.message_created. |
target_url (required) | string (uri) | Public https:// URL on port 443, without credentials. Internal hosts and private IP addresses are rejected. Redirects are not followed. Max 2000 characters. |
exclude_own_changes | boolean | Do not deliver changes made with THIS API key. Turn it on when your platform both writes through the API and listens to the same events. Default false. |
Responses
201— Created. The only response that includessecret. ReturnsSubscriptionCreated.400—INVALID_REQUEST— validation failed (details[]lists up to 20 fields), the body is not valid JSON, orContent-Typeis notapplication/json.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.413—PAYLOAD_TOO_LARGE— the body is over 1 MB.422—UNPROCESSABLE— valid JSON that could not be applied (e.g. too many active subscriptions, a value the database rejected).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
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": "lead.created",
"target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/"
}'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": "lead.created",
"target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/"
}),
});
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": "lead.created",
"target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
},
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": "subscription",
"id": "0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"event_type": "lead.created",
"target_url": "https://hooks.zapier.com/hooks/catch/1234567/abcdefg/",
"is_active": true,
"failure_count": 0,
"disabled_reason": null,
"last_success_at": null,
"created_at": "2026-10-19T14:40:00.000Z",
"exclude_own_changes": false,
"secret": "whsec_EXAMPLE0nlyN0tARealSecretDoNotUseIt00000"
}Unsubscribe
DELETE /subscriptions/{id} · Scope: write
Deletes the subscription permanently (any subscription of the organization, active or deactivated). Retries still pending for it are cancelled.
Scope: write (a read-only key answers 403 INSUFFICIENT_SCOPE).
| Parameter | In | Type | Description |
|---|---|---|---|
id (required) | path | string (uuid) | The subscription id. An id that is not a UUID, or that belongs to another organization, answers 404. |
Accept-Language | header | string | Language of error.message: en or es. Without it, the language of the profile of the person who created the key. Codes and field names never change. |
Responses
200— Deleted.401—INVALID_API_KEY— the key is missing, malformed, unknown or revoked.402—PLAN_UPGRADE_REQUIRED— the organization is not on an active paid plan (free plan, cancelled or unpaid subscription, closed account). No grace period.403—INSUFFICIENT_SCOPE— the key lacks the scope this operation needs.404—NOT_FOUND— no such record in this organization (also for ids that are not UUIDs).429—RATE_LIMITED— over 120 requests per minute or 10,000 per day for this key (defaults). WaitRetry-Afterseconds.500—INTERNAL_ERROR— unexpected error on our side. Safe to retry with backoff; quoterequest_idto support if it persists.503—SERVICE_BUSY(the server is at capacity) orELIGIBILITY_UNAVAILABLE(your plan could not be verified). Retry afterRetry-Afterseconds (5 or 10).
Example request
curl -X DELETE "https://app.spacekru.com/api/public/v1/subscriptions/0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d" \
-H "Authorization: Bearer $SPACEKRU_API_KEY"const res = await fetch('https://app.spacekru.com/api/public/v1/subscriptions/0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d', {
method: 'DELETE',
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.delete(
"https://app.spacekru.com/api/public/v1/subscriptions/0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
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": "subscription",
"id": "0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"deleted": true
}Objects
Lead
| Field | Type | Description |
|---|---|---|
object | "lead" | — |
id | string (uuid) | — |
type | string | One of: person, business. |
full_name | string or null | — |
business_name | string or null | — |
job_title | string or null | — |
work_email | string or null | — |
personal_email | string or null | — |
work_phone | string or null | — |
personal_phone | string or null | — |
company_name | string or null | — |
city | string or null | — |
country | string or null | — |
status | string or null | Managed by SpaceKru (read-only). Free text; common values: new, contacted, qualified, proposal, won. |
source | string or null | Where the lead came from (read-only). api for leads created through the API. |
tags | array of string | — |
instagram_profile | string or null | — |
linkedin_profile_url | string or null | — |
website | string or null | — |
external_id | string or null | The id of this lead in YOUR system (unique per organization). Use it to sync without storing SpaceKru ids. |
created_at | string (date-time) or null | When the lead was created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). Used by updated_since. |
Deal
| Field | Type | Description |
|---|---|---|
object | "deal" | — |
id | string (uuid) | — |
lead_id | string or null | — |
title | string or null | — |
value | number or null | — |
stage | string or null | Name of the pipeline stage. |
status | string or null | Free text; common values: open, won, lost. |
close_date | string or null | Calendar date YYYY-MM-DD or a date-time. |
lost_reason | string or null | — |
stage_id | string or null | Id of the pipeline stage (see GET /pipeline-stages). The reliable way to know and change the stage. |
agent_id | string or null | The AI Employee in charge of the deal (it does the follow-ups). |
external_id | string or null | The id of this deal in YOUR system. |
next_step_action | string or null | Next scheduled step, e.g. "Follow-up scheduled (WhatsApp)". |
next_step_date | string (date-time) or null | When the next step is scheduled (UTC). |
created_at | string (date-time) or null | When the deal was created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). |
Quote
| Field | Type | Description |
|---|---|---|
object | "quote" | — |
id | string (uuid) | — |
number | string or null | — |
lead_id | string or null | — |
deal_id | string or null | — |
status | string or null | Free text; common values: draft, sent, accepted, rejected. |
subtotal | number or null | — |
tax_total | number or null | — |
shipping_cost | number or null | — |
discount_total | number or null | — |
total | number or null | — |
valid_until | string or null | Calendar date, YYYY-MM-DD. |
items | array of LineItem | — |
external_id | string or null | The id of this quote in YOUR system. |
notes | string or null | — |
payment_terms | string or null | — |
created_at | string (date-time) or null | When the quote was created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). |
Order
| Field | Type | Description |
|---|---|---|
object | "order" | — |
id | string (uuid) | — |
number | string or null | — |
quote_id | string or null | — |
lead_id | string or null | — |
deal_id | string or null | — |
status | string or null | Free text; common values: draft, pending, processing, shipped, delivered, cancelled. |
payment_status | string or null | Free text; common values: unpaid, partial, paid, refunded. |
subtotal | number or null | — |
tax | number or null | — |
shipping_cost | number or null | — |
total | number or null | — |
tracking_number | string or null | — |
carrier | string or null | — |
items | array of LineItem | — |
external_id | string or null | The id of this order in YOUR system. |
delivery_type | string or null | pickup or delivery. |
shipping_address | object or null | — |
billing_address | object or null | — |
notes | string or null | — |
amount_paid | number | Sum of the recorded payments. |
created_at | string (date-time) or null | When the order was created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). |
Line item
| Field | Type | Description |
|---|---|---|
product_id | string or null | — |
name | string or null | — |
sku | string or null | — |
description | string or null | — |
quantity | number or null | — |
unit_price | number or null | — |
total | number or null | — |
variant_id | string or null | — |
tax_rate | number or null | Percent. |
discount | number or null | Percent. |
Payment
| Field | Type | Description |
|---|---|---|
object | "payment" | — |
id | string (uuid) | — |
order_id | string (uuid) | — |
amount | number | — |
payment_method | string or null | — |
reference_code | string or null | — |
payment_date | string (date-time) or null | When it was paid. |
created_at | string (date-time) or null | When it was recorded. |
order_payment_status | string or null | unpaid, partial or paid after this payment. |
Product
| Field | Type | Description |
|---|---|---|
object | "product" | — |
id | string (uuid) | — |
name | string | — |
sku | string or null | — |
reference_code | string or null | — |
category | string or null | — |
description | string or null | — |
brand | string or null | — |
price | number or null | — |
sale_price | number or null | — |
currency | string or null | ISO 4217, e.g. USD, COP, MXN. |
stock_quantity | integer or null | — |
track_stock | boolean | — |
status | string | One of: active, draft, archived. |
images | array of string | — |
url | string or null | — |
min_order_quantity | integer or null | — |
has_variants | boolean | — |
variants | array of ProductVariant | — |
external_id | string or null | — |
created_at | string (date-time) or null | Created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). |
Product variant
| Field | Type | Description |
|---|---|---|
object | "product_variant" | — |
id | string (uuid) | — |
sku | string or null | — |
title | string or null | — |
price | number or null | — |
stock_quantity | integer or null | — |
attributes | object or null | — |
AI Employee
| Field | Type | Description |
|---|---|---|
object | "agent" | — |
id | string (uuid) | — |
name | string | — |
role | string | customer_service, prospector, community_manager… |
status | string or null | — |
language | string or null | — |
channels | array of string | Channels turned on: whatsapp, email, web_chat, instagram… |
Pipeline stage
| Field | Type | Description |
|---|---|---|
object | "pipeline_stage" | — |
id | string (uuid) | — |
name | string | — |
type | string | One of: OPEN, WON, LOST. |
position | integer | — |
Campaign
| Field | Type | Description |
|---|---|---|
object | "campaign" | — |
id | string (uuid) | — |
name | string or null | — |
type | string | prospecting, recontact, inbound, email_marketing, whatsapp_marketing… |
status | string | — |
agent_id | string or null | — |
channels | array of string | — |
enrollable | boolean | true = leads can be added with POST /campaigns/{id}/leads. |
created_at | string (date-time) or null | When the campaign was created (UTC). |
Enrollment
| Field | Type | Description |
|---|---|---|
object | "enrollment" | — |
campaign_id | string (uuid) | — |
dry_run | boolean | — |
requested | integer | — |
enrolled | integer | Added (or that would be added, on a dry run). |
already_in_campaign | integer | — |
excluded | integer | — |
excluded_lead_ids | array of string (uuid) | — |
reasons | object | — |
Campaign lead
A lead inside one of your campaigns. Only delivered by the campaign_lead.* webhooks.
| Field | Type | Description |
|---|---|---|
object | "campaign_lead" | — |
id | string (uuid) | — |
campaign_id | string (uuid) | — |
lead_id | string or null | — |
status | string or null | Status inside the campaign. Free text; common values: new, contacted, replied, awaiting_supervisor, converted, not_interested, closed_unresponsive. |
form_submitted_at | string (date-time) or null | When the lead submitted the campaign form, if it did. |
lead | Lead or any or null | The lead itself. |
Follow-up
| Field | Type | Description |
|---|---|---|
object | "follow_up" | — |
deal_id | string (uuid) | — |
scheduled_message_id | string (uuid) | — |
scheduled_at | string (date-time) | Adjusted to the working hours of the AI Employee. |
channel | string | One of: whatsapp, email. |
Booking resource
| Field | Type | Description |
|---|---|---|
object | "booking_resource" | — |
id | string (uuid) | — |
name | string | — |
capacity | integer or null | — |
category | string or null | — |
zone | string or null | — |
is_active | boolean | — |
min_slot_duration | integer or null | Minutes. |
Availability slot
| Field | Type | Description |
|---|---|---|
object | "availability_slot" | — |
start_time | string (date-time) | — |
end_time | string (date-time) | — |
resource | object | — |
Booking
| Field | Type | Description |
|---|---|---|
object | "booking" | — |
id | string (uuid) | — |
resource_id | string or null | — |
lead_id | string or null | — |
start_time | string (date-time) | — |
end_time | string (date-time) | — |
status | string | One of: confirmed, cancelled, seated, completed. |
party_size | integer or null | — |
notes | string or null | — |
meeting_url | string or null | — |
external_id | string or null | — |
created_at | string (date-time) or null | Created (UTC). |
updated_at | string (date-time) or null | Last change (UTC). |
Conversation
| Field | Type | Description |
|---|---|---|
object | "conversation" | — |
id | string (uuid) | — |
lead_id | string or null | — |
status | string | One of: open, closed. |
external_conversation_id | string or null | — |
locale | string or null | — |
message_count | integer | — |
last_activity_at | string (date-time) or null | Last message (UTC). |
created_at | string (date-time) or null | Opened (UTC). |
Conversation message
| Field | Type | Description |
|---|---|---|
object | "conversation_message" | — |
id | string (uuid) | — |
conversation_id | string (uuid) | — |
role | string | contact = your customer, agent = the AI Employee, human = a person replying from the SpaceKru Inbox. One of: contact, agent, human. |
text | string or null | — |
created_at | string (date-time) or null | When it was written (UTC). |
Conversation turn
| Field | Type | Description |
|---|---|---|
object | "conversation_turn" | — |
conversation_id | string (uuid) | — |
messages | array of ConversationMessage | The AI Employee's replies to this message (it can send more than one). |
deferred | boolean | true = no reply now. |
deferred_reason | string or null | One of: no_inbound_campaign, agent_not_ready, replies_disabled, message_filtered, campaign_inactive, no_reply. |
Subscription
| Field | Type | Description |
|---|---|---|
object | "subscription" | — |
id | string (uuid) | — |
event_type | string | One of: lead.created, lead.updated, deal.created, deal.stage_changed, deal.updated, quote.created, quote.status_changed, order.created, order.status_changed, campaign_lead.replied, campaign_lead.form_submitted, product.created, product.updated, booking.created, booking.updated, conversation.message_created. |
target_url | string | — |
is_active | boolean | false once deactivated (see disabled_reason). A deactivated subscription is never reactivated: delete it and create a new one. |
failure_count | integer | Consecutive failed delivery attempts. Any success resets it to 0. |
disabled_reason | string or null | Why it was deactivated: target_gone_410 — Your endpoint answered 410 Gone. disabled_after_20_consecutive_failures — Too many consecutive failed delivery attempts (the number is in the value, e.g. disabled_after_20_consecutive_failures). key_revoked — The API key that created the subscription was revoked. |
last_success_at | string (date-time) or null | Last successful delivery. |
created_at | string (date-time) | — |
exclude_own_changes | boolean | true = changes made with the API key that created this subscription are not delivered to it (no echo in two-way syncs). |
Note
| Field | Type | Description |
|---|---|---|
object | "note" | — |
id | string | — |
lead_id | string | — |
content | string | — |
created_at | string (date-time) or null | When the note was added (UTC). |
Error
The only error shape of the API.
| Field | Type | Description |
|---|---|---|
error (required) | object | — |
Webhook events
Each event is POSTed to the target_url of its subscriptions. Signatures, retries and guarantees: Webhooks.
lead.created
Fires when: A lead is created from any source: the app, a CSV import, a form, the Lead Finder, an inbound message or the API.
`data`: the lead as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "lead.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "api",
"api_key_id": "0b6c1d2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"
},
"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"
}
}lead.updated
Fires when: One of these fields of a lead changes: type, full_name, business_name, job_title, work_email, personal_email, work_phone, personal_phone, company_name, status, tags, city, country, instagram_profile, linkedin_profile_url, website, external_id. Changes to any other field (for example internal follow-up counters) never fire it.
`data`: the lead as it is when the event is delivered.
`changes`: { "field": [old, new] } for each field that changed. Values are the raw stored values (for example tags exactly as stored), and only those 17 fields appear.
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "lead.updated",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"status": [
"new",
"contacted"
],
"job_title": [
"Operations Manager",
"Head of Operations"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "lead",
"id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"type": "person",
"full_name": "Maria Lopez",
"business_name": null,
"job_title": "Head of Operations",
"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": "contacted",
"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"
}
}deal.created
Fires when: A deal is created: in the app, by an AI Employee, from a quote or through the API.
`data`: the deal as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "deal.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "api",
"api_key_id": "0b6c1d2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"
},
"data": {
"object": "deal",
"id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"title": "Acme Corp — 3 locations",
"value": 4800,
"stage": "Proposal",
"status": "open",
"close_date": "2026-11-30",
"lost_reason": null,
"stage_id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
"agent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"external_id": "opp-5521",
"next_step_action": "Follow-up scheduled (WhatsApp)",
"next_step_date": "2026-10-24T15:00:00.000Z",
"created_at": "2026-10-20T09:15:00.000Z",
"updated_at": "2026-10-22T16:40:12.000Z"
}
}deal.stage_changed
Fires when: A deal's pipeline stage changes, or its status changes (open, won, lost). Use it for "deal won" and "deal lost".
`data`: the deal as it is when the event is delivered.
`changes`: stage, stage_id and/or status, each as [old, new].
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "deal.stage_changed",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"stage": [
"Proposal",
"Won"
],
"stage_id": [
"c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80"
],
"status": [
"open",
"won"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "deal",
"id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"title": "Acme Corp — 3 locations",
"value": 5200,
"stage": "Won",
"status": "won",
"close_date": "2026-10-28T17:02:00.000Z",
"lost_reason": null,
"stage_id": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
"agent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"external_id": "opp-5521",
"next_step_action": null,
"next_step_date": null,
"created_at": "2026-10-20T09:15:00.000Z",
"updated_at": "2026-10-28T17:02:00.000Z"
}
}deal.updated
Fires when: One of these stored fields of a deal changes WITHOUT a stage or status change: deal_title, deal_value, agent_id, close_date, lost_reason, next_step_action, next_step_date, external_id. (deal_title and deal_value are title and value in the Deal object.)
`data`: the deal as it is when the event is delivered.
`changes`: { "field": [old, new] } with the stored field names listed above.
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "deal.updated",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"deal_value": [
4800,
5200
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "deal",
"id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"title": "Acme Corp — 3 locations",
"value": 5200,
"stage": "Proposal",
"status": "open",
"close_date": "2026-11-30",
"lost_reason": null,
"stage_id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
"agent_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5e",
"external_id": "opp-5521",
"next_step_action": "Follow-up scheduled (WhatsApp)",
"next_step_date": "2026-10-24T15:00:00.000Z",
"created_at": "2026-10-20T09:15:00.000Z",
"updated_at": "2026-10-22T16:40:12.000Z"
}
}quote.created
Fires when: A quote is created.
`data`: the quote as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "quote.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "quote",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"number": "Q-1042",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "sent",
"subtotal": 1200,
"tax_total": 84,
"shipping_cost": 0,
"discount_total": 0,
"total": 1284,
"valid_until": "2026-11-15",
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "qt-7781",
"notes": "Prices valid for the 2026 rollout.",
"payment_terms": "Net 15",
"created_at": "2026-10-22T16:45:00.000Z",
"updated_at": "2026-10-22T16:47:31.000Z"
}
}quote.status_changed
Fires when: A quote's status changes (for example from sent to accepted).
`data`: the quote as it is when the event is delivered.
`changes`: status as [old, new].
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "quote.status_changed",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"status": [
"sent",
"accepted"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "quote",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"number": "Q-1042",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "accepted",
"subtotal": 1200,
"tax_total": 84,
"shipping_cost": 0,
"discount_total": 0,
"total": 1284,
"valid_until": "2026-11-15",
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "qt-7781",
"notes": "Prices valid for the 2026 rollout.",
"payment_terms": "Net 15",
"created_at": "2026-10-22T16:45:00.000Z",
"updated_at": "2026-10-22T16:47:31.000Z"
}
}order.created
Fires when: An order is created.
`data`: the order as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "order.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "order",
"id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
"number": "ORD-2088",
"quote_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "pending",
"payment_status": "unpaid",
"subtotal": 1200,
"tax": 84,
"shipping_cost": 0,
"total": 1284,
"tracking_number": null,
"carrier": null,
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "erp-so-3310",
"delivery_type": "pickup",
"shipping_address": null,
"billing_address": null,
"notes": null,
"amount_paid": 0,
"created_at": "2026-10-24T10:05:00.000Z",
"updated_at": "2026-10-24T18:20:09.000Z"
}
}order.status_changed
Fires when: An order's status changes, or its payment status changes (for example to paid).
`data`: the order as it is when the event is delivered.
`changes`: status and/or payment_status, each as [old, new].
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "order.status_changed",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"payment_status": [
"unpaid",
"paid"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "order",
"id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
"number": "ORD-2088",
"quote_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"deal_id": "8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f",
"status": "processing",
"payment_status": "paid",
"subtotal": 1200,
"tax": 84,
"shipping_cost": 0,
"total": 1284,
"tracking_number": null,
"carrier": null,
"items": [
{
"product_id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Annual plan — per location",
"sku": "PLAN-LOC-12",
"description": null,
"quantity": 3,
"unit_price": 400,
"total": 1284,
"variant_id": null,
"tax_rate": 7,
"discount": 0
}
],
"external_id": "erp-so-3310",
"delivery_type": "pickup",
"shipping_address": null,
"billing_address": null,
"notes": null,
"amount_paid": 1284,
"created_at": "2026-10-24T10:05:00.000Z",
"updated_at": "2026-10-24T18:20:09.000Z"
}
}campaign_lead.replied
Fires when: A lead in one of your campaigns replies (its campaign status becomes replied).
`data`: the campaign lead as it is when the event is delivered.
`changes`: status as [old, "replied"].
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "campaign_lead.replied",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"status": [
"contacted",
"replied"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "campaign_lead",
"id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
"campaign_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"status": "replied",
"form_submitted_at": null,
"lead": {
"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"
}
}
}campaign_lead.form_submitted
Fires when: A lead in one of your campaigns submits the campaign's form.
`data`: the campaign lead as it is when the event is delivered.
`changes`: {} (always empty). data.form_submitted_at has the time.
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "campaign_lead.form_submitted",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "campaign_lead",
"id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
"campaign_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"status": "contacted",
"form_submitted_at": "2026-10-19T14:31:58.000Z",
"lead": {
"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"
}
}
}product.created
Fires when: A product is created: in the app, by a CSV import, by an AI Employee or through the API.
`data`: the product as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "product.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "product",
"id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Dining chair",
"sku": "CHAIR-OAK",
"reference_code": null,
"category": "Furniture",
"description": "Solid oak dining chair.",
"brand": "Acme Home",
"price": 199,
"sale_price": 179,
"currency": "USD",
"stock_quantity": 40,
"track_stock": true,
"status": "active",
"images": [
"https://cdn.example.com/chair.jpg"
],
"url": "https://shop.example.com/chair",
"min_order_quantity": 1,
"has_variants": true,
"variants": [
{
"object": "product_variant",
"id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"sku": "CHAIR-OAK-BLK",
"title": "Oak / Black",
"price": 189,
"stock_quantity": 12,
"attributes": {
"wood": "Oak",
"color": "Black"
}
}
],
"external_id": "sku-88731",
"created_at": "2026-09-02T12:00:00.000Z",
"updated_at": "2026-10-19T09:30:00.000Z"
}
}product.updated
Fires when: One of these fields of a product changes: name, sku, reference_code, category, description, brand, price, sale_price, currency, stock_quantity, track_stock, status, images, url, min_order_quantity, external_id. Variant changes do not fire it.
`data`: the product as it is when the event is delivered.
`changes`: { "field": [old, new] } for each field that changed.
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "product.updated",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"stock_quantity": [
40,
35
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "product",
"id": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"name": "Dining chair",
"sku": "CHAIR-OAK",
"reference_code": null,
"category": "Furniture",
"description": "Solid oak dining chair.",
"brand": "Acme Home",
"price": 199,
"sale_price": 179,
"currency": "USD",
"stock_quantity": 35,
"track_stock": true,
"status": "active",
"images": [
"https://cdn.example.com/chair.jpg"
],
"url": "https://shop.example.com/chair",
"min_order_quantity": 1,
"has_variants": true,
"variants": [
{
"object": "product_variant",
"id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"sku": "CHAIR-OAK-BLK",
"title": "Oak / Black",
"price": 189,
"stock_quantity": 12,
"attributes": {
"wood": "Oak",
"color": "Black"
}
}
],
"external_id": "sku-88731",
"created_at": "2026-09-02T12:00:00.000Z",
"updated_at": "2026-10-19T09:30:00.000Z"
}
}booking.created
Fires when: A booking is created: in the app, by an AI Employee or through the API.
`data`: the booking as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "booking.created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "api",
"api_key_id": "0b6c1d2e-3f4a-4b5c-9d6e-7f8a9b0c1d2e"
},
"data": {
"object": "booking",
"id": "f6a7b8c9-d0e1-4f2a-9b3c-5d6e7f809102",
"resource_id": "e5f6a7b8-c9d0-4e1f-8a2b-4c5d6e7f8091",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"start_time": "2026-10-25T00:00:00.000Z",
"end_time": "2026-10-25T01:30:00.000Z",
"status": "confirmed",
"party_size": 4,
"notes": "Birthday",
"meeting_url": null,
"external_id": "pos-res-1042",
"created_at": "2026-10-20T14:00:00.000Z",
"updated_at": "2026-10-20T14:00:00.000Z"
}
}booking.updated
Fires when: One of these fields of a booking changes: resource_id, lead_id, start_time, end_time, status, party_size, notes, meeting_url, external_id.
`data`: the booking as it is when the event is delivered.
`changes`: { "field": [old, new] } for each field that changed.
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "booking.updated",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {
"status": [
"confirmed",
"seated"
]
},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "booking",
"id": "f6a7b8c9-d0e1-4f2a-9b3c-5d6e7f809102",
"resource_id": "e5f6a7b8-c9d0-4e1f-8a2b-4c5d6e7f8091",
"lead_id": "3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b",
"start_time": "2026-10-25T00:00:00.000Z",
"end_time": "2026-10-25T01:30:00.000Z",
"status": "seated",
"party_size": 4,
"notes": "Birthday",
"meeting_url": null,
"external_id": "pos-res-1042",
"created_at": "2026-10-20T14:00:00.000Z",
"updated_at": "2026-10-20T14:00:00.000Z"
}
}conversation.message_created
Fires when: The AI Employee, or a person replying from the Inbox, writes in a conversation that was opened through the API.
`data`: the conversation message as it is when the event is delivered.
`changes`: {} (always empty).
{
"id": "e4d3c2b1-a098-4765-8432-10fedcba9876",
"event": "conversation.message_created",
"occurred_at": "2026-10-19T14:32:11.000Z",
"organization_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"changes": {},
"triggered_by": {
"type": "app",
"api_key_id": null
},
"data": {
"object": "conversation_message",
"id": "29304b5c-6d7e-4f80-91a2-b3c4d5e6f708",
"conversation_id": "18293a4b-5c6d-4e7f-8091-a2b3c4d5e6f7",
"role": "human",
"text": "Hi Maria, this is Ana from the team. Your table is ready.",
"created_at": "2026-10-20T15:00:06.000Z"
}
}