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.

Leer en español

Base URL & authentication

https://app.spacekru.com/api/public/v1
Authorization: Bearer $SPACEKRU_API_KEY

All 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.

ParameterInTypeDescription
Accept-LanguageheaderstringLanguage 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. Returns Ping.
  • 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/ping" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
limitqueryintegerPage size, 1 to 100. Default 25. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters.
updated_sincequerystring (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.
statusquerystringExact match on status. Common values: new, contacted, qualified, proposal, won. Max 50 characters.
typequerystringOnly people or only businesses. One of: person, business.
external_idquerystringExact match on the external_id of the lead (the id in your own system). Max 200 characters.
Accept-LanguageheaderstringLanguage 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, bad limit, updated_since or cursor).
  • 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/leads" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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).

ParameterInTypeDescription
Idempotency-KeyheaderstringAny unique string per logical request, e.g. a UUID or your form response id. Max 255 characters.
Accept-LanguageheaderstringLanguage 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.

FieldTypeDescription
full_namestringMax 200 characters.
business_namestringMax 200 characters.
job_titlestringMax 200 characters.
work_emailstring (email)Max 320 characters.
personal_emailstring (email)Max 320 characters.
work_phonestringMax 40 characters.
personal_phonestringMax 40 characters.
company_namestringMax 200 characters.
citystringMax 120 characters.
countrystringMax 120 characters.
websitestringMax 500 characters.
instagram_profilestringMax 300 characters.
linkedin_profile_urlstringMax 500 characters.
typestringOne of: person, business.
tagsarray of stringUp to 20 tags of up to 50 characters. A comma-separated string is also accepted (e.g. "typeform, vip"). Up to 20 items.
initial_notestringAdded to the lead's timeline. Max 5000 characters.
external_idstringYour id for this lead (unique per organization). Max 200 characters.
campaign_idstring (uuid)Add the new lead to this prospecting or recontact campaign.

Responses

  • 201 — Created. On a replay, the original response. With campaign_id, it also has enrollment. Returns Lead.
  • 400 — INVALID_REQUEST — validation failed (details[] lists up to 20 fields), the body is not valid JSON, or Content-Type is not application/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: see existing_id) or IDEMPOTENCY_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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
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."
}'
Node.js
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);
Python
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)
Response 201
{
  "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.

ParameterInTypeDescription
id (required)pathstring (uuid)The lead id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/leads/3f2b8c1e-9a4d-4e6f-b7a8-1c2d3e4f5a6b" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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).

ParameterInTypeDescription
id (required)pathstring (uuid)The lead id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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.

FieldTypeDescription
full_namestringMax 200 characters.
business_namestringMax 200 characters.
job_titlestringMax 200 characters.
work_emailstring (email)Max 320 characters.
personal_emailstring (email)Max 320 characters.
work_phonestringMax 40 characters.
personal_phonestringMax 40 characters.
company_namestringMax 200 characters.
citystringMax 120 characters.
countrystringMax 120 characters.
websitestringMax 500 characters.
instagram_profilestringMax 300 characters.
linkedin_profile_urlstringMax 500 characters.
typestringOne of: person, business.
tagsarray of stringUp to 20 tags of up to 50 characters. A comma-separated string is also accepted (e.g. "typeform, vip"). Up to 20 items.
external_idstring or nullYour 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, or Content-Type is not application/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 this external_id (see existing_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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
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"
  ]
}'
Node.js
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);
Python
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)
Response 200
{
  "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).

ParameterInTypeDescription
id (required)pathstring (uuid)The lead id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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.

FieldTypeDescription
content (required)stringMax 5000 characters.

Responses

  • 201 — Created. Returns Note.
  • 400 — INVALID_REQUEST — validation failed (details[] lists up to 20 fields), the body is not valid JSON, or Content-Type is not application/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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
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."
}'
Node.js
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);
Python
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)
Response 201
{
  "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.

ParameterInTypeDescription
limitqueryintegerPage size, 1 to 100. Default 25. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters.
updated_sincequerystring (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.
statusquerystringExact match on status. Common values: open, won, lost. Max 50 characters.
external_idquerystringExact match on the external_id of the deal (the id in your own system). Max 200 characters.
lead_idquerystring (uuid)Only the deals of this lead.
stage_idquerystring (uuid)Only the deals in this pipeline stage (see GET /pipeline-stages).
Accept-LanguageheaderstringLanguage 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, bad limit, updated_since or cursor).
  • 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/deals" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
id (required)pathstring (uuid)The deal id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/deals/8d1e2f3a-4b5c-4d6e-8f70-9a1b2c3d4e5f" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
limitqueryintegerPage size, 1 to 100. Default 25. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters.
updated_sincequerystring (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.
statusquerystringExact match on status. Common values: draft, sent, accepted, rejected. Max 50 characters.
external_idquerystringExact match on the external_id of the quote (the id in your own system). Max 200 characters.
Accept-LanguageheaderstringLanguage 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, bad limit, updated_since or cursor).
  • 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/quotes" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
id (required)pathstring (uuid)The quote id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/quotes/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
limitqueryintegerPage size, 1 to 100. Default 25. 1 to 100. Default 25.
cursorquerystringThe next_cursor of the previous page. Opaque: do not build or edit it (an invalid cursor answers 400). Max 500 characters.
updated_sincequerystring (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.
statusquerystringExact match on status. Common values: draft, pending, processing, shipped, delivered, cancelled. Max 50 characters.
external_idquerystringExact match on the external_id of the order (the id in your own system). Max 200 characters.
Accept-LanguageheaderstringLanguage 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, bad limit, updated_since or cursor).
  • 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/orders" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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.

ParameterInTypeDescription
id (required)pathstring (uuid)The order id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/orders/9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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).

ParameterInTypeDescription
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl "https://app.spacekru.com/api/public/v1/subscriptions" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "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).

ParameterInTypeDescription
Accept-LanguageheaderstringLanguage 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.

FieldTypeDescription
event_type (required)stringOne 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_changesbooleanDo 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 includes secret. Returns SubscriptionCreated.
  • 400 — INVALID_REQUEST — validation failed (details[] lists up to 20 fields), the body is not valid JSON, or Content-Type is not application/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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
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/"
}'
Node.js
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);
Python
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)
Response 201
{
  "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).

ParameterInTypeDescription
id (required)pathstring (uuid)The subscription id. An id that is not a UUID, or that belongs to another organization, answers 404.
Accept-LanguageheaderstringLanguage 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). Wait Retry-After seconds.
  • 500 — INTERNAL_ERROR — unexpected error on our side. Safe to retry with backoff; quote request_id to support if it persists.
  • 503 — SERVICE_BUSY (the server is at capacity) or ELIGIBILITY_UNAVAILABLE (your plan could not be verified). Retry after Retry-After seconds (5 or 10).

Example request

curl
curl -X DELETE "https://app.spacekru.com/api/public/v1/subscriptions/0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d" \
  -H "Authorization: Bearer $SPACEKRU_API_KEY"
Node.js
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);
Python
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)
Response 200
{
  "object": "subscription",
  "id": "0a1b2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
  "deleted": true
}

Objects

Lead

FieldTypeDescription
object"lead"—
idstring (uuid)—
typestringOne of: person, business.
full_namestring or null—
business_namestring or null—
job_titlestring or null—
work_emailstring or null—
personal_emailstring or null—
work_phonestring or null—
personal_phonestring or null—
company_namestring or null—
citystring or null—
countrystring or null—
statusstring or nullManaged by SpaceKru (read-only). Free text; common values: new, contacted, qualified, proposal, won.
sourcestring or nullWhere the lead came from (read-only). api for leads created through the API.
tagsarray of string—
instagram_profilestring or null—
linkedin_profile_urlstring or null—
websitestring or null—
external_idstring or nullThe id of this lead in YOUR system (unique per organization). Use it to sync without storing SpaceKru ids.
created_atstring (date-time) or nullWhen the lead was created (UTC).
updated_atstring (date-time) or nullLast change (UTC). Used by updated_since.

Deal

FieldTypeDescription
object"deal"—
idstring (uuid)—
lead_idstring or null—
titlestring or null—
valuenumber or null—
stagestring or nullName of the pipeline stage.
statusstring or nullFree text; common values: open, won, lost.
close_datestring or nullCalendar date YYYY-MM-DD or a date-time.
lost_reasonstring or null—
stage_idstring or nullId of the pipeline stage (see GET /pipeline-stages). The reliable way to know and change the stage.
agent_idstring or nullThe AI Employee in charge of the deal (it does the follow-ups).
external_idstring or nullThe id of this deal in YOUR system.
next_step_actionstring or nullNext scheduled step, e.g. "Follow-up scheduled (WhatsApp)".
next_step_datestring (date-time) or nullWhen the next step is scheduled (UTC).
created_atstring (date-time) or nullWhen the deal was created (UTC).
updated_atstring (date-time) or nullLast change (UTC).

Quote

FieldTypeDescription
object"quote"—
idstring (uuid)—
numberstring or null—
lead_idstring or null—
deal_idstring or null—
statusstring or nullFree text; common values: draft, sent, accepted, rejected.
subtotalnumber or null—
tax_totalnumber or null—
shipping_costnumber or null—
discount_totalnumber or null—
totalnumber or null—
valid_untilstring or nullCalendar date, YYYY-MM-DD.
itemsarray of LineItem—
external_idstring or nullThe id of this quote in YOUR system.
notesstring or null—
payment_termsstring or null—
created_atstring (date-time) or nullWhen the quote was created (UTC).
updated_atstring (date-time) or nullLast change (UTC).

Order

FieldTypeDescription
object"order"—
idstring (uuid)—
numberstring or null—
quote_idstring or null—
lead_idstring or null—
deal_idstring or null—
statusstring or nullFree text; common values: draft, pending, processing, shipped, delivered, cancelled.
payment_statusstring or nullFree text; common values: unpaid, partial, paid, refunded.
subtotalnumber or null—
taxnumber or null—
shipping_costnumber or null—
totalnumber or null—
tracking_numberstring or null—
carrierstring or null—
itemsarray of LineItem—
external_idstring or nullThe id of this order in YOUR system.
delivery_typestring or nullpickup or delivery.
shipping_addressobject or null—
billing_addressobject or null—
notesstring or null—
amount_paidnumberSum of the recorded payments.
created_atstring (date-time) or nullWhen the order was created (UTC).
updated_atstring (date-time) or nullLast change (UTC).

Line item

FieldTypeDescription
product_idstring or null—
namestring or null—
skustring or null—
descriptionstring or null—
quantitynumber or null—
unit_pricenumber or null—
totalnumber or null—
variant_idstring or null—
tax_ratenumber or nullPercent.
discountnumber or nullPercent.

Payment

FieldTypeDescription
object"payment"—
idstring (uuid)—
order_idstring (uuid)—
amountnumber—
payment_methodstring or null—
reference_codestring or null—
payment_datestring (date-time) or nullWhen it was paid.
created_atstring (date-time) or nullWhen it was recorded.
order_payment_statusstring or nullunpaid, partial or paid after this payment.

Product

FieldTypeDescription
object"product"—
idstring (uuid)—
namestring—
skustring or null—
reference_codestring or null—
categorystring or null—
descriptionstring or null—
brandstring or null—
pricenumber or null—
sale_pricenumber or null—
currencystring or nullISO 4217, e.g. USD, COP, MXN.
stock_quantityinteger or null—
track_stockboolean—
statusstringOne of: active, draft, archived.
imagesarray of string—
urlstring or null—
min_order_quantityinteger or null—
has_variantsboolean—
variantsarray of ProductVariant—
external_idstring or null—
created_atstring (date-time) or nullCreated (UTC).
updated_atstring (date-time) or nullLast change (UTC).

Product variant

FieldTypeDescription
object"product_variant"—
idstring (uuid)—
skustring or null—
titlestring or null—
pricenumber or null—
stock_quantityinteger or null—
attributesobject or null—

AI Employee

FieldTypeDescription
object"agent"—
idstring (uuid)—
namestring—
rolestringcustomer_service, prospector, community_manager…
statusstring or null—
languagestring or null—
channelsarray of stringChannels turned on: whatsapp, email, web_chat, instagram…

Pipeline stage

FieldTypeDescription
object"pipeline_stage"—
idstring (uuid)—
namestring—
typestringOne of: OPEN, WON, LOST.
positioninteger—

Campaign

FieldTypeDescription
object"campaign"—
idstring (uuid)—
namestring or null—
typestringprospecting, recontact, inbound, email_marketing, whatsapp_marketing…
statusstring—
agent_idstring or null—
channelsarray of string—
enrollablebooleantrue = leads can be added with POST /campaigns/{id}/leads.
created_atstring (date-time) or nullWhen the campaign was created (UTC).

Enrollment

FieldTypeDescription
object"enrollment"—
campaign_idstring (uuid)—
dry_runboolean—
requestedinteger—
enrolledintegerAdded (or that would be added, on a dry run).
already_in_campaigninteger—
excludedinteger—
excluded_lead_idsarray of string (uuid)—
reasonsobject—

Campaign lead

A lead inside one of your campaigns. Only delivered by the campaign_lead.* webhooks.

FieldTypeDescription
object"campaign_lead"—
idstring (uuid)—
campaign_idstring (uuid)—
lead_idstring or null—
statusstring or nullStatus inside the campaign. Free text; common values: new, contacted, replied, awaiting_supervisor, converted, not_interested, closed_unresponsive.
form_submitted_atstring (date-time) or nullWhen the lead submitted the campaign form, if it did.
leadLead or any or nullThe lead itself.

Follow-up

FieldTypeDescription
object"follow_up"—
deal_idstring (uuid)—
scheduled_message_idstring (uuid)—
scheduled_atstring (date-time)Adjusted to the working hours of the AI Employee.
channelstringOne of: whatsapp, email.

Booking resource

FieldTypeDescription
object"booking_resource"—
idstring (uuid)—
namestring—
capacityinteger or null—
categorystring or null—
zonestring or null—
is_activeboolean—
min_slot_durationinteger or nullMinutes.

Availability slot

FieldTypeDescription
object"availability_slot"—
start_timestring (date-time)—
end_timestring (date-time)—
resourceobject—

Booking

FieldTypeDescription
object"booking"—
idstring (uuid)—
resource_idstring or null—
lead_idstring or null—
start_timestring (date-time)—
end_timestring (date-time)—
statusstringOne of: confirmed, cancelled, seated, completed.
party_sizeinteger or null—
notesstring or null—
meeting_urlstring or null—
external_idstring or null—
created_atstring (date-time) or nullCreated (UTC).
updated_atstring (date-time) or nullLast change (UTC).

Conversation

FieldTypeDescription
object"conversation"—
idstring (uuid)—
lead_idstring or null—
statusstringOne of: open, closed.
external_conversation_idstring or null—
localestring or null—
message_countinteger—
last_activity_atstring (date-time) or nullLast message (UTC).
created_atstring (date-time) or nullOpened (UTC).

Conversation message

FieldTypeDescription
object"conversation_message"—
idstring (uuid)—
conversation_idstring (uuid)—
rolestringcontact = your customer, agent = the AI Employee, human = a person replying from the SpaceKru Inbox. One of: contact, agent, human.
textstring or null—
created_atstring (date-time) or nullWhen it was written (UTC).

Conversation turn

FieldTypeDescription
object"conversation_turn"—
conversation_idstring (uuid)—
messagesarray of ConversationMessageThe AI Employee's replies to this message (it can send more than one).
deferredbooleantrue = no reply now.
deferred_reasonstring or nullOne of: no_inbound_campaign, agent_not_ready, replies_disabled, message_filtered, campaign_inactive, no_reply.

Subscription

FieldTypeDescription
object"subscription"—
idstring (uuid)—
event_typestringOne 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_urlstring—
is_activebooleanfalse once deactivated (see disabled_reason). A deactivated subscription is never reactivated: delete it and create a new one.
failure_countintegerConsecutive failed delivery attempts. Any success resets it to 0.
disabled_reasonstring or nullWhy 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_atstring (date-time) or nullLast successful delivery.
created_atstring (date-time)—
exclude_own_changesbooleantrue = changes made with the API key that created this subscription are not delivered to it (no echo in two-way syncs).

Note

FieldTypeDescription
object"note"—
idstring—
lead_idstring—
contentstring—
created_atstring (date-time) or nullWhen the note was added (UTC).

Error

The only error shape of the API.

FieldTypeDescription
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).

Payload
{
  "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.

Payload
{
  "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).

Payload
{
  "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].

Payload
{
  "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.

Payload
{
  "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).

Payload
{
  "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].

Payload
{
  "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).

Payload
{
  "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].

Payload
{
  "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"].

Payload
{
  "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.

Payload
{
  "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).

Payload
{
  "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.

Payload
{
  "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).

Payload
{
  "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.

Payload
{
  "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).

Payload
{
  "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"
  }
}