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:
- Fetch — SalesLobe pushes
lead.replied(and optionallycorty.suggested) to your webhook endpoint. - Decide — your agent fetches the full reply + thread and determines the response.
- 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.
| Scope | For |
|---|---|
leads.read | GET /v1/replies/:id — fetch the full reply + thread |
replies.send | POST /v1/replies/:id/send — send the response back |
webhooks.manage | Register 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
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);