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 → API Keys with exactly these four 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; POST /v1/replies/:id/skip — record a deliberate skip
replies.approveGET /v1/replies?status=queued — list pending drafts; POST /v1/replies/:id/approve and /reject — approve or reject from your own UI
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 X-SalesLobe-Webhook-Signature: t=<unix>,v1=<hex> (HMAC SHA-256 of "<t>." + raw body, signed with your whsec_ secret) plus the legacy X-SalesLobe-Signature: sha256=<hex> over the raw body, and X-SalesLobe-Event-Id for dedupe. 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", "reason": "no_active_autonomy_grant"} 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 — a repeat send replaces it ("replaced": true, same suggestion_id); the latest draft wins.
  • The org owner can grant higher autonomy levels as the agent builds a track record. At level 1 the queue behavior is unchanged — only the reported reason becomes review_required_at_autonomy_level_1 and the Decision Trail records the level. See Autonomy Levels for the full table.
  • From level 2, the send goes out immediately and you get 200 {"status": "sent"} — your code doesn't need to change, only the response does. A second send on the same reply then returns 409 already_sent.

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

Approve, reject, or skip from your own system​

If you want to build your own review UI instead of using the SalesLobe dashboard:

  1. Self-inspect — GET /v1/me returns your key identity, scopes, autonomy level, and remaining credits. Use it as a health check at startup.
  2. List the queue — GET /v1/replies?status=queued returns all pending drafts with the original reply context (body is automatically stripped of quoted threads).
  3. Approve — POST /v1/replies/:id/approve with {"approver": "Name"} sends the draft and logs the approver in the Decision Trail. Triggers a reply.approved webhook event.
  4. Reject — POST /v1/replies/:id/reject with {"approver": "Name", "reason": "..."} removes the draft; the reply stays unanswered.
  5. Skip — POST /v1/replies/:id/skip with {"reason": "..."} records a deliberate decision not to respond. The skip is visible in the Decision Trail — silence is no longer invisible.

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, replies.approve, 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);