Skip to main content

External AI Agents — Fetch, Decide, Send (Governed)

Connect your own AI agent to SalesLobe. Your agent receives replies via webhooks, decides on the response itself, and sends it back through the API. SalesLobe remains the governance layer: every send attempt is logged and subject to the organization's autonomy system.

The loop:

  1. Fetch — SalesLobe pushes lead.replied (and optionally corty.suggested) to your webhook endpoint.
  2. Decide — your agent fetches the full reply + thread and determines the response.
  3. Send — you POST the response to /v1/replies/:id/send. Depending on the autonomy level, it's sent immediately or placed in the human review queue.

Required scopes

Create a key in Settings → Connections → API Keys with exactly these three scopes. Don't use a * key for an agent integration — least privilege.

ScopeFor
leads.readGET /v1/replies/:id — fetch the full reply + thread
replies.sendPOST /v1/replies/:id/send — send the response back
webhooks.manageRegister and manage the webhook endpoint

Step 1 — Register your webhook

curl -X POST "$BASE/v1/webhooks" \
-H "X-SalesLobe-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My AI Agent", "url": "https://agent.example.com/saleslobe-webhook", "events": ["lead.replied", "corty.suggested"]}'

The response includes a one-time HMAC secret with prefix whsec_ — store it securely.

Step 2 — Verify every delivery (HMAC SHA-256)

Every delivery includes the header X-SalesLobe-Signature: sha256=<hex> — an HMAC SHA-256 of the raw request body, signed with your whsec_ secret. Never process an event without verifying the signature (see Signature Verification).

Step 3 — Fetch the full reply

warning

reply_text in the lead.replied payload and suggestion in corty.suggested are truncated to 500 characters. Never base your agent's decision on the webhook payload alone — always fetch the full body and thread:

curl "$BASE/v1/replies/<reply_id>" -H "X-SalesLobe-Key: $KEY"

Step 4 — Send your decision back

curl -X POST "$BASE/v1/replies/<reply_id>/send" \
-H "X-SalesLobe-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"body": "Hey John, great question — here is how that works..."}'

Did your agent use a Corty suggestion (from corty.suggested) as a starting point and edit it? Include original_suggestion and edited: true — SalesLobe logs the edit distance and learns from the adjustment.

Autonomy: what the 202 behavior means

  • A new agent (API key) starts at autonomy level 0: every send returns 202 {"status": "queued_for_review"} and the draft appears in the SalesLobe review queue for human approval.
  • A 202 is success-with-pending, not an error. The draft has arrived. Don't retry it, don't log it as a failure.
  • Idempotent: resending the same call returns the same 202. There's one pending draft per reply — the latest draft wins.
  • The org owner can grant higher autonomy levels as the agent builds a track record.
  • At sufficient autonomy level, the send goes out immediately and you get 200 {"status": "sent"} — your code doesn't need to change, only the response does.

Once a human approves the draft, the reply is sent through the normal flow; if you're subscribed to reply.sent, you'll receive a webhook event for it. Every send attempt — sent or queued — is visible to the organization in the audit trail (Decision Trail in the inbox).

Credits

Governance-only sends are metered; pricing will be finalized during the partner pilot. The full pipeline (Corty suggestion via /v1/suggest) still costs 1 credit. See Credits & Billing.

End-to-end example (Node)

const crypto = require('crypto');
const express = require('express');

const app = express();
const BASE = 'https://<your-project>.supabase.co/functions/v1/api-gateway';
const KEY = process.env.SALESLOBE_KEY; // sl_live_... (scopes: leads.read, replies.send, webhooks.manage)
const SECRET = process.env.WEBHOOK_SECRET; // whsec_...

// Keep the raw body around for signature verification
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

function verifySignature(req) {
const signature = req.get('X-SalesLobe-Signature') || '';
const expected = 'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.rawBody).digest('hex');
return signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/saleslobe-webhook', async (req, res) => {
if (!verifySignature(req)) return res.status(401).end();
res.status(200).end(); // ack immediately, process after

const { event, data } = req.body;
if (event !== 'lead.replied') return;

// 1. Fetch — full reply + thread (webhook payload is truncated to 500 chars)
const replyRes = await fetch(`${BASE}/v1/replies/${data.reply_id}`, {
headers: { 'X-SalesLobe-Key': KEY },
});
const { reply, thread } = await replyRes.json();

// 2. Decide — your own agent logic
const draft = await myAgent.decide({ reply, thread });

// 3. Send — back to SalesLobe
const sendRes = await fetch(`${BASE}/v1/replies/${data.reply_id}/send`, {
method: 'POST',
headers: { 'X-SalesLobe-Key': KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: draft.text }),
});
const result = await sendRes.json();

if (result.status === 'sent') {
console.log('Sent immediately:', result.reply_id);
} else if (result.status === 'queued_for_review') {
// Success-with-pending: draft awaits human approval. No retry.
console.log('Queued for review:', result.reply_id, '—', result.reason);
} else if (result.error) {
console.error('Send failed:', result.error.code, result.error.message);
}
});

app.listen(3000);