INITIALIZING SYSTEMS...
DEVELOPER DOCS · v1

API & Webhooks

Read leads, push leads, hand them off downstream, and receive lifecycle events. The same surface Krew uses internally - versioned at /api/v1/* with signed outbound webhooks for state changes.

1. Authentication

All /api/v1/* requests require a Bearer token. Tokens are minted by the operator from the cockpit and shown to you exactly once at creation - store the plaintext somewhere safe (env var, secret manager).

Authorization: Bearer ai93_live_<your-key>

Each token has scopes (leads:read, leads:write, leads:handoff, sources:read) and may be locked to a specific business - in which case all reads filter to that business automatically and writes must omit / match the business ID.

Failed auth returns 401 with a JSON { error, message }. Insufficient scope returns 403.

2. Endpoints

GET/api/v1/leadsscope: leads:read

List leads. Filter by status, target_type, since, source, tag (one or comma-separated for AND), q (free-form match across contact name/email/phone/handle and notes), limit (max 500, default 100).

curl https://ai93.ca/api/v1/leads?status=new&limit=10 \
  -H "Authorization: Bearer ai93_live_..."
Response
{
  "leads": [
    {
      "id": "ld_a1b2c3...",
      "businessId": "biz_...",
      "targetType": "customer_lead",
      "source": "reddit",
      "status": "new",
      "displayName": "u/some_redditor",
      "context": { "subreddit": "HomeImprovement", "title": "...", "url": "..." },
      "createdAt": "2026-05-04T13:22:01.000Z",
      "handedOffAt": null,
      "handoffDestination": null
    }
  ],
  "count": 10
}
GET/api/v1/leads/{id}scope: leads:read

Fetch a single lead. Returns 404 (not 403) for leads outside your business scope.

GET/api/v1/leads.csvscope: leads:read

Same filters as /api/v1/leads, returns text/csv. Suitable for ingesting into spreadsheets, BI, or another CRM. Defaults to 5000 rows, max 25000 per request.

curl https://ai93.ca/api/v1/leads.csv?status=converted&since=2026-01-01 \
  -H "Authorization: Bearer ai93_live_..." \
  -o leads.csv
POST/api/v1/leadsscope: leads:write

Create a lead. Business-scoped keys must omit businessId or match their scope.

curl -X POST https://ai93.ca/api/v1/leads \
  -H "Authorization: Bearer ai93_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "source": "manual",
    "targetType": "customer_lead",
    "displayName": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+15555551234",
    "context": { "url": "https://janes-business.com" }
  }'
Response
{ "lead": { "id": "ld_...", "status": "new", ... } }
POST/api/v1/leads/{id}/handoffscope: leads:handoff

Mark a lead as handed off to a downstream system (e.g. Krew). Sets handedOffAt + handoffDestination and fires a lead.handed_off event.

curl -X POST https://ai93.ca/api/v1/leads/ld_abc/handoff \
  -H "Authorization: Bearer ai93_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "destination": "krew", "metadata": { "krewLeadId": "kl_xyz" } }'
GET/api/v1/sourcesscope: sources:read

List supported lead sources (reddit, realtor_ca, google_maps, public_tool, facebook_lead_ad, manual) with each source's enrichment field schema and target type mapping.

GET/api/v1/scrapers/runsscope: sources:read

Recent scraper run log. Filter by source, status, since, limit. Lets consumers monitor when AI93 last scraped each source and whether it's healthy.

3. Outbound webhooks

When a lead changes state, AI93 POSTs the event to every active subscription. Subscriptions are minted by the operator from the cockpit and locked to either all businesses or a single business.

Event types

TypeFires when
lead.createdA new lead is inserted (any source - scraper, public tool, FB ad, manual).
lead.status_changedStatus moves (e.g. new → contacted → replied).
lead.repliedAn inbound reply is recorded against the lead.
lead.convertedStatus moves to converted (operator-triggered or via API).
lead.handed_offPOST /handoff is called - the lead is now Krew's responsibility.

Payload shape

{
  "id": "evt_<16hex>",
  "type": "lead.converted",
  "createdAt": "2026-05-04T14:00:00.000Z",
  "consumerWorkspace": "krew",
  "data": {
    /* Lead object - same shape as GET /api/v1/leads */
  }
}

4. Signature verification

Every delivery includes X-AI93-Signature: sha256=<hex> and X-AI93-Timestamp. The signature is HMAC-SHA256 of {timestamp}.{rawBody} using your subscription's signing secret (the whsec_… string shown once at mint time).

Node.js (web crypto)

async function verify(req: Request, secret: string): Promise<boolean> {
  const sig = req.headers.get('x-ai93-signature') ?? '';
  const ts  = req.headers.get('x-ai93-timestamp') ?? '';
  const body = await req.text();

  // Reject anything older than 5 minutes - replay protection.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
  );
  const sigBuf = await crypto.subtle.sign(
    'HMAC', key, new TextEncoder().encode(`${ts}.${body}`)
  );
  const expected = 'sha256=' + Array.from(new Uint8Array(sigBuf))
    .map((b) => b.toString(16).padStart(2, '0')).join('');
  return sig === expected;
}

Other headers

  • X-AI93-Event - event type (e.g. lead.converted)
  • X-AI93-Event-Id - idempotency key; safe to dedupe on this
  • X-AI93-Attempt - 1 for first fire, 2+ for retries
  • User-Agent - AI93Platform/1.0 (or /1.0 (retry))

5. Retry behavior

Your endpoint should return any 2xx for success. Any non-2xx (or network error / timeout) is logged as failed and queued for retry.

Backoff schedule (cumulative from the initial fire): 1 min, 5 min, 30 min, 2 hr. After 5 total attempts the delivery dead-letters and the operator can force a manual retry from the cockpit.

Each fire is a separate row in our delivery log. Use X-AI93-Event-Id for idempotency - a successful retry will redeliver an event you already accepted, so dedupe on event id, not delivery id.

Request timeout: 10 seconds. Endpoints that need longer should ack fast and process async.

6. Errors

All endpoints return a JSON body on error:

{ "error": "missing_scope", "message": "API key lacks required scope: leads:write" }
  • 400 - bad input (missing required field, malformed body)
  • 401 - missing / invalid bearer token
  • 403 - token lacks required scope
  • 404 - resource not found (or out of business scope)
  • 500 - server error (will include a message)
Need a key, or want a new event type? Email support@ai93.ca.