The Kommandr OS API

A REST API over your own data — inventory, catches, contacts — plus signed webhooks so another system can react the moment something happens. Your business does not end at our walls.

Base URL
https://www.kommandros.com/api/v1

Authentication

Every request carries an API key. Create one in Settings → Integrations → API keys. Keys are shown once at creation and stored only as a hash. If you lose one, revoke it and make another.

curl https://www.kommandros.com/api/v1/me \
  -H "Authorization: Bearer kmdr_live_..."

# Or, if your platform sends a plain header:
curl https://www.kommandros.com/api/v1/me -H "X-API-Key: kmdr_live_..."

GET /me is the endpoint to test a connection against. It returns your account identity and the key's scope.

Endpoints

GET/meneeds read

Who this key belongs to. Use it to test a connection.

Returns: { id, email, business_name, scope }

GET/itemsneeds read

Inventory. Filter by status, category and age; sort by age or value.

Query: status, category, age_gt, sort, limit, cursor

Returns: { data: [ { id, title, category, status, cost_cents, list_price_cents, days_listed, channels } ], next_cursor }

GET/catchesneeds read

Listings Deal Sniper has found for your hunt filters.

Query: status, since, limit, cursor

Returns: { data: [ { id, title, price_cents, location, distance_miles, url, photo_url, found_at } ], next_cursor }

GET/contactsneeds read

Your buyers and leads.

Query: limit, cursor

Returns: { data: [ { id, name, email, phone, status, source, pipeline_stage } ], next_cursor }

POST/contactsneeds write

Add a lead. Needs at least one of name, email or phone.

Body: { name?, email?, phone?, source?, notes? }

Returns: The created contact.

Marketing and SMS consent are never set through the API. Consent is something a person gives, not a field an integration writes.

GET/hooksneeds read

Your webhook subscriptions, with their health.

Returns: { data: [ { id, event, target_url, active, failures, last_error, last_success_at } ] }

POST/hooksneeds write

Subscribe an https URL to an event.

Body: { event, target_url }

Returns: { id, event, target_url, secret } — the secret is shown once and never again.

DELETE/hooks/{id}needs write

Unsubscribe. Returns 204 whether or not the subscription existed.

Returns: 204 No Content

Webhooks

Subscribe an https endpoint to an event and we POST to it when the event happens. Every delivery is signed, retried with backoff on 5xx, and a subscription that fails repeatedly is switched off with the reason recorded — so a stopped integration has an answer rather than a silence.

Events

  • catch.createdNew catch found
  • listing.postedNew listing posted
  • listing.soldListing sold
  • listing.failedPosting failed or needs reconnect
  • message.receivedNew message received
  • sale.createdNew sale recorded
  • payment.receivedPayment received
  • job.scheduledPickup or delivery scheduled

Subscribing

curl -X POST https://www.kommandros.com/api/v1/hooks \
  -H "Authorization: Bearer kmdr_live_..." \
  -H "Content-Type: application/json" \
  -d '{"event":"listing.sold","target_url":"https://example.com/hooks/kommandr"}'

# → { "id": "...", "event": "listing.sold", "secret": "whsec_..." }
#   The secret is shown once. Store it.

Payload

{
  "event": "listing.sold",
  "id": "b0f1...",                       // unique per delivery — use it to dedupe
  "occurred_at": "2026-08-25T14:03:11.000Z",
  "data": { ... }
}

Verifying the signature

Each delivery carries a Kommandr-Signature header of the form t=<unix>,v1=<hex>. The HMAC is over timestamp + "." + rawBody, so a captured delivery cannot be replayed later against a receiver that checks the age. Verify before you act on it — your endpoint is public and anyone who learns the URL can post to it.

const crypto = require('crypto')

function verify(secret, rawBody, header, toleranceSec = 300) {
  const m = /t=(\d+),v1=([a-f0-9]+)/i.exec(header || '')
  if (!m) return false
  const ts = Number(m[1])
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret)
    .update(ts + '.' + rawBody).digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(m[2], 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Conventions

Errors
Always `{ "error": { "code": "...", "message": "..." } }` with the HTTP status. The code is stable and machine-readable; the message is for a human and may change.
Money
Always integer cents, never decimals. `list_price_cents: 34000` is $340.00. Floating-point dollars are how an automation ends up emailing somebody about $84.50000001.
Dates
Always ISO 8601 with a timezone, e.g. `2026-08-25T14:03:11.000Z`.
Pagination
Cursor-based. A list response carries `next_cursor`; pass it back as `?cursor=`. `null` means the end. Offset pagination silently skips rows when new ones arrive mid-page, which is exactly what happens on a busy account.
Limits
`?limit=` defaults to 50 and is capped at 200.
Rate limit
120 requests a minute per key. Over it you get 429 with a `Retry-After` header.
Scopes
A key is read, write or propose. A write endpoint called with a read key returns 403 `insufficient_scope` and names the scope it needed.
Missing data
Fields we do not have are omitted, not returned as 0 or null. A zero claims something was measured; absence is the truth.

What no key can do

Published rather than hidden, because the ceiling on an integration is something you should be able to check before you install it. These are refused regardless of scope — there is no key, and no plan, that reaches them.

  • Release or edit a payout, or change a payout method or bank detail
  • Delete any record
  • Read or change billing, subscription or plan
  • Change team member permissions
  • Create or revoke API keys

Something missing? Email support@kommandros.com. The API is versioned — /api/v1 keeps its shape, and anything that changes it gets a new version rather than breaking what you built.